0. 개요
1. 큐브맵
2. 스카이박스
3. 큐브맵 반사
0. 개요
= 현재까지 구현한 프로젝트의 문제점 : 주변 환경에서 오는 빛도 같이 반사하는 기능이 없다.
= 큐브맵(cubemap)이라는 특수한 텍스쳐를 사용해 주변 환경의 모습을 담은 이미지를 제공한다.
= shader에서 텍스쳐를 샘플링해 광택이 있는 오브젝트가 반사하는 색상을 구한다.
= 큐브맵으로 씬의 스카이박스(skybox)를 렌더링해 경계를 없앨 것이다.
(++ 반사 효과는 주변 환경을 반영해서 실시간으로 렌더링되지는 않는다.
최신 라이팅 모델은 실제 세계처럼 환경 라이팅을 포함하는 경우가 많지만, 해당 프로젝트에서는 포함되어 있지 않다.)
1. 큐브맵
= 메모리에 저장된 6개의 개별 텍스쳐로 구성된 텍스쳐 중 하나.
= 1개의 텍스쳐처럼 처리할 수 있다.
= 6개의 텍스쳐를 육면체의 면처럼 다룬다.
1.1. 큐브맵 샘플링
= 샘플링할 때 방향 벡터를 사용한다. (2D 텍스쳐는 UV 좌표 사용)
(샘플링 방식 : 큐브맵 육면체 중앙에 앉아 있는 오브젝트가 정면에 있는 픽셀을 바라보면, 오브젝트에게서 시작되어 (0, 0, 1) 방향으로 나아가 육면체 픽셀에 이르는 벡터를 그리는 방식.)
코드는 아래와 같다.
vec3 nrm = normalize(fragNrm);
vec4 cubemap = textureCube(cubemap, nrm);
= fragment는 큐브맵 중앙에 위치하고, 큐브맵 한 면을 바라본다고 가정한다.
= 노말 벡터를 사용해 큐브맵을 샘플링한다.
1.2. 큐브맵 로드
큐브맵을 openframeworks는 지원하지 않아서 프로젝트가 지원하도록 코드를 작성해야 한다.
해당 자료에서는 큐브맵을 불러오는 과정을 처리할 ofxEasyCubemap 클래스를 준비했는데, 사용하는 법만 간단하게 확인해본다.
(사진은 11강 에셋 폴더에서 다운받아서 사용한다.)
ofxEasyCubemap cubemap;
cubemap.load("cube_front.jpg", "cube_back.jpg", "cube_left.jpg", "cube_right.jpg", "cube_top.jpg", "cube_bottom.jpg"
//유니폼 변수로 shader에서 사용할 경우
shd.setUniformTexture("name", cuubemap.getTexture(), 1);
= 생성자는 큐브맵의 각 면을 구성하는 텍스쳐 경로를 받고, 각 텍스쳐를 부러온다.
= ofImage와 차이점은 ofxEasyCubemap은 명시적으로 getTexture()를 호출해야 한다는 것이다.
1.3. 큐브맵 렌더링
우선 큐브랩을 렌더링하는 shader를 만들 것이다.
1.3.1. vertex shader 작성
UV좌표를 사용하지 않으므로 fragment에 전달할 vertex 위치를 작성한다.
작성하면 아래와 같다.
cubemap.vert
#version 410
layout (location = 0) in vec3 pos;
uniform mat4 mvp;
out vec3 fromCam;
void main()
{
fromCam = pos;
gl_Position = mvp * vec4(pos, 1.0);
}
= 오브젝트 공간의 위치를 내보내는 것이 mesh의 오브젝트 공간 원점에서 빠져나오는 벡터라고 생각한다.
=> 각 vertex 위치는 vertex가 큐브맵을 바라볼 때 향하는 방향 벡터이다.
=> 오브젝트 공간 벡터를 내보내는 것 (= fragment shader에서 보간된 벡터를 구하는 것)은 기존 vertex shader에서 UV를 내보내는 것과 동일하다.
1.3.2. fragment shader 작성
= vertex shader가 보내준 보간된 오브젝트 공간 위치 값을 사용한다.
cubemap.frag
#version 410
uniform samplerCube envMap;
in vec3 fromCam;
out vec4 outCol;
void main()
{
outCol = texture(envMap, fromCam);
}
= 큐브맵은 텍스쳐가 아니어서 새로운 타입의 샘플러를 사용한다.
++ samplerCube는 샘플링할 때 vec3 값을 필요로 한다.
1.3.3. cpp 작성
헤더파일로 큐브맵 텍스쳐, 쉐이더, draw함수, mesh 등을 추가한다.
++ 그리고 ofxEasyCubemap.h, ofxEasyCubemap.cpp을 ofApp.cpp와 같은 위치에 추가한다.
#include "ofxEasyCubemap.h"
void drawCube(glm::mat4& proj, glm::mat4& view);
ofShader cubemapShader;
ofxEasyCubemap cubemap;
ofMesh cubeMesh;
setup()에서 큐브 mesh, shader, 텍스쳐를 불러온다.
void ofApp::setup() {
ofDisableArbTex();
ofEnableDepthTest();
cubeMesh.load("cube.ply");
cubemapShader.load("cubemap.vert", "cubemap.frag");
cubemap.load( "cube_front.jpg", "cube_back.jpg",
"cube_right.jpg", "cube_left.jpg",
"cube_top.jpg", "cube_bottom.jpg");
//생략
}
draw함수에 drawCube()를 추가하고, 회전된 형태의 큐브맵을 보이게 하기 위해 회전행렬을 추가한다.
void ofApp::draw(){
//생략
mat4 rotation = mat4();
mat4 view = inverse(translate(cam.pos)) * rotation;
drawCube(proj, view);
}
drawCube()함수를 작성한다.
void ofApp::drawCube(glm::mat4& proj, glm::mat4& view)
{
using namespace glm;
static float rotAngle = 0.0;
rotAngle += 0.2f;
mat4 r = rotate(radians(rotAngle), vec3(0, 1, 0));
mat4 s = scale(vec3(0.4, 0.4, 0.4));
mat4 model = translate(vec3(0.0, 0.75, 0.0f)) * r *s;
mat4 mvp = proj * view * model;
ofShader& shd = cubemapShader;
shd.begin();
shd.setUniformMatrix4f("mvp", mvp);
shd.setUniformTexture("envMap", cubemap.getTexture(), 0);
shd.setUniform3f("cameraPos", cam.pos);
cubeMesh.draw();
shd.end();
}
draw()의 drawCube()만 남기고 나머지를 주석처리하면 작성하면 결과는 아래와 같다.

= 위 그림처럼 면 사이 경계가 잘 보이지 않아야 한다.
++ 필자는 ofxEasyCubemap 헤더파일과 클래스 파일을 넣었을 때 아래와 같은 에러 로그가 떴다.
"error: implicit instantiation of undefined template 'ofPixels_<unsigned char>'"
그래서 ofImage 부분에서 계속 버그가 뜨길래 아래 링크의 공식 문서를 들어가서 내용을 살펴보았다.
https://openframeworks.cc/documentation/graphics/ofImage/
ofImage | openFrameworks
openframeworks.cc
그 결과 ofxEasyCubemap.h에서 아래 헤더만 남기고 전부 주석처리했더니 문제가 해결되었다.
#include "ofMain.h"
2. 스카이박스
2.1. 스카이박스
= 배경을 채워 씬을 사실적으로 보여주게 하는 기법에 사용된다.
= 언제나 카메라와 같은 곳에 위치하는 거대한 육면체
(카메라는 언제나 육면체 안에 있고, 카메라가 움직여도 육면체는 보이지 않는 것처럼 보인다.)
2.2. 구현
++ 화면에 보이는 대상 중 스카이박스가 카메라로부터 가장 멀리 있는 대상이 되게 하려면?
= 카메라가 그려지는 거리를 조정할 때마다 지속적으로 스카이박스 mesh 크기 조정은 x.
=> 행렬에 따라 vertex shader가 육면체 mesh 크기를 조절해야 한다.
(++ 카메라가 그려지는 거리는 투영 행렬을 만들면서 설정한다.)
위 과정을 구현하기 위해 투영 행렬을 어떻게 다루는지 공부해야 한다.
2.3. 원근 분할
= vertex shader로부터 vertex 위치가 나오면 GPU가 수행하는 과정.
= gl_Position 벡터의 XYZ 컴포넌트를 W 컴포넌트로 나누는 것.
= 멀리 있는 대상을 수평선 상의 중심으로 옮기는 방식을 사용한다. (대상이 3D 공간에 있는 것처럼 보인다.)
= 좌표의 W값이 클수록 대상을 화면 중앙으로 더 끌어당긴다.
= 렌더링된 이미지에 원근 효과를 더하고, 위치와 곱해졌을 때 멀리있을수록 W값이 커진다.
= 투영 행렬에 의해 얻게 되는 위치 벡터 W값 : glm::perspective()에서 near, far인자에 의해 결정된다.
=> 원근 효과 : 카메라 프러스텀의 모습과 일치.
++ 직교 투영 행렬의 경우, 변환하는 모든 위치 컴포넌트가 1이 되도록 만들기 때문에 원근 분할에 대해 고려할 필요가 없음.
= 씬에서 볼 수 있는 대상 중에서 가장 멀리 있어야 하는 스카이박스는 원근 효과가 필요 없다.
2.4. 스카이박스와 원근 분할
관련된 vertex shader 코드는 아래와 같다.
#version 410
//생략
void main()
{
fromCam = pos;
gl_Position = (mvp * vec4(pos, 1.0)).xyww;
}
= 원근 분할은 스카이박스에 적용하면 안된다.
(스카이박스의 목표 : 무한히 뻗어있는 하늘(또는 배경)을 보여줘야 하기 때문.)
=> NDC 변환을 거쳐 대상들이 갖는 Z값을 -1~1로 제한
(1을 초과할 경우 카메라 프러스텀 밖에 위치.)
=> vertex 가장 먼 거리려면 Z값 1.0
=> 벡터의 Z, W를 같은 값으로 설정해 해결.
(원근 분할이 Z / W를 수행)
2.5. 스카이박스 그리기
2.5.1. vertex shader 작성
2.4에서 배운 방식을 사용해서 작성하면 아래와 같다.
skybox.vert
#version 410
layout (location = 0) in vec3 pos;
out vec3 fromCam;
uniform mat4 mvp;
void main()
{
fromCam = pos;
gl_Position = (mvp * vec4(pos, 1.0)).xyww;
}
2.5.2. fragment shader 작성
이전에 작성한 cubemap.frag 내용을 그대로 사용한다.
cubemap.frag
#version 410
uniform samplerCube envMap;
in vec3 fromCam;
out vec4 outCol;
void main()
{
outCol = texture(envMap, fromCam);
}
2.5.3. cpp 작성
우선 헤더에 생성할 skybox shader과 drawSkybox함수를 선언한다.
ofApp.h
void drawSkybox(DirectionalLight& dirLight, glm::mat4& proj, glm::mat4& view);
ofShader skyboxShader;
skybox shader에 vertex shader, skybox shader을 로드하도록 구성한다.
skyboxShader.load("skybox.vert", "cubemap.frag");
아래와 같이 drawSkybox() 함수를 작성하고, draw() 함수에 해당 함수를 추가한다.
void ofApp::drawSkybox(DirectionalLight& dirLight, glm::mat4& proj, glm::mat4& view)
{
using namespace glm;
mat4 model = translate(cam.pos);
mat4 mvp = proj * view * model;
ofShader& shd = skyboxShader;
glDepthFunc(GL_LEQUAL);
shd.begin();
shd.setUniformMatrix4f("mvp", mvp);
shd.setUniformTexture("envMap", cubemap.getTexture(), 0);
cubeMesh.draw();
shd.end();
glDepthFunc(GL_LESS);
}
//생략
void ofApp::draw(){
//생략
drawSkybox(dirLight, proj, view);
}
= OpenGL함수인 glDepthFunc을 호출해서 끝없는 배경을 구현한다.
++ 왜 OpenGL함수를 사용할까?
= openframeworks가 사용하는 깊이 비교 함수 모드는 GL_LESS이지만, 스카이박스에서 사용하기는 어렵다.
(GL_LESS는 NDC에서 1.0보다 작은 vertex만 그리기 때문)
= 깊이 비교 모드를 바꿔야 한다. (openframeworks는 OpenGL함수 호출이 있어야 깊이 비교 모드를 변경 가능)
=> openframeworks의 기본 깊이 비교 함수를 사용하면 Z값이 1.0인 스카이박스를 볼 수 없어서 OpenGL함수 호출을 통해 깊이 비교 모드를 변경해야 한다.
=> GL_LEQUAL을 깊이 비교 모드로 채택한다.
(NDC에서 Z값이 1.0 이하인 값들을 그리므로 Z값이 1인 스카이박스를 볼 수 있다.)
++ 스카이박스를 그린 직후 깊이 비교 모드를 이전 상태로 되돌려서 현재 프로젝트에서는 다른 부분에 영향을 주지 않는다.
위와 같이 코드를 작성하면 결과는 아래와 같다.

++ 백페이스 컬링
= mesh 페이스 뒷면을 렌더링하지 않음으로써 렌더링 시간을 단축하는 최적화 기법.
(해당 기능을 활성화했다면 스카이박스를 볼 수 없음.
카메라가 육면체 mesh 안에 있기 때문 = 어디를 보든 모든 mesh 페이스의 뒷면을 바라보게 된다.)
= 백페이스 컬링을 사용할 경우, 페이스가 안쪽을 향하는 다른 육면체 mesh 사용 / 깊이 비교 함수에서 채택한 방식처럼 컬링을 비활성화해야 한다.
3. 큐브맵 반사
스카이박스에 사용한 큐브맵을 방패와 물의 라이팅 계산에 추가할 것이다.
3.1. 환경 맵(envorionment map)
= 큐브맵을 주변 환경의 라이팅 정보를 제공할 용도로 사용하여 광택이 있는 부분이 그 환경을 반사하는 듯한 모습을 구현하는 것.
= 환경의 색상을 mesh에 입히는 텍스쳐.
++ 그러나 라이팅 모델은 환경 라이팅을 제공하지 않아 환경 맵을 제공된 .
3.1.1. 실제 디퓨즈 값을 가지지 않은 오브젝트(물)의 디퓨즈 색상 변경
(이전에는 물의 디퓨즈 값을 하드 코딩해서 넣었으니 해당 부분을 수정하는 것이다.)
= 물에 반사를 더하기 위해 환경 맵에서 샘플링한 색상이 적용되도록 디퓨즈 라이팅을 수정한다.
= water.frag에서 envMap이라는 유니폼 변수를 선언해 아래와 같이 로직을 수정한다.
수정된 코드
water.frag
uniform samplerCube envMap;
vec3 diffCol = texture(envMap, (reflect(-viewDir, nrm))).xyz * lightCol * diffAmt;
= 더 이상 vertex 위치를 사용해 큐브맵의 색상을 구하지 않는다.
=> 환경이 fragment에 더하는 색상은 노말 벡터 방향(fragment 방향)에 존재하는 광경의 색상이다.
= 물 효과가 노말 벡터를 움직여서 더 사실적으로 보인다.
cpp도 drawWater() 함수에 아래 코드를 추가한다.
shd.setUniformTexture("envMap", cubemap.getTexture(), 1);
그러면 결과는 아래와 같다.

3.1.2. 실제 디퓨즈 값을 가진 오브젝트(방패)의 디퓨즈 색상 변경
= 방패 mesh의 색상을 적용시키면서 큐브맵 반사를 더해야 하므로, 환경 효과는 약하게 지원된다.
= 블린-퐁 라이팅의 경우 환경 라이팅이 포함되어 있지 않다.
=> 여기서는 라이팅 계산에 활용되는 lightCol변수와 합치고, lightCol을 SceneLight로 수정하면 수정된 부분은 아래와 같다.
blinnphongTexture.frag
uniform samplerCube envMap;
//생략
vec3 envSample = texture(envMap, reflect(-viewDir, nrm)).xyz;
vec3 sceneLight = mix(lightCol, envSample + lightCol * 0.5, 0.5);
float diffAmt = max(0.0, dot(nrm, lightDir));
vec3 diffCol = texture(diffuseTex, fragUV).xyz * sceneLight * diffAmt;
float specAmt = max(0.0, dot(halfVec, nrm));
float specBright = pow(specAmt, 4.0);
vec3 specCol = texture(specTex, fragUV).x * sceneLight * specBright;
= mix()에서 어느 정도의 라이트 색상을 큐브맵에서 구한 색상과 더해 큐브맵에 의해 mesh가 어두워지는 것을 방지한다.
(보통 큐브맵이 사용되는 라이트 색상보다 어둡기 때문.)
cpp에 drawShield()에 아래와 같이 유니폼 변수 선언 부분을 추가한다.
shd.setUniformTexture("envMap", cubemap.getTexture(), 3);
적용하면 결과는 아래와 같다.

(현재 밝은 큐브맵, 하얀 디렉셔널 라이트, 강한 앰비언트 라이트를 가져서 효과가 잘 보이지 않을 수 있다. 결과를 더 자세히 보고 싶다면 풍부하고 좀 더 어두운 큐브맵과 약한 앰비언트 라이트를 사용하면 된다.)
해당 큐브맵 이미지와 자료는 깃헙 링크를 참조한다.
이번에는 큐브맵과 스카이박스를 이용해 환경 라이트를 여러 mesh들에 적용하는 방법을 배우고, 원근 분할을 통해 원근 투영을 적용해 배경을 렌더링하는 방법도 배웠다.