본문 바로가기
카테고리 없음

쉐이더 심화학습09-노말 매핑(물 효과)

by mazayong 2024. 4. 7.

이전에 배운 노말 매핑을 이용해 물 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();
}

 

 

그리고 해당 결과를 출력하면 아래와 같다.

<fig 1>

 

 

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

<fig 2>

= 전체적으로 라이트에 의해 하이라이트와 그림자가 생기는 부분은 유사하지만, 해당 텍스쳐끼리 충돌해서 생기는 물결이 상대적으로 적어서 fig 1보다 덜 정적인 느낌이 난다.

 

 

++ waterLight, dirLight의 방향을 -1로 통일하면 아래와 같다.

(direction = normalize(0.5, -1, -1)

<fig 3>

= 서로 하이라이트 방향이 같고, 카메라로부터 씬을 향하는 방향으로 발사되고 있어 물의 반사광이 보이지 않는다.

(반사광을 보려면 씬에서 카메라를 향하는 방향으로 설정해야 한다.)

 

 

++ 물결이 전진하는 방향을 바꾸고 싶다면 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에 계산을 집중하기 위함)

 

이번에는 전체적으로 노말 맵과 노말 매핑에 대해 배우고, 탄젠트 벡터 연산도 함께 사용하면서 물과 광택이 들어간 방패를 렌더링하였다.