이전에 배운 노말 매핑을 이용해 물 shader를 만들어보고, 노말 매핑을 현재 사용하는 방법에 대해 알아볼 것이다.
3. 물 효과
4. 노말 매핑의 현재
3. 물 효과
= 노말 매핑은 UV 스크롤링과 더해져 mesh 표면에 물결 효과를 만든다.
= 직접 실습하기 위해 깃허브 11장 폴더에서 plane.ply, water_nrm.png를 다운받는다.
3.1. vertex shader 생성
3.1.1. 애니메이션 효과를 위한 시간 적용 및 UV값 여러 개 생성
= mesh.vert와 유사하지만, 시간 값을 유니폼 변수로 전달한다. (애니메이션 생성 목적)
= 두 개의 UV 좌표를 생성한다. (서로 다른 방향으로 스크롤링 목적)
위 내용을 기반으로 추가할 내용은 아래와 같다.
water.vert
//생략
out vec2 fragUV;
out vec2 fragUV2;
uniform float time;
float t = time * 0.05;
float t2 = time * 0.02;
fragUV = vec2(uv.x+t, uv.y) * 3.0f;
fragUV2 = vec2(uv.x+t2, uv.y-t2) * 2.0f;
//생략
= time에 각각 다른 상수를 곱한다.
(2개의 UV좌표를 서로 다른 속도로 스크롤링 가능.)
= 시간 변수를 mesh에서 가져온 UV좌표에 더한다.
(두 개의 노말 맵을 겹친 상태에서 서로 다른 방향으로 스크롤하기 위해 서로 다른 연산을 수행한다.)
=> 이렇게 노말 맵의 시간에 따른 연산을 적용하는 부분은 일반적으로 적절한 조합을 계속 찾아야 한다.
= fragUV 연산에서 노말 맵의 랩모드는 반복으로 설정해야 한다.
=> vertex shader의 UV가 0-1을 넘어도 문제가 없다.
++ UV에 1 이상의 값을 곱하면 노말 맵 텍스쳐를 타일링하는 효과를 얻는다.
= fragUV에서 서로 다른 스칼라값을 각각 곱해주면 스크롤될 때 텍스쳐가 반복되는 느낌이 줄어든다.
(반복의 느낌을 줄이기 위해 서로소인 수를 곱한다.)
해당 조건을 적용한 vertex shader는 아래와 같다.
water.vert
3.2. fragment shader
3.2.1. 물 mesh 표면 입력값 수정
= blinnphongTexture.frag와 유사하지만 mesh 표면을 제어하는 부분을 수정한다.
(++ 실제 게임 프로젝트에서는 해당 값들이 유니폼 변수로 노출되어 있어서 cpp에서 오브젝트 단위로 설정 가능한다.
그러나 여기서는 1개의 오브젝트만 존재해서 약식으로 코딩했다.)
수정한 부분은 아래와 같다.
water.frag
float diffAmt = max(0.0, dot(nrm, lightDir));
vec3 diffCol = vec3(0.3, 0.3, 0.4) * lightCol * diffAmt;
float specAmt = max(0.0, dot(halfVec, nrm));
float specBright = pow(specAmt, 512.0);
vec3 specCol = lightCol * specBright;
= 물의 디퓨즈 라이팅에 디퓨즈 텍스쳐가 아닌 색상 벡터값(코드에선 파랑색)을 직접 사용한다.
= 스펙큘러 계산에서 광택 지수를 높인다.
(물은 거울과 같은 특성이기 때문)
= 스펙큘러 텍스쳐를 사용하지 않아서 specCol에서 텍스쳐 샘플을 제거한다.
3.2.2. 겹쳐진 노말 맵 스크롤링
= 두 벡터를 합치기 위해 두 벡터를 더하고 정규화한다.
(노말 맵 텍스쳐 샘플 블렌딩, 서로 다른 노말 맵 텍스쳐를 사용할 때도 적용)
water.frag
in vec2 fragUV2;
void main()
{
vec3 nrm = texture(normTex, fragUV).rgb;
nrm = (nrm * 2.0 - 1.0);
vec3 nrm2 = texture(normTex, fragUV2).rgb;
nrm2 = (nrm2 * 2.0 - 1.0);
nrm = normalize(TBN * (nrm + nrm2));
//생략
= 텍스쳐를 적용해서 움직이는 효과를 가진 오브젝트를 만드려면, 근본적으로 해당 상황에 맞는 텍스쳐를 잘 골라야 한다.
++ 좋은 노말 맵 텍스쳐를 고르는
= 강한 수직/수평 패턴이 보이는 노말 맵은 지양한다.
= 타일링되었을 때 경계를 찾기 힘든 텍스쳐는 지향한다.
위와 같은 효과를 적용한 fragment shader는 아래와 같다.
water.frag
3.3. cpp 수정
3.3.1. 노말 맵 로드 및 랩 모드 반복으로 설정
= 물을 생성할 때 필요한 노말 맵, mesh, shader를 헤더파일에 설정한다.
ofApp.h
ofMesh planeMesh;
ofImage waterNrm;
ofShader waterShader;
= 노말 맵과 shader 로드 및 mesh가 노말 맵이 적용되도록 탄젠트 연산을 수행한다.
= 텍스쳐에 대해 랩 모드를 설정한다.
ofApp.cpp
void ofApp::setup() {
//생략
planeMesh.load("plane.ply");
calcTangents(planeMesh);
waterShader.load("water.vert", "water.frag");
waterNrm.load("water_nrm.png");
waterNrm.getTexture().setTextureWrap(GL_REPEAT, GL_REPEAT);
}
3.3.2. shield, water mesh를 같이 렌더링하기 위한 draw() 함수 분리
= 물과 방패를 따로 각각 그려서 나타낼 것이기 때문에 두 오브젝트를 렌더링하는 부분을 함수로 분리한다.
= 두 mesh가 한 화면에 보이게 하기 위해 뷰와 모델 행렬을 아래와 같이 수정한다.
ofApp.cpp
void ofApp::draw(){
using namespace glm;
cam.pos = vec3(0, 0.75f, 1.0f);
cam.fov = radians(90.0f);
float aspect = 1024.0f / 768.0f;
DirectionalLight dirLight;
dirLight.direction = normalize(vec3(0.5, -1, -1));
dirLight.color = vec3(1, 1, 1);
dirLight.intensity = 1.0f;
DirectionalLight waterLight;
waterLight.direction = normalize(vec3(0.5, -1, 1));
waterLight.color = vec3(1, 1, 1);
waterLight.intensity = 1.0f;
mat4 view = inverse(translate(cam.pos));
mat4 proj = perspective(cam.fov, aspect, 0.01f, 10.0f);
drawShield(dirLight, proj, view);
drawWater(waterLight, proj, view);
}
= 물과 방패 각각의 mesh를 비추는 라이트의 하이라이트 방향이 다르다.
(물 mesh의 스펙큘러 하이라이트를 보이게 하기 위해 물 평면에 반사된 라이트가 카메라를 향해야 하기 때문.)
= 방패를 비추는 라이트는 물과 반대 방향(카메라로부터 씬을 향하는 방향)이다.
그리고 헤더파일에 해당 함수들을 선언한다.
ofApp.h
void drawWater(DirectionalLight& dirLight, glm::mat4& proj, glm::mat4& view);
void drawShield(DirectionalLight& dirLight, glm::mat4& proj, glm::mat4& view);
그리고 아래와 같이 선언한 함수들을 작성한다.
void ofApp::drawWater(DirectionalLight& dirLight, glm::mat4& proj, glm::mat4& view)
{
using namespace glm;
static float t = 0.0f;
t += ofGetLastFrameTime();
vec3 right = vec3(1, 0, 0);
mat4 rotation = rotate(radians(-90.0f), right);
mat4 model = rotation * scale(vec3(5.0, 4.0, 4.0));
mat4 mvp = proj * view * model;
mat3 normalMatrix = mat3(transpose(inverse(model)));
ofShader& shd = waterShader;
shd.begin();
shd.setUniformMatrix4f("mvp", mvp);
shd.setUniformMatrix4f("model", model);
shd.setUniformMatrix3f("normal", normalMatrix);
shd.setUniform3f("meshSpecCol", glm::vec3(1, 1, 1));
shd.setUniformTexture("normTex", waterNrm, 0);;
shd.setUniform1f("time", t);
shd.setUniform3f("ambientCol", glm::vec3(0.1, 0.1, 0.1));
shd.setUniform3f("lightDir", getLightDirection(dirLight));
shd.setUniform3f("lightCol", getLightColor(dirLight));
shd.setUniform3f("cameraPos", cam.pos);
planeMesh.draw();
shd.end();
}
void ofApp::drawShield(DirectionalLight& dirLight, glm::mat4& proj, glm::mat4& view)
{
using namespace glm;
mat4 model = translate(vec3(0.0, 0.75, 0.0f));
mat4 mvp = proj * view * model;
mat3 normalMatrix = mat3(transpose(inverse(model)));
ofShader& shd = blinnphongShader;
shd.begin();
shd.setUniformMatrix4f("model", model);
shd.setUniformMatrix4f("mvp", mvp);
shd.setUniform3f("meshSpecCol", glm::vec3(1, 1, 1));
shd.setUniform3f("meshCol", glm::vec3(1, 0, 0));
shd.setUniform3f("lightDir", getLightDirection(dirLight));
shd.setUniform3f("lightCol", getLightColor(dirLight));
shd.setUniform3f("cameraPos", cam.pos);
shd.setUniformMatrix3f("normal", normalMatrix);
shd.setUniform3f("ambientCol", glm::vec3(0.1, 0.1, 0.1));
shd.setUniformTexture("diffuseTex", diffuseTex, 0);
shd.setUniformTexture("specTex", specTex, 1);
shd.setUniformTexture("normTex", nrmTex, 2);
shieldMesh.draw();
shd.end();
}
그리고 해당 결과를 출력하면 아래와 같다.

++ water.frag에서 time 변수들에 곱하는 값을 0.05로 통일하면 아래와 같다.

= 전체적으로 라이트에 의해 하이라이트와 그림자가 생기는 부분은 유사하지만, 해당 텍스쳐끼리 충돌해서 생기는 물결이 상대적으로 적어서 fig 1보다 덜 정적인 느낌이 난다.
++ waterLight, dirLight의 방향을 -1로 통일하면 아래와 같다.
(direction = normalize(0.5, -1, -1)

= 서로 하이라이트 방향이 같고, 카메라로부터 씬을 향하는 방향으로 발사되고 있어 물의 반사광이 보이지 않는다.
(반사광을 보려면 씬에서 카메라를 향하는 방향으로 설정해야 한다.)
++ 물결이 전진하는 방향을 바꾸고 싶다면 water.vert값을 수정한다.
아래와 같이 수정한 결과, 카메라 기준으로 10시 방향으로 이동하는 물결이 출력된다.
fragUV = vec2(uv.x-t, uv.y-t) * 3.0f;
fragUV2 = vec2(uv.x+t2, uv.y-t2) * 2.0f;

4. 노말 매핑의 현재
= 꼭 노말 매핑의 정석대로 하는 건 아니다.
(노말 벡터를 탄젠트에서 월드 공간으로 변환하는 것이 아닌, 다른 벡터들을 월드에서 탄젠트 공간으로 변환한다.)
= 일부 shader는 모든 벡터를 뷰 공간으로 가져가서 연산한다.
(vertex shader에 계산을 집중하기 위함)
이번에는 전체적으로 노말 맵과 노말 매핑에 대해 배우고, 탄젠트 벡터 연산도 함께 사용하면서 물과 광택이 들어간 방패를 렌더링하였다.