Domain Shader로 구현하는 GPU기반 Deferred Shading Sphere Lighting Volume 코드 리뷰 [[ HLSL
사실 이 책은 예전에 보던 책인데, DirectX 재활하는중이기도 하고 복습하는 느낌으로 한번 더 보고있다.
그 중 Deferred Shading 부분의 Point Light룰 구현하는 부분이 있다.
이 책이 정말 흥미로운 점이 Deferred Shading에서 사용하는 Lighting Volume을 GPU로 구현한다는것이다.
정말 재미있다.
나는 CPU로도 당연히 구현을 해봤는데 개인적으로 Hull Shader와 Domain Shader를 사용하는 이 책의 방법이 매우 매력적이다.
오늘 포스팅에서 할 내용은 이 코드의 핵심 부분을 리뷰하고 실험하고 테스트 하는 그런 글이다.
아마 이 글은 나랑 같이 이 책을 읽고 있는 사람들을 위힌 글 아닐까?
코드 (Vertex Shader)
float4 PointLightVS() : SV_Position
{
return float4(0.0, 0.0, 0.0, 1.0);
}
정점 셰이더에서는 어떠한 정보도 넘겨주지 않고 있다.
당연하게도 이 책에서는 드로우콜을 할때 CPU메모리에서 어떠한 정점정보를 넘겨주지 않고 있다.
어차피 Hull Shader에서 정점 정보를 생성할것이기 때문이다.
Hull Shader
static const float3 HemilDir[2] = {
float3(1.0, 1.0,1.0),
float3(-1.0, 1.0, -1.0)
};
[domain("quad")]
[partitioning("integer")]
[outputtopology("triangle_ccw")]
[outputcontrolpoints(4)]
[patchconstantfunc("PointLightConstantHS")]
HS_OUTPUT PointLightHS(uint PatchID : SV_PrimitiveID)
{
HS_OUTPUT Output;
Output.HemiDir = HemilDir[PatchID];
return Output;
}
Hull Shader에서는 총 2개의 패치 정보를 넘겨줄 생각이다.
우리가 만들 볼륨메시는 구가 되어야하는데 반구 2개를 합쳐서 구를 만들고 있다.
즉, 1개의 패치가 반구 한쪽을 담당한다는것을 볼 수 있다. HemiDir의 역할은 Domain Shader에서 설명하겠다.
Domain Shader
/////////////////////////////////////////////////////////////////////////////
// Domain Shader shader
/////////////////////////////////////////////////////////////////////////////
struct DS_OUTPUT
{
float4 Position : SV_POSITION;
float2 cpPos : TEXCOORD0;
};
[domain("quad")]
DS_OUTPUT PointLightDS( HS_CONSTANT_DATA_OUTPUT input, float2 UV : SV_DomainLocation, const OutputPatch<HS_OUTPUT, 4> quad)
{
// Transform the UV's into clip-space
float2 posClipSpace = UV.xy * 2.0 - 1.0;
// Find the absulate maximum distance from the center
float2 posClipSpaceAbs = abs(posClipSpace.xy);
float maxLen = max(posClipSpaceAbs.x, posClipSpaceAbs.y);
// Generate the final position in clip-space
float3 normDir = normalize(float3(posClipSpace.xy, (maxLen - 1.0)) * quad[0].HemiDir);
float4 posLS = float4(normDir.xyz, 1.0);
// Transform all the way to projected space
DS_OUTPUT Output;
Output.Position = mul( posLS, LightProjection );
// Store the clip space position
Output.cpPos = Output.Position.xy / Output.Position.w;
return Output;
}
사실상 이 부분이 이 포스팅에서 설명하고자 한 핵심이다.
그러는 동시에 Sphere Lighting Volume을 생성하는 핵심파트이기도 하다.
우선
float2 posClipSpace = UV.xy * 2.0 - 1.0;
이 부분은 좌표계를 바꾸는 부분이다.
기존 Domain Shader에서 넘겨주는 SV_DomainLocation 이라는 SystemValue는 좌측 하단이 (0, 0)이고 우측 상단으로 갈수록 (1, 1)에 가까워지는 좌표계이다.
하지만 위와 같은 연산을 거치며, 일반적으로 우리가 수학좌표계에서 볼 수 있는[-1, 1] 범위의 정가운데가 원점인 좌표계로 변환된다.
float2 posClipSpaceAbs = abs(posClipSpace.xy);
float maxLen = max(posClipSpaceAbs.x, posClipSpaceAbs.y);
이부분은 xy의 값을 비교해서 가장자리로 갈수록 z값의 더 변화를 줌으로써 최종적으로 반구 모양이 될수 있도록 하는 코드이다.
실제로 xy값에 따른 maxLen의 값변화 그래프가 이런 양상을 띄고 있다.

가장자리로 갈수록 1에 더 가까워지는(더 구부러지는)걸 볼수가 있다.
float3 normDir = normalize(float3(posClipSpace.xy, (maxLen - 1.0)) * quad[0].HemiDir);
float4 posLS = float4(normDir.xyz, 1.0);
// Transform all the way to projected space
DS_OUTPUT Output;
Output.Position = mul( posLS, LightProjection );
이 코드는 이제 실제로 View 공간으로 변환하기 이전에 Hull Shader에서 넘겨주는 패치에 따른 반구의 최종 위치를 정하는 부분이다.
xy부분은 그대로 유지하는데 z값에 의문이 생길수 있다.
현재 maxLen의 범위는 [0, 1]인데 -1을 하게되면 [-1, 0]이 된다.
이렇게 정의역을 설정한 이유는 View 공간에서는 -Z방향이 앞부분이 되기 때문이다.
따라서 정의역을 [-1, 0]으로 설정한것이다.
그리고 HemiDir이 이제 패치에 따른 반구를 나누어 주는 부분이다.
위로 거슬러 올라가면 HemiDir의 내용이 이런식으로 되있다는것을 볼 수 있다.
static const float3 HemilDir[2] = {
float3(1.0, 1.0,1.0),
float3(-1.0, 1.0, -1.0)
};
난 개인적으로 이 부분이 작년에 공부했을때 이해가 안갔었는데 올해 들어서야 이해가 갔다.
일단 단순하게 생각했을때
xy부분은 달라질게 없고 그냥 z부분만 다르게해서
static const float3 HemilDir[2] = {
float3(1.0, 1.0,1.0),
float3(1.0, 1.0, -1.0)
};
이렇게 해도 되지 않을까? 생각했었는데 당연히 안된다.
이게 안됬던 이유는 normDir벡터가 View 공간에서 계산된 벡터라는것을 가정하지 않았기 때문이다.
실제 이 상태로 프로그램을 구동해보면 이런식으로 나오는걸 볼 수가 있다.


위 결과를 통해서 어떤 상황이 벌어졌는지 비로소 이해가 갔다.
한마디로 하나의 반구는 counter-clockWise로 렌더링이 되었는데 다른쪽 반구는 clockWise로 렌더링이 되면서 서로 맞물리지 않는것이다. 따라서 다른쪽 반구는 x방향(또는 y방향)도 반전시켜줌으로써 면이 컬링되는 부분을 역전시켜준것이다.
당연히 HemiDir = (1, -1, -1)로 줘도 작동한다.
// Transform all the way to projected space
DS_OUTPUT Output;
Output.Position = mul( posLS, LightProjection );
// Store the clip space position
Output.cpPos = Output.Position.xy / Output.Position.w;
이 코드는 이제 View행렬과 Projection행렬을 곱한 LightProjection행렬을 곱한것이다.
그러면 이제 Output.Position은 Clip좌표계로 변환이 완료되었지만 실제 빛 연산을 수행하기 위해서
w성분을 나눔으로써 NDC좌표계로 변환하면서 Domain Shader가 끝나게 된다.
결과

마무리
오랜만에 기술글을 썼는데 다음에는 내가 이전에 올렸던 글중
https://mawile.tistory.com/374
특정 축을 기준으로 회전하는 알고리즘 아이디어
그냥 선형대수학 공부하고있는데 뜬금없이 생각나서 적어본다. 나중에 까먹을 가능성이 높아서 여기다 적어놓는다. 틀릴수도있는데 일단 되게 간단하고 직접 손으로 값대입해보면서 넣어봤는
mawile.tistory.com
이 글이 있다.
특정 축을 기준으로 회전하는 알고리즘을 구현해보고 실험해보는 글을 올릴 생각이다.
기본적으로 해당 글에 오류가 있는데 그 부분을 지적해보고 좋은 알고리즘을 공부해와서 그 알고리즘을 글로 쓸것이다.
아주 좋다 ㅎㅎ
'🕹️자체엔진 > DirectX 11 개인공부' 카테고리의 다른 글
DirectX11 공부 7주차. 멀티 텍스쳐링과 텍스쳐 배열 (0) | 2022.03.18 |
---|---|
rastertek강좌에서 소개하는 프러스텀 컬링 기법이 의미하는 수학적 해석 (0) | 2022.01.22 |
DirectX11 공부 6주차. 폰트 엔진, DirectInput (0) | 2021.11.27 |
DirectX11 공부 5주차. 2D모델 렌더링 (0) | 2021.11.20 |
DirectX11 공부 4주차. 정반사광, 인스턴싱 (2) | 2021.11.17 |
댓글