Specular는 Diffuse랑 비슷한데 차이가 있다. 이를 알아보자. 우선 Diffuse처럼 물체의 표면에 수직하는 Normal 벡터값과 조명에서 나온는 빛의 방향을 필요로 하는것은 동일하다.
그러나 빛이 물체의 표면에 반사돼서 노멀벡터값을 기준으로 대칭되는 방향으로 나아가는 빛을 계산한다.
즉, 반사되는 부분의 빛을 나타내기 위해 사용한다는 것이다.
크리링의 머리에 빛이 반사되는 빨간 원 안의 부분의 효과를 위한 것이다.
구하는 원리를 간단하게 살펴보면
Diffuse는 노멀벡터와 빛 사이의 각도(→n, →L)가 핵심이었다면 Specular는 반사된 빛(주황색)과 눈(카메라) 사이(파랑색)의 각(연두색 θ)을 중심으로 보는 것이다. Diffuse와 비슷한 원리로 Specular도 카메라의 각과 빛이 반사된는 각이 0에 가까울 수록(눈뽕에 가까울수록) 빛의 강도가 강하고 90도에 가까울수록 빛의 강도가 약하다.
빛이 오는 방향 벡터를 L→ 라면 반사되는 방향벡터는 어떻게 구하는지 생각해보자.
normal벡터를 축으로 대칭이므로
카메라의 월드포지션 위치벡터에서 오브젝트 표면의 위치벡터를 빼면 방향벡터가 나온다.(하늘색 선)
그리고 반사광의 벡터는 빛의 방향벡터 L을 표면 아래까지 가져오고 노멀벡터도 마찬가지로 화면 아래까지 가져왔을때 둘 사이의 수선의 발을 내린 그 크기를 두배 위로 올라가면 오브젝트 표면에서 그 위치까지 반사광의 방향벡터가 나온다..?
우선 카메라의 위치를 알아내는 가장 쉬운 방법은 뷰의 행렬을 이용하는거다.
뷰행렬은 월드 좌표로 있던 모든 사물들을 카메라 기준으로 좌표를 바꾸는것이 카메라 좌표계 변환 행렬이었다.
카메라 좌표계 변환 행렬에서 3*3까지는 각각 첫행이 upvector, 두번째행이 rigjtvector 세번째 행이 lookvector이런 회전과 관련된 부분이었고 네번째 행이 위치 이동과 관련된 부분이었다.
그래서 네번째 행에 0열,1열,2열에 a,b,c가 있었다면 반대로 모든 물체들은 카메라 반대방향이므로 카메라는 월드의 원점으로부터 (-a,-b,-c)의 위치에 있는것이다.
그래서 뷰행렬의 네번째행의 값들에 마이너스를 곱하면 카메라의 좌표가 될 수도 있고 아니면 뷰의 역행렬을 구하고 그 행렬의 네번째행이 오브젝트가 움직인 값이 나오므로 방법은 여러가지다. 얘에서 오브젝트 표면의 위치벡터를 빼면 카메라의 방향벡터(하늘색선)E를 구할 수 있을 것이다.
10.Lighting_Diffuse.fx를 복붙해서 11.Lighting_Specular를 만들고 같은 방법으로 SpecularDemo 클래스를 만들었다.
쉐이더에서 LightDir을 조명의 방향으로 쓸 것이고 float4 LightSpecular를 만들어 specular 빛의 색상을 담을것이다. 또 MaterialSpecular가 물체가 받아주는 specular양을 담도록 한다.
// 11.Light_Specular.fx
#include "00. Global.fx"
float3 LightDir;
float4 LightSpecular;
float4 MaterialSpecular;
Texture2D DiffuseMap;
MeshOutput VS(VertexTextureNormal input)
{
MeshOutput output;
output.position = mul(input.position, W);
output.worldPosition = input.position;
output.position = mul(output.position, VP);
output.uv = input.uv;
output.normal = mul(input.normal, (float3x3)W);
return output;
}
// Specular (반사광)
// 한방향으로 완전히 반사되는 빛 (Phong)
float4 PS(MeshOutput input) : SV_TARGET
{
//float3 R = reflect(LightDir, input.normal);
float3 R = LightDir - (2 * input.normal * dot(LightDir, input.normal));
R = normalize(R);
float3 cameraPosition = -V._41_42_43;
float3 E = normalize(cameraPosition - input.worldPosition);
float value = saturate(dot(R, E)); // clamp(0~1)
float specular = pow(value, 10);
float4 color = LightSpecular * MaterialSpecular * specular;
return color;
}
technique11 T0
{
PASS_VP(P0, VS, PS)
};
그리고 지금까지는 VS에서 input.position에 W,VP행렬을 곱해 output.position을 구해줬는데 이는 다음단계인 래스터라이저에서 z값으로 나눠지고하면 비율에 맞는 위치가 나오곤 했다.
그러면 픽셀 쉐이더에서 specular값을 계산하기 위해서 필요한 worldPosition값은 전단계 버텍스 쉐이더로 넘어가게 되면 결국 스크린좌표계로 까지 변환이되어서 원하는 좌표로 남지 않게되어서 따로 worldPosition을 저장했다가 넘겨줘야한다.
// 00.Global.fx
// ...
struct VertexOutput
{
float4 position : SV_POSITION;
float2 uv : TEXCOORD;
float3 normal : NORMAL;
};
struct MeshOutput
{
float4 position : SV_POSITION;
float3 worldPosition : POSITION1;
float2 uv : TEXCOORD;
float3 normal : NORMAL;
};
그래서 00.Global.fx에서 VertexOuput 구조체를 대신해 MeshOutput타입을 만들고 worldPosition을 따로 추가했다. 앞의 예제까지만해도 VertexOutput을 썼지만 픽셀쉐이더 원래 정점의 월드포지션 값이 specular 연산(E벡터 구하는데)에 필요해 MeshOutput 구조체를 새로 정의하고 멤버로 worldPosition을 넣은것이다.
그리고 다시 Lighting_Specular.fx로 돌아와서 VS에서
output.position = mul(input.position, W);
output.worldPosition = output.position;
으로 바로 각 버텍스의 로컬포지션에 월드좌표계변환행렬을 곱한 월드포지션을 넣어줬다. 물로 VS의 반환타입도 방금 만든 MeshOuput으로 바꿔준다.
그리고 PS에서는 MeshOutput을 input으로 받는데 여기서 어떤 영역에서 연산을 할지 골라줘야한다.
월드좌표를 이용할것이면 모든 좌표를 월드로 받아준다음에 활용하는 방법도 있겟지만 뷰스페이스로 모든 좌표를 변환해서 사용하는 방법도 있다. 뷰스페이스에서 연산할때의 장점은 모든 물체들이 결국 카메라 기준으로 하는 좌표로 되어있으므로 굳이 카메라 위치에서 표면의 월드좌표를 빼는 연산이 생략될 수 있다.
그럼에도 가장 기본적인 연산방법이 월드좌표에서 작업을 하는것이므로 그 방법을 사용해 PS에서 Specular연산을 해보자. 다시한번 Specular에 대해 이야기해보면 반사광이라고 불리우고 한반향으로 완전히 반사되는 빛을 의미한다. Phong 연산을 사용한다.
// Specular (반사광)
// 한방향으로 완전히 반사되는 빛 (Phong)
float4 PS(MeshOutput input) : SV_TARGET
{
//float3 R = reflect(LightDir, input.normal);
float3 R = LightDir - (2 * input.normal * dot(LightDir, input.normal));
R = normalize(R);
float3 cameraPosition = -V._41_42_43;
float3 E = normalize(cameraPosition - input.worldPosition);
float value = saturate(dot(R, E)); // clamp(0~1)
float specular = pow(value, 10);
float4 color = LightSpecular * MaterialSpecular * specular;
return color;
}
먼저 순차적으로
- LightDir이 있을 때 input.normal의 방향벡터를 기준으로 대칭되게 반대 방향으로 가는 반사광의 방향벡터(R)를 구해준다.
- 이를 구하는 방법은 2가지인데 하나는 내장함수 reflect를 이용하는 것이다. float3 R = reflect(LightDir, input.normal)
- 직접 수학식을 이용하려면
float3 R = LightDir -(2 * input.normal * dot(LightDir, input.normal ))이 식이 같은 값이 나오게 된다.
여기서 내적한 다음에 다시 input.normal값을 곱해준 이유는 내적을 한 결과가 스칼라 값으로 15:17캡쳐 의 값이 구해준 것이다. 그런데 여기에 normal방향으로 2배를 이동해야하므로이다. 그리고 LightDir에서 빼는 이유는 빛의 방향은 표면 아래이고 normal방향은 표면 수직위방향이므로 내적하면 음수가 나오니깐 -를 해서 절대값을 더한 것이다.
먼저 순차적으로
2. LightDir이 있을 때 input.normal의 방향벡터를 기준으로 대칭되게 반대 방향으로 가는 반사광의 방향벡터(R)를 구해준다.
- 이를 구하는 방법은 2가지인데 하나는 내장함수 reflect를 이용하는 것이다. float3 R = reflect(LightDir, input.normal)
- 직접 수학식을 이용하려면 float3 R = LightDir -(2 * input.normal * dot(LightDir, input.normal ))이 식이 같은 값이 나오게 된다.
여기서 내적한 다음에 다시 input.normal값을 곱해준 이유는 내적을 한 결과가 스칼라 값으로 15:17캡쳐 의 값이 구해준 것이다. 그런데 여기에 normal방향으로 2배를 이동해야하므로이다. 그리고 LightDir에서 빼는 이유는 빛의 방향은 표면 아래이고 normal방향은 표면 수직위방향이므로 내적하면 음수가 나오니깐 -를 해서 절대값을 더한 것이다.
3. 오브젝트의 현재 정점의 WorldPosition에서 카메라의 위치까지 방향벡터(파란색 선)을 구해보자.방법은 카메라의 위 치에서 오브젝트의 현재 정점의 WorldPosition값을 빼주면 된다.
float3 E = normalize(cameraPosition - input.worldPosition );
그리고 단위벡터로 사용하기 위해 normalize도 해준다.
4. 마지막으로 3에서 구한 표면에서 카메라까지의 방향벡터와 반사광 방향벡터의 내적한 값을 구한 값을 비율에 맞게 빛의 강도에 연산해주면 된다.
- 그런데 만약에 R,E를 내적한 값이 무조건 0~1사이의 값을 가지도록 하고 싶다면 (0이하의 값은 0으로 1 이상의 값은 1로 유지하도록)saturate함수를 이용한다.(이런걸 클램프라고한다.?)
- 그리고 specular값을 구하는데 보통 이 value값을 그대로 쓰지 않고 제곱을 한값이나 10승을 한 값을 이용한다. 보통 코사인값이 바로 0~1사이의 값을 가지므로 드라마틱하지 않은데, 실제 반사광이 바로 닿는부분은 영역이 좁고 빛이 엄청 강하기 때문에 극명한 효과를 주기 위해서이다. float speular = pow(value,10); 제곱연산에 큰값을 줄수록 영역이 좁고 밝아지고 작을수록 넓고 옅은 색을 가지게 될 것이다.
- 최종적으로 specular값만 볼 것이므로 color에다가 LightSpecular(빛)와 MaterialSpecular(빛을 받아주는 비율)로 빛의 색을 구하고 텍스쳐에서 샘플링해 가져온 color값을 곱하고 지금까지 구한 반사광 specular값을 곱한다.
float value = saturate(dot(R,E));
float specular = pow(value,10);// 10승 연산
float4 color = DiffulseMap.Sample(LinearSampler, input.uv);
color = color * specular * LightSpecular * MaterialSpecular;
물론 이것도 정해진 식이 있는것은 아니고 필요에 따라 적절하게 조절하면 된다.
이제 이 값을 이용해 세팅하여 값이 정상 동작하는지 살펴보자.
'[DirectX11]' 카테고리의 다른 글
Light #5 Light 통합 (0) | 2024.09.09 |
---|---|
Light #4 - Emissive (0) | 2024.09.09 |
Light #2 - Diffuse (0) | 2024.08.30 |
Light #1 - Ambient (0) | 2024.08.27 |
3D - Geometry, Sampling (0) | 2024.08.07 |