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

쉐이더 심화학습08-라이팅 모델(스펙큘러 라이팅, 앰비언트 라이팅)

by mazayong 2024. 3. 31.

이전에 배운 디퓨즈 라이팅을 포함해 두 종류의 라이팅 모델에 대해 배우고, 모델을 통합하는 방법과 일반적으로 사용되는 통합되는 라이팅 모델에 대해 공부할 것이다.

 

 

0. 렌더링에 라이팅 연산을 사용하는 방법

1. 스펙큘러 라이팅

2. 디퓨즈와 스펙큘러 라이팅 합치기

3. 앰비언트 라이팅

 

 

 

 

 

 

 

 

 

 

 

 

0. 렌더링에 라이팅 연산을 사용하는 방법

= 이전에 배웠던 디퓨즈 라이팅만으로는 모든 대상을 렌더링하기 어렵기 때문에 여러 라이팅 연산들을 적절히 조합해야 한다.

(디퓨즈 라이팅 : 광택이 없는 표면을 가진 대상을 렌더링하기 위한 방법이기 때문.)

= 라이팅 모델(lighting model) : 여러 종류의 라이팅 계산을 합쳐 모든 대상의 라이팅을 계산할 수 있는 하나의 공식. (게임에서 주로 사용)

= 보통 라이팅 모델을 게임엔진에서 만들어서 활용한다.

 

= 여기서는 스펙큘러와 앰비언트를 살펴보고, 기존 shader에 두 라이팅을 통합한다. 그리고 통합한 결과인 블린-퐁 라이팅 모델을 공부할 것이다.

 

 

 

 

 

 

 

 

 

 

 

1. 스펙큘러 라이팅

1.1. 스펙큘러 라이팅(specular lighting)

= 광택이 있는 대상을 렌더링할 때 사용된다.

(빛이 매끄러운 표면과 상호작용할 때를 재현.)

= 표면에 도달한 빛은 반사각(angle of reflection)으로만 반사된다.

(무작위로 흩어지는 빛의 양이 줄어듦)

 

= 육면체 : 디퓨즈 라이팅을 사용했을 때와 유사.

(게임 카메라를 향해 반사되는 각도로 비추는 빛이 없기 때문)

= 구, 주전자와 같은 곡면을 가진 오브젝트들은 스펙큘러 라이팅과 디퓨즈 라이팅의 차이가 뚜렷하다.

 

++ 거울이 아닌 이상 거의 모든 물체는 적어도 약간의 디퓨즈 라이팅을 받는다. 

++ 거울과 같이 매우 매끄러운 물체는 반사가 있어야 정상적으로 보인다.

 

 

1.2. 디퓨즈 라이팅과의 차이

= 빛이 반사돼 나오는 방향에서 바라보면 매우 밝은 하이라이트가 보이지만, 바라보는 각도가 빛 반사 방향의 수평에서 멀어질수록 하이라이트는 사라진다.

(디퓨즈 라이팅은 바라보는 각도와 독립적으로 빛의 반사가 연산된다.)

 

 

1.3. 스펙큘러 셰이더(specular shader)

 

1.3.1. fragment shader 생성

= 디퓨즈 라이팅처럼 벡터 간의 내적을 구하는 것이 핵심이다.

 

1.3.1.1. 완벽하게 반사되는 광선이 fragment로부터 튕겨 나오는 방향

= reflect()를 통한 계산 가능.

 

vec3 refl = reflect(-lightDir, normalVec);

= 인자 : (라이트 벡터, 현재 fragment normal 벡터)

= 두 인자의 차원이 같아야 모든 차원의 벡터가 사용 가능.

(들어오는 라이트 벡터가 mesh 표면을 향한다고 예상하기 때문.)

= 라이트 벡터 방향 반전 필요.

(디퓨즈 라이팅 계산 과정에서 라이트 방향이 mesh 표면에서 밖을 향했다고 반전했기 때문.)

 

1.3.1.2. fragment 위치로부터 카메라를 향하는 방향 벡터.

= 림 라이트 효과를 만들 때 구한 벡터.

= vertex shader이 내보낸 fragmentWorldPosition 데이터, 유니폼 변수로 받은 cameraPosition 데이터를 사용해 아래와 같이 연산한다. 

vec3 toCam = normalize(camPos - fragWorldPos);

 

 

1.3.1.3. 두 벡터로 연산 및 하이라이트 제어하기

= 구한 두 벡터의 내적을 구한다.

= 내적 결과를 거듭제곱해 하이라이트의 모습을 재현한다.

= 지수가 클수록 스펙큘러 하이라이트가 더 집중해서 맺히는 더 매끄러운 표면을 재현한다.

++ 광택값(shiniess) : 내적을 거듭제곱하는 지수

위 방법을 사용해 스펙큘러 라이팅의 밝기를 계산하면 아래와 같다.

float specAmt = max(0.0, dot(refl, viewDir));
float specBrightness = pow(specAmt, 16.0);

 

 

1.3.1.1 ~ 1.3.1.3을 적용해서 fragment shader를 만들면 아래와 같다.

 

specular.frag

#version 410

uniform vec3 lightDir;
uniform vec3 lightCol;
uniform vec3 meshCol;
uniform vec3 meshSpecCol;
uniform vec3 cameraPos;

in vec3 fragNrm;
in vec3 fragWorldPos;
out vec4 outCol;

void main()
{
    vec3 nrm = normalize(fragNrm);
    vec3 refl = reflect(-lightDir, nrm);
    vec3 viewDir = normalize(cameraPos - fragWorldPos);

    float specAmt = max(0.0, dot(refl, viewDir));
    float specBright = pow(specAmt, 16.0);

    outCol = vec4(lightCol * meshSpecCol * specBright, 1.0);
}

 

 

 

1.3.2. vertex shader 수정

= 이전 디퓨즈 라이팅에서 했던 방식과 동일하게 수정한다.

= 색상 좌표에 uv 데이터를 적용한다는 것에 유의한다.

 

lit_mesh.vert

#version 410

layout (location = 0) in vec3 pos;
layout (location = 2) in vec3 nrm;
layout (location = 3) in vec2 uv;

uniform mat4 mvp; 
uniform mat4 model; 
uniform mat3 normal;

out vec2 fragUV;
out vec3 fragNrm;
out vec3 fragWorldPos; 

void main(){
	fragNrm = normal * nrm;
	fragUV = uv;
	fragWorldPos = (model * vec4(pos, 1.0)).xyz; 

	gl_Position = mvp * vec4(pos, 1.0);
}

 

 

 

 

1.3.3. cpp 수정

= 빛의 방향값을 수정한다.

= mesh 색은 원하는 색으로 임의로 설정한다.

= ofApp.h에서 사용할 shader이름을 바꿔주고, vertex shader과 fragment shader 이름도 각각 바꿔서 로드한다.

 

 

ofApp.cpp

void ofApp::draw(){
	using namespace glm;
	//생략
	dirLight.direction = normalize(vec3(1, -1, 0));
	dirLight.color = vec3(1, 1, 1);
	dirLight.intensity = 1.0f;

	specularShader.begin();
	specularShader.setUniformMatrix4f("model", model);
	specularShader.setUniformMatrix4f("mvp", mvp);
	specularShader.setUniform3f("meshSpecCol", glm::vec3(1, 1, 1));
    specularShader.setUniform3f("meshCol", glm::vec3(0, 0, 0));
	specularShader.setUniform3f("lightDir", getLightDirection(dirLight));
	specularShader.setUniform3f("lightCol", getLightColor(dirLight));
	specularShader.setUniform3f("cameraPos", cam.pos);
	specularShader.setUniformMatrix3f("normal", normalMatrix);
	torusMesh.draw();

	specularShader.end();
}

 

 

결과는 아래와 같이 나온다.

 

 

 

 

 

 

 

 

 

 

 

2. 디퓨즈와 스펙큘러 라이팅 합치기

= 이전 shader의 디퓨즈 계산을 현재 스펙큘러 fragment shader에 합친다.

 

아래와 메인함수를 같이 수정한다.

 

spec.frag

void main()
{
    vec3 nrm = normalize(fragNrm);
    vec3 refl = reflect(-lightDir, nrm);
    vec3 viewDir = normalize(cameraPos - fragWorldPos);

    float diffAmt = max(0.0, dot(nrm, lightDir));
    vec3 diffCol = meshCol * lightCol * diffAmt;

    float specAmt = max(0.0, dot(refl, viewDir));
    float specBright = pow(specAmt, 16.0);
    vec3 specCol = meshSpecCol * lightCol * specBright;

    outCol = vec4(diffCol + specCol, 1.0);
}

수정한 결과는 아래와 같다.

 

 

 

++ 빛의 색을 (1, 0, 1)로 수정한 결과이다.

이 때 파란 빛을 보내고 싶다고 (0, 0, 1)로 보내면 빛에 빨간색 데이터가 없어서 빨간색 데이터가 사라지고, 파란빛만 렌더링되니 주의해야 한다.

dirLight.color = vec3(1, 0, 1); 결과
dirLight.color = vec3(0, 0, 1); 결과

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

3. 앰비언트 라이팅

3.1. 글로벌 조명(global illumination)

= 물체에서 반사된 빛. 

= 해당 빛은 다른 물체를 비출 수 있다.

 

3.2. 앰비언트 라이팅

= 글로벌 조명을 가장 간단하게 재현하는 라이팅

= mesh의 모든 fragment에 어떤 색상을 더해주는 방식으로 작동. 

(fragment 검정 방지, 주변 환경으로 인한 반사광 구현 가능)

 

3.2.1. 구현 과정

= 씬의 앰비언트 색상을 담은 유니폼 변수 생성

= 유니폼 변수 * mesh 색상

= fragment shader의 최종적으로 내보내는 색상값에 위의 값을 더한다.

위 과정으로 fragment shader와 draw() 함수를 수정하면 아래와 같다.

(아래 내용을 추가한다.)

 

specular.frag

uniform vec3 ambientCol;

vec3 ambient = ambientCol * meshCol;
outCol = vec4(diffCol + specCol + ambient, 1.0);

 

 

 

ofApp.cpp

void ofApp::draw(){
//생략
specularShader.setUniform3f("ambientCol", glm::vec3(0.0, 0.0, 0.0));
//생략
}

 

 

아래는 ambientCol값에 따른 출력 결과의 차이이다.

 

(0.0, 0.0, 0.0)

 

(0.2, 0.0, 0.0)
(0.5, 0.0, 0.0)
(1.0, 0.0, 0.0)

 

 

(++ 현재 보여지는 색이 배경과 오브젝트를 통틀어 red 속성밖에 없으므로 현재는 red 속성의 변화만 반영된다.)

 

 

 

 

 

 

현재까지 라이트 연산을 렌더링에 적용하는 방법과 디퓨즈 연산, 스펙큘러 라이팅, 앰비언트 라이팅을 공부하고 세 라이팅을 합치는 방법도 알아보았다. 다음에는 세 라이팅이 합쳐져 있는 일반적으로 사용되는 퐁 라이팅 모델에 대해 공부할 것이다.