쉐이더 심화학습11-라이팅 심화(디렉셔널 라이트, 포인트 라이트, 스포트 라이트)
이전까지 배운 디렉셔널 라이트로 라이팅을 처리하는 방식은 실외의 태양광을 처리할 때 좋은 방식이다.
이번에는 여러 개의 라이트를 동시에 사용하고, 이전에 배운 것과 다른 라이트를 소개할 것이다.
0. 디렉셔널 라이트
1. 포인트 라이트
2. 스포트 라이트
0. 디렉셔널 라이트
= 게임 렌더링에서 가장 간단하고 많이 사용되는 라이트 유형이다.
= 색상과 방향 벡터로 정의한다.
= 무한히 멀리 있는 광원으로부터 오는 빛을 재현해서 실제 광원의 위치는 문제가 되지 않는다.
= 태양/달과 같은 광원 표현에 적합하다.
= 모든 방향에서 동일한 각도로 빛을 비춘다.
=> 색상과 방향 데이터만으로 블린-퐁 라이팅에서 사용 가능.
= 성능적 측면에서 가장 저렴하다.
(GPU가 라이트 위치에서 라이트 방향 벡터를 찾거나 라이트로부터 거리에 따라 fragment 밝기를 계산할 필요가 없기 때문.)
해당 라이트를 구현하기 위한 구조체는 아래와 같다.
struct DirectionalLight {
glm::vec3 direction;
glm::vec3 color;
float intensity;
};
1. 포인트 라이트
= 두 번째로 많이 사용하는 라이트 유형.
= 전구처럼 3D공간의 한 지점에서 빛을 내보내고, 빛은 그 지점으로부터 사방으로 퍼져나가는 방식.
(디렉셔널 라이트와 다르게 광선이 평행하지 않다.)
= fragment가 받는 빛의 양은 포인트 라이트와의 거리에 따라 달라진다.
해당 라이트를 구현하는 구조체는 아래와 같다.
struct PointLight{
glm::vec3 position;
glm::vec3 color;
float intensity;
float radius;
};
= 모든 방향으로 라이트를 비춰서 방향 벡터가 없다.
= 라이트의 위치와 위치를 중심으로 한 거리가 영향을 미치므로 구의 반지름을 저장한다.
(구의 경계까지 라이트를 비춘다.)
1.1. fragment shader
= 라이트 감쇠(falloff)를 저장한다.
(라이트가 비추는 각 대상이 그 라이트로부터의 거리에 따라 얼마나 많은 라이트를 받는지 결정)
1.1.1. 디퓨즈 라이팅 계산
라이팅을 계산하는 코드를 작성하면 아래와 같다.
pointLight.frag
#version 410
uniform vec3 meshCol;
uniform vec3 lightPos;
uniform float lightRadius;
uniform vec3 lightCol;
in vec3 fragNrm;
in vec3 fragWorldPos;
out vec4 outCol;
void main(){
vec3 normal = normalize(fragNrm);
vec3 toLight = lightPos - fragWorldPos;
vec3 lightDir = normalize(toLight);
float distToLight = length(toLight);
float falloff = max(0.0, 1.0 - (distToLight / lightRadius));
vec3 adjLightCol = lightCol * falloff;
float finalBright = max(0, dot(toLight, normal));
outCol = vec4(finalBright * adjLightCol * meshCol, 1.0);
}
= fragment에 도달하는 라이트의 방향과 광원으로부터의 거리를 알아야하므로 toLight 벡터를 구한다.
= fragment에 도달한 라이트의 밝기를 계산하기 위해 감쇠 계산을 수행한다. (선형 감쇠 계산)
(distToLight / lightRadius, 거리 값이 라이트가 영향을 미치는 영역의 경계에 근접할수록 백분율이 커진다.)
++ 해당 값이 1.0보다 큰 경우는 라이트의 영향을 받지 않는다는 의미이므로 1에서 해당 값을 빼준다.
++ 해당 방식으로 선형 감쇠를 계산하는 것이 가장 단순하고, 물리적 정확성과 GPU 연산 비용 중 어디에 가치를 두느냐에 따라 계산하는 방법이 달라진다.
= fragment와 라이트 사이의 거리는 라이팅 계산에 소요되는 시간과 관계가 없다.
=> 라이트를 받지 않는 거리에 fragment가 있어도 성능 상의 비용은 생긴다는 의미.
=> 포인트 라이트에 GPU 자원을 사용하지 않는 방향 지향.
= 대부분의 게임은 shader 외적 방법으로 mesh에 영향을 미치는 라이트인지 판단하고, 라이팅을 처리하는 shader를 실행한다.
1.1.2. 라이팅 모델 함수
= 포인트 라이트를 지원하도록 라이팅 모델 함수를 아래와 같이 작성한다.
pointLight.frag
float diffuse(vec4 lightDir, vec3 nrm)
{
float diffAmt = max(0.0, dot(nrm, lightDir));
return diffAmt;
}
float specular(vec3 lightDir, vec3 viewDir, vec3 nrm, float shininess)
{
vec3 halfVec = normalize(viewDir + lightDir);
float specAmt = max(0.0, dot(halfVec, nrm));
return pow(specAmt, shininess);
}
위 함수를 사용하도록 fragment shader의 메인함수를 수정하면 아래와 같다.
void main(){
vec3 nrm = texture(normTex, fragUV).rgb;
nrm = normalize(nrm * 2.0 - 1.0);
nrm = normalize(TBN * nrm);
vec3 viewDir = normalize(cameraPos - fragWorldPos);
vec3 envSample = texture(envMap, reflect(-viewDir, nrm)).xyz;
vec3 sceneLight = mix(lightCol, envSample + lightCol * 0.5, 0.5);
float diffAmt = diffuse(lightDir, nrm);
float specAmt = specular(lightDir, viewDir, nrm, 4.0);
vec3 diffCol = texture(diffuseTex, fragUV).xyz * sceneLight * diffAmt;
fkiat specMask = texture(specTex, fragUV).x;
vec3 specCol = specMask * sceneLight * specAmt;
outCol = vec4(diffCol + specCol + ambientCol, 1.0);
}
= 라이팅 연산을 diffAmt의 diffuse()와 specAmt의 specular()로 대체했다.
=> 함수로 각 라이트 연산을 분리했으므로 라이트 유형에 상관없이 라이트 연산을 유지할 ㅅ ㅜ있다.
여기에 라이트 방향을 계산하고, 라이트 감쇠 값과 일부 값을 곱해 연산한다.
해당 수정사항을 반영한 main함수는 아래와 같다.
uniform vec3 lightPos;
uniform float lightRadius;
void main()
{
//생략
vec3 envSample = texture(envMap, reflect(-viewDir, nrm)).xyz;
vec3 sceneLight = mix(lightCol, envSample + lightCol * 0.5, 0.5);
vec3 toLight = lightPos - fragWorldPos;
vec3 lightDir = normalize(toLight);
float distToLight = length(toLight);
float falloff = 1.0 - (distToLight / lightRadius);
float diffAmt = diffuse(lightDir, nrm) * falloff;
float specAmt = specular(lightDir, viewDir, nrm, 4.0) * falloff;
vec3 diffCol = texture(diffuseTex, fragUV).xyz * sceneLight * diffAmt;
//생략
}
= diffuse연산 이후와 specular 연산 후 라이트의 감쇠를 곱한다.
(라이팅을 받는 정도는 라이트-fragment 거리에 반비례하기 때문)
작성한 결과는 아래와 같다.
pointLight.frag
#version 410
uniform vec3 meshCol;
uniform vec3 lightPos;
uniform float lightRadius;
uniform vec3 lightCol;
uniform vec3 cameraPos;
uniform vec3 ambientCol;
uniform sampler2D diffuseTex;
uniform sampler2D specTex;
uniform sampler2D normTex;
uniform samplerCube envMap;
in vec3 fragNrm;
in vec3 fragWorldPos;
in vec2 fragUV;
in mat3 TBN;
out vec4 outCol;
float diffuse(vec3 lightDir, vec3 nrm)
{
float diffAmt = max(0.0, dot(nrm, lightDir));
return diffAmt;
}
float specular(vec3 lightDir, vec3 viewDir, vec3 nrm, float shininess)
{
vec3 halfVec = normalize(viewDir + lightDir);
float specAmt = max(0.0, dot(halfVec, nrm));
return pow(specAmt, shininess);
}
void main()
{
vec3 nrm = texture(normTex, fragUV).rgb;
nrm = normalize(nrm * 2.0 - 1.0);
nrm = normalize(TBN * nrm);
vec3 viewDir = normalize( cameraPos - fragWorldPos);
vec3 envSample = texture(envMap, reflect(-viewDir, nrm)).xyz;
vec3 sceneLight = mix(lightCol, envSample + lightCol * 0.5, 0.5);
vec3 toLight = lightPos - fragWorldPos;
vec3 lightDir = normalize(toLight);
float distToLight = length(toLight);
float falloff = 1.0 - (distToLight / lightRadius);
float diffAmt = diffuse(lightDir, nrm) * falloff;
float specAmt = specular(lightDir, viewDir, nrm, 4.0) * falloff;
vec3 diffCol = texture(diffuseTex, fragUV).xyz * sceneLight * diffAmt;
float specMask = texture(specTex, fragUV).x;
vec3 specCol = specMask * sceneLight * specAmt;
outCol = vec4(diffCol + specCol + ambientCol, 1.0);
}
1.2. cpp 수정
= draw함수에서 포인트 라이트를 생성하고, 포인트 라이트의 위치, 색상과 같은 조건을 추가한다.
void ofApp::draw(){
using namespace glm;
static float t = 0.0f;
t += ofGetLastFrameTime();
PointLight pointLight;
pointLight.color = vec3(1, 1, 1);
pointLight.radius = 1.0f;
pointLight.position = vec3(sin(t), 0.5, 0.25);
pointLight.intensity = 3.0;
cam.pos = vec3(0, 0.75f, 1.0f);
cam.fov = radians(90.0f);
float aspect = 1024.0f / 768.0f;
mat4 proj = perspective(cam.fov, aspect, 0.01f, 10.0f);
mat4 rotation = mat4();
mat4 view = inverse(translate(cam.pos)) * rotation;
//drawCube(proj, view);
drawShield(pointLight, proj, view);
drawWater(pointLight, proj, view);
drawSkybox(pointLight, proj, view);
}
++ 씬 전체를 비추는 것이 아닌 구 형태의 라이트를 다룬다.
(그래서 sin함수를 사용해 라이트가 X축을 따라 -1~1을 왕복한다.)
= drawShield(), drawWater()에서 LightDir를 대신 아래 코드로 교체한다.
shd.setUniform3f("lightPos", pointLight.position);
shd.setUniform1f("lightRadius", pointLight.radius);
shd.setUniform3f("lightCol", getLightColor(pointLight));
= drawShild(), drawWater(), drawSkybox()도 아래와 같은 매개변수를 받아오도록 수정한다.
void ofApp::drawWater(PointLight& pointLight, glm::mat4& proj, glm::mat4& view)
++ 그리고 헤더파일도 아래와 같이 선언을 수정한다.
void drawWater(PointLight& pointLight, glm::mat4& proj, glm::mat4& view);
void drawShield(PointLight& pointLight, glm::mat4& proj, glm::mat4& view);
void drawCube(glm::mat4& proj, glm::mat4& view);
void drawSkybox(PointLight& pointLight, glm::mat4& proj, glm::mat4& view);
= setup함수에서 shield는 fragment shader를 pointLight.frag, water는 pointLightWater.frag를 사용하므로 아래와 같이 수정한다.
blinnphongShader.load("mesh.vert", "pointLight.frag");
waterShader.load("water.vert", "pointLightWater.frag");;
= getLightColor함수의 선언과 함수의 인자를 받아오는 부분도 수정한다.
glm::vec3 getLightColor(PointLight &l);
glm::vec3 getLightColor(PointLight& l) {
return l.color * l.intensity;
}
1.3. 다른 오브젝트의 fragment shader 수정하기
1.3.1. 물
= pointLight.frag와 같이 연산을 분리해두고 lightDir을 lightPos, lightRadius로 수정한다.
결과는 아래와 같다.
pointLightWater.frag
#version 410
uniform vec3 lightPos;
uniform float lightRadius;
uniform vec3 lightCol;
uniform vec3 cameraPos;
uniform vec3 ambientCol;
uniform sampler2D normTex;
uniform samplerCube envMap;
in vec3 fragNrm;
in vec3 fragWorldPos;
in vec2 fragUV;
in vec2 fragUV2;
in mat3 TBN;
out vec4 outCol;
float diffuse(vec3 lightDir, vec3 nrm)
{
float diffAmt = max(0.0, dot(nrm, lightDir));
return diffAmt;
}
float specular(vec3 lightDir, vec3 viewDir, vec3 nrm, float shininess)
{
vec3 halfVec = normalize(viewDir + lightDir);
float specAmt = max(0.0, dot(halfVec, nrm));
return pow(specAmt, shininess);
}
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));
vec3 toLight = lightPos - fragWorldPos;
vec3 lightDir = normalize(toLight);
float distToLight = length(toLight);
float falloff = 1.0 - (distToLight / lightRadius);
vec3 viewDir = normalize( cameraPos - fragWorldPos);
float diffAmt = diffuse(lightDir, nrm) * falloff;
float specAmt = specular(lightDir, viewDir, nrm, 512.0) * falloff;
vec3 diffCol = texture(envMap, (reflect(-viewDir, nrm))).xyz * lightCol * diffAmt;
vec3 specCol = lightCol * specAmt;
outCol = vec4(diffCol + specCol + ambientCol, 1.0);
}
위와 같이 작성하면 결과는 아래와 같다.
= 디렉셔널 라이트가 없는 상황이기 때문에 mesh가 너무 어두워보인다.
(사실적인 모습을 만들기를 원한다면 스카이박스의 배경을 밤 시간대처럼 처리하면 된다.)
2. 스포트 라이트
= 원뿔 형태의 라이트로, 게임 내의 특정 위치를 나타낸다는 점에서 포인트 라이트와 동일하다.
해당 라이트의 구조체는 아래와 같다.
struct SpotLight
{
glm::vec3 position;
glm::vec3 direction;
float cutoff;
glm::vec3 color;
float intensity;
}
= 기본적으로 위치와 방향 벡터가 필요하다.
= 원뿔의 너비를 나타내기 위한 cutoff가 추가되었다.
++ 거리에 따라 라이트를 받는 양이 반비례하므로 해당 부분을 적용하기 위한 반지름 / 범위 값을 추가할 수도 있다.
(해당 코드에서는 원뿔 모양이고, 특정 부분에 빛을 많이 받는다는 부분을 집중하기 위해 제외했다.)
2.1. fragment shader
2.1.1. 기본 틀 제작
pointLight.frag에서 코드를 붙여넣고 내용을 일부 수정한다. 기본적으로 만든 베이스는 아래와 같다.
spotLightf.frag
//생략
uniform vec3 lightConeDir;
uniform float lightCutoff;
//생략
void main()
{
vec3 nrm = texture(normTex, fragUV).rgb;
nrm = normalize(nrm * 2.0 - 1.0);
nrm = normalize(TBN * nrm);
vec3 viewDir = normalize( cameraPos - fragWorldPos);
vec3 envSample = texture(envMap, reflect(-viewDir, nrm)).xyz;
vec3 sceneLight = mix(lightCol, envSample + lightCol * 0.5, 0.5);
//계산식
float diffAmt = diffuse(lightDir, nrm) * falloff;
float specAmt = specular(lightDir, viewDir, nrm, 4.0) * falloff;
//생략
}
++ diffAmt와 specAmt는 감쇠 값을 곱해야 한다.
(여기서 감쇠 = fragtment가 스포트 라이트 원뿔 안에 있는지 여부 판단.)
++ 스포트 라이트는 여러 유형 라이트 동시에 지원할 경우 유용한다.
(스포트 라이트와 포인트 라이트 차이 << 포인트 라이트와 디렉셔널 라이트 간 차이)
2.1.2. 라이팅 연산
2.1.2.1. 라이트 방향 벡터
= lightConeDir 유니폼 변수를 사용하는 것이 아닌, 다시 라이트 방향 변수를 계산해야 한다.
=> 스포트 라이트 광선은 디렉셔널 라이트처럼 평행하지 않고, 서로 다른 방향으로 나가기 때문.
++ 라이트 방향 벡터 : 라이트 결과에 반영할 감쇠 값을 계산할 때만 사용.
라이트 방향 벡터는 아래와 같다.
vec3 toLight = lightPos - fragWorldPos;
vec3 lightDir = normalize(toLight);
= 포인트 라이트의 부분과 동일하지만, fragment-라이트 거리는 연산하지 않았다.
2.1.2.2. fragment의 스포트 라이트 위치 여부
= fragment의 스포트 라이트가 원뿔 안에 있는지 여부를 판단한다.
= 0/1의 감쇠값으로 표현.
=> 스포트 라이트 = 감쇠 계산 방식이 다른 포인트 라이트.
= 스포트 라이트의 원뿔 방향 벡터(lightConedir), fragment에서 라이트로 향하는 방향 벡터(lightDir) 사이 각도를 구한다.
= 두 벡터 간 각도가 라이트 컷오프(cutoff) 각보다 작으면, fragment는 스포트 라이트의 원뿔 범위 내부에 존재하고 라이트의 영향을 받는다.
(lightCutoff = 스포트 라이트 원뿔의 코사인이므로 해당 결과, 내적값을 사용해 lightDir-lightConeDir 사이 각과 컷오프 각을 비교할 수 있다.)
코드는 아래와 같다.
vec3 toLight = lightPos - fragWorldPos;
vec3 lightDir = normalize(toLight);
float cosAngle = dot(lightConeDir, -lightDir);
float falloff = 0.0;
if(cosAngle > lightCutoff)
{
falloff = 1.0;
}
= 두 벡터는 같은 방향을 향해야 한다. (lightDIR은 라이트로부터 시작하는 벡터가 아니어서 음수로 변환.)
= cosAngle이 lightCutoff보다 커야 한다는 조건을 옳은 조건이다.
(코사인(lightCutoff)를 shader로 전달하고, cosAngle이 1에 가까울수록 lightDir과 lightConeDir은 평행에 가까워지기 때문.)
++ 실제 각도를 비교했다면 조건이 반대로 되어야 한다.
2.1.3. 방패 fragment shader 수정
방패와 물 fragment shader에도 아래와 같이 수정을 한다.
pointLight.frag(shield)
uniform vec3 meshCol;
uniform vec3 lightPos;
uniform float lightCutoff;
uniform vec3 lightCol;
uniform vec3 cameraPos;
uniform vec3 ambientCol;
uniform sampler2D diffuseTex;
uniform sampler2D specTex;
uniform sampler2D normTex;
uniform samplerCube envMap;
//생략
void main()
{
vec3 nrm = texture(normTex, fragUV).rgb;
nrm = normalize(nrm * 2.0 - 1.0);
nrm = normalize(TBN * nrm);
vec3 viewDir = normalize( cameraPos - fragWorldPos);
vec3 envSample = texture(envMap, reflect(-viewDir, nrm)).xyz;
vec3 sceneLight = mix(lightCol, envSample + lightCol * 0.5, 0.5);
vec3 toLight = lightPos - fragWorldPos;
vec3 lightDir = normalize(toLight);
float cosAngle = dot(lightConeDir, -lightDir);
float falloff = 0.0;
if(cosAngle > lightCutoff)
{
falloff = 1.0;
}
float diffAmt = diffuse(lightDir, nrm) * falloff;
float specAmt = specular(lightDir, viewDir, nrm, 4.0) * falloff;
vec3 diffCol = texture(diffuseTex, fragUV).xyz * sceneLight * diffAmt;
float specMask = texture(specTex, fragUV).x;
vec3 specCol = specMask * sceneLight * specAmt;
vec3 envSample = texture(envMap, reflect(-viewDir, nrm)).xyz;
vec3 envLighting = envSample * specMask * diffAmt;
specCol = mix(envLighting, specCol, min(1.0, specAmt));
outCol = vec4(diffCol + specCol + ambientCol, 1.0);
}
pointLightWater.frag
uniform vec3 lightPos;
uniform float lightCutoff;
uniform vec3 lightCol;
uniform vec3 cameraPos;
uniform vec3 ambientCol;
uniform sampler2D normTex;
uniform samplerCube envMap;
//생략
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));
vec3 toLight = lightPos - fragWorldPos;
vec3 lightDir = normalize(toLight);
float distToLight = length(toLight);
float falloff = 1.0 - (distToLight / lightRadius);
vec3 viewDir = normalize( cameraPos - fragWorldPos);
float diffAmt = diffuse(lightDir, nrm) * falloff;
float specAmt = specular(lightDir, viewDir, nrm, 512.0) * falloff;
vec3 diffCol = texture(envMap, (reflect(-viewDir, nrm))).xyz * lightCol * diffAmt;
vec3 specCol = lightCol * specAmt;
outCol = vec4(diffCol + specCol + ambientCol, 1.0);
}
2.2. cpp 수정
= draw및 getLightColor의 매개변수, 헤더파일의 함수 선언을 수정한다.
ofApp.h
void drawWater(SpotLight& spotLight, glm::mat4& proj, glm::mat4& view);
void drawShield(SpotLight& spotLight, glm::mat4& proj, glm::mat4& view);
void drawSkybox(SpotLight& spotLight, glm::mat4& proj, glm::mat4& view);
ofApp.cpp
glm::vec3 getLightColor(SpotLight &l);
void ofApp::setup() {
//생략
blinnphongShader.load("mesh.vert", "spotLightf.frag");
//생략
}
void ofApp::drawWater(SpotLight& spotLight, glm::mat4& proj, glm::mat4& view)
{
//생략
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.setUniformTexture("envMap", cubemap.getTexture(), 1);
shd.setUniform3f("ambientCol", glm::vec3(0.1, 0.1, 0.1));
shd.setUniform3f("lightPos", spotLight.position);
shd.setUniform1f("lightCutoff", spotLight.cutoff);
shd.setUniform3f("lightCol", getLightColor(spotLight));
shd.setUniform3f("cameraPos", cam.pos);
planeMesh.draw();
shd.end();
}
void ofApp::drawShield(SpotLight& spotLight, glm::mat4& proj, glm::mat4& view)
{
//생략
shd.begin();
shd.setUniformMatrix4f("model", model);
shd.setUniformMatrix4f("mvp", mvp);
shd.setUniformMatrix3f("normal", normalMatrix);
shd.setUniform3f("meshSpecCol", glm::vec3(1, 1, 1));
shd.setUniform3f("meshCol", glm::vec3(1, 0, 0));
shd.setUniform3f("lightPos", spotLight.position);
shd.setUniform1f("lightcutoff", spotLight.cutoff);
shd.setUniform3f("lightCol", getLightColor(spotLight));
shd.setUniform3f("cameraPos", cam.pos);
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);
shd.setUniformTexture("envMap", cubemap.getTexture(), 3);
shieldMesh.draw();
shd.end();
}
void ofApp::drawSkybox(SpotLight& spotLight, glm::mat4& proj, glm::mat4& view)
{
//생략
}
glm::vec3 getLightColor(SpotLight& l) {
return l.color * l.intensity;
}
위와 같이 코드들을 수정하면 결과는 아래와 같다.
= 이렇게 원뿔 안에 있는 범위만 라이트를 받는다. 컷오프 각도를 키우고, 카메라 위치를 오른쪽으로 옮기면 더 많은 부분이 라이트를 받는 것을 확인할 수 있다.