Geometry Shader
아주 멀리있는 오브젝트나 반복되는 물체는 Billboard를 통해서 작업하게 된다. 그전에 작업했던 방식을 떠올려보면 하나의 vertex buffer에 정점을 하나의 위치에 4개를 전부 다 배치한 다음 쉐이더에 넘겨주고, 쉐이더 측에서 정점을 옮겨 위치를 잡아주는 형식을 사용했다. 하지만 이보다 조금 더 우월한 방식을 사용해보자.
렌더링 파이프라인에서 VS와 RS 단계 사이에 Geometry Shader 단계가 있다. 이 친구가 하는 역할은 입력 받은 정점의 개수를 임의로 늘렸다 줄였다 할 수 있게 해준다. 즉, 정점을 맘대로 늘릴 수 있기 때문에 입력된 오브젝트를 복사 아닌 복사가 가능하다.
나무를 빌보드를 이용해 만들 것인데, GeometryShader를 이용한다.
코드를 살펴보자.
void TreeBillboardDemo::BuildTreeSpritesBuffer()
{
Vertex::TreePointSprite v[TreeCount];
for (UINT i = 0; i < TreeCount; ++i)
{
float x = MathHelper::RandF(-35.0f, 35.0f);
float z = MathHelper::RandF(-35.0f, 35.0f);
float y = GetHillHeight(x, z);
// Move tree slightly above land height.
y += 10.0f;
v[i].pos = XMFLOAT3(x, y, z);
v[i].size = XMFLOAT2(24.0f, 24.0f);
}
D3D11_BUFFER_DESC vbd;
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof(Vertex::TreePointSprite) * TreeCount;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
vbd.MiscFlags = 0;
D3D11_SUBRESOURCE_DATA vinitData;
vinitData.pSysMem = v;
HR(_device->CreateBuffer(&vbd, &vinitData, _treeSpritesVB.GetAddressOf()));
}
나무의 개수 만큼 버퍼를 만들 것인데, 이때 각 버퍼에 들어가 있는건 단 하나의 정점, 나무의 위치만이 버퍼에 들어가 있다. 이를 쉐이더에 넘겨주게 되면 그 위치를 기준으로 4개의 정점으로 늘린다음, 그 안에 물체를 그려주게 될 것이다.
// We expand each point into a quad (4 vertices), so the maximum number of vertices
// we output per geometry shader invocation is 4.
[maxvertexcount(4)]
void GS(point VertexOut gin[1], uint primID : SV_PrimitiveID, inout TriangleStream<GeoOut> triStream)
{
//
// Compute the local coordinate system of the sprite relative to the world
// space such that the billboard is aligned with the y-axis and faces the eye.
//
float3 up = float3(0.0f, 1.0f, 0.0f);
float3 look = gEyePosW - gin[0].CenterW;
look.y = 0.0f; // y-axis aligned, so project to xz-plane
look = normalize(look);
float3 right = cross(up, look);
//
// Compute triangle strip vertices (quad) in world space.
//
float halfWidth = 0.5f*gin[0].SizeW.x;
float halfHeight = 0.5f*gin[0].SizeW.y;
float4 v[4];
v[0] = float4(gin[0].CenterW + halfWidth*right - halfHeight*up, 1.0f);
v[1] = float4(gin[0].CenterW + halfWidth*right + halfHeight*up, 1.0f);
v[2] = float4(gin[0].CenterW - halfWidth*right - halfHeight*up, 1.0f);
v[3] = float4(gin[0].CenterW - halfWidth*right + halfHeight*up, 1.0f);
//
// Transform quad vertices to world space and output
// them as a triangle strip.
//
GeoOut gout;
[unroll]
for(int i = 0; i < 4; ++i)
{
gout.PosH = mul(v[i], gViewProj);
gout.PosW = v[i].xyz;
gout.NormalW = look;
gout.Tex = gTexC[i];
gout.PrimID = primID;
triStream.Append(gout);
}
}
VS 단계가 끝나면 이 새로 만든 GS 단계로 넘어오게 된다.
maxvertexcount를 4로 설정하고 VertexOut gin[1]를 받는다는 의미는 정점 하나를 입력 받고 이를 4개로 늘려서 돌려주겠다는 의미이다.
코드 안에서 카메라를 바라보게 만든 후 4개의 정점을 만든 후 triStream에 append함으로 밀어 넣고 건네주게 된다.
나무나 눈 같은 1000개의 오브젝트가 있을 때 되게 유용하게 쓸 수 있다. 이를 발전 시킨 것이 결국 Effect이다.
Compute Shader
Compute Shdaer를 왜 사용하느냐 에 대한 질문에 대한 답을 할 때 CPU와 GPU를 잘 이해하고 있는지 여부가 중요하다.
CPU는 코어 개수가 많지 않은, ALU의 비율보다 메모리의 비율이 높다. 반면 GPU는 ALU가 메모리보다 훨씬 자리를 많이 차지 하기 때문에 단순한 계산 연산을 하는데 유리하다.
병렬 처리를 해야하는 연산, 수 많은 정점들의 좌표를 계산하고 빛과 그림자를 계산하는 등 단순하지만 많은 연산을 요구하는 것은 CPU가 GPU에게 일을 떠넘겨 진행하는 것이다.
그런데 꼭 게임에서도 렌더링을 하는 부분 뿐만아니라 GPU의 연산속도가 필요로 할 때가 있다.
렌더링 파이프라인을 구글에 검색하면 여러 그림들이 나올텐데 그 중 Compute Shader는 어느 단계 인가??
찾을 수 없다. 즉, 렌더링 파이프라인 이랑 별개로 우리가 따로 Compute Shader를 만들어서 독단적으로 실행하면 된다.
물방울책에 있는 VecAddDemo는 GPU에게 두개의 값을 던져주면 계산해서 txt파일로 내보내게 하는 실습이다.
struct Data
{
float3 v1;
float2 v2;
};
StructuredBuffer<Data> gInputA;
StructuredBuffer<Data> gInputB;
RWStructuredBuffer<Data> gOutput;
[numthreads(32, 1, 1)]
void CS(int3 dtid : SV_DispatchThreadID)
{
gOutput[dtid.x].v1 = gInputA[dtid.x].v1 + gInputB[dtid.x].v1;
gOutput[dtid.x].v2 = gInputA[dtid.x].v2 + gInputB[dtid.x].v2;
}
technique11 VecAdd
{
pass P0
{
SetVertexShader(NULL);
SetPixelShader(NULL);
SetComputeShader(CompileShader(cs_5_0, CS()));
}
}
StructureBuffer Input 두개를 만들고 이를 더해서 RWStructureBuffer를 만들어서 CPU에 돌려주는 아주 간단한 쉐이더 이다.
BulrDemo의 결과는 다음과 같다. GPU에게 계산을 떠 넘겨서 뭘 할 수 있을까? 에대한 예제이다.
가우시안 블러 기법을 이용했는데 이를 간단히 얘기하면 각 픽셀의 주변 영역을 전부 가져와 합산 한 후 대표 색상을 뽑아내 해당 픽셀에 적용하는 기법이다.
이를 가로로 한번 세로로 한번 총 두번을 진행해서 블러를 주게된다.
모든 픽셀에 대한 연산은 CPU가 하기에는 너무 단순하고 오래걸리기 때문에 GPU에게 넘겨서 진행한 것이다.
그리고 블러처리를 하기 위해 원래는 우리가 물이나 나무, 산 같은 것을 View에다 그렸는데 그것이 아닌 텍스처에 그리게 될 것이다.
먼저 텍스처에 우리가 그리고자 하는 오브젝트를 전부 그린 후
이를 가우시안 블러 처리 연산을한다.
그리고 그 결과 값을 화면에 보여주게 조작하면 된다.
// Render to our offscreen texture. Note that we can use the same depth/stencil buffer
// we normally use since our offscreen texture matches the dimensions.
ID3D11RenderTargetView* renderTargets[1] = { _offscreenRTV.Get()};
_deviceContext->OMSetRenderTargets(1, renderTargets, _depthStencilView.Get());
_deviceContext->ClearRenderTargetView(_offscreenRTV.Get(), reinterpret_cast<const float*>(&Colors::Silver));
_deviceContext->ClearDepthStencilView(_depthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
//
// Draw the scene to the offscreen texture
//
DrawWrapper();
맨 처음 Draw를 시작할 때 _offscreenRTV를 타겟으로 설정해서 여기다가 그려달라고 요청하게 된다. 즉, 이 텍스처 위에다가 오브젝트를 그려달라고 한것이다. 그리고 DrwaWrapper를 통해 원래 있던 오브젝트를 _offscreenRTV에 그리게 된다.
renderTargets[0] = _renderTargetView.Get();
_deviceContext->OMSetRenderTargets(1, renderTargets, _depthStencilView.Get());
//mBlur.SetGaussianWeights(4.0f);
_blur.BlurInPlace(_deviceContext, _offscreenSRV, _offscreenUAV, 4);
//
// Draw fullscreen quad with texture of blurred scene on it.
//
_deviceContext->ClearRenderTargetView(_renderTargetView.Get(), reinterpret_cast<const float*>(&Colors::Silver));
_deviceContext->ClearDepthStencilView(_depthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
그 후 다시 최종 결과물을 그릴 renderTarget으로 바꿔 준후 blur 처리를 요청하게된다.
blur가 처리되면 이를 다시 최종 화면에 그리게 된다.
cbuffer cbSettings
{
float gWeights[11] =
{
0.05f, 0.05f, 0.1f, 0.1f, 0.1f, 0.2f, 0.1f, 0.1f, 0.1f, 0.05f, 0.05f,
};
};
cbuffer cbFixed
{
static const int gBlurRadius = 5;
};
Texture2D gInput;
RWTexture2D<float4> gOutput;
#define N 256
#define CacheSize (N + 2*gBlurRadius)
groupshared float4 gCache[CacheSize];
[numthreads(N, 1, 1)]
void HorzBlurCS(int3 groupThreadID : SV_GroupThreadID,
int3 dispatchThreadID : SV_DispatchThreadID)
{
//
// Fill local thread storage to reduce bandwidth. To blur
// N pixels, we will need to load N + 2*BlurRadius pixels
// due to the blur radius.
//
// This thread group runs N threads. To get the extra 2*BlurRadius pixels,
// have 2*BlurRadius threads sample an extra pixel.
if (groupThreadID.x < gBlurRadius)
{
// Clamp out of bound samples that occur at image borders.
int x = max(dispatchThreadID.x - gBlurRadius, 0);
gCache[groupThreadID.x] = gInput[int2(x, dispatchThreadID.y)];
}
if (groupThreadID.x >= N - gBlurRadius)
{
// Clamp out of bound samples that occur at image borders.
int x = min(dispatchThreadID.x + gBlurRadius, gInput.Length.x - 1);
gCache[groupThreadID.x + 2 * gBlurRadius] = gInput[int2(x, dispatchThreadID.y)];
}
// Clamp out of bound samples that occur at image borders.
gCache[groupThreadID.x + gBlurRadius] = gInput[min(dispatchThreadID.xy, gInput.Length.xy - 1)];
// Wait for all threads to finish.
GroupMemoryBarrierWithGroupSync();
//
// Now blur each pixel.
//
float4 blurColor = float4(0, 0, 0, 0);
[unroll]
for (int i = -gBlurRadius; i <= gBlurRadius; ++i)
{
int k = groupThreadID.x + gBlurRadius + i;
blurColor += gWeights[i + gBlurRadius] * gCache[k];
}
gOutput[dispatchThreadID.xy] = blurColor;
}
[numthreads(1, N, 1)]
void VertBlurCS(int3 groupThreadID : SV_GroupThreadID,
int3 dispatchThreadID : SV_DispatchThreadID)
{
//
// Fill local thread storage to reduce bandwidth. To blur
// N pixels, we will need to load N + 2*BlurRadius pixels
// due to the blur radius.
//
// This thread group runs N threads. To get the extra 2*BlurRadius pixels,
// have 2*BlurRadius threads sample an extra pixel.
if (groupThreadID.y < gBlurRadius)
{
// Clamp out of bound samples that occur at image borders.
int y = max(dispatchThreadID.y - gBlurRadius, 0);
gCache[groupThreadID.y] = gInput[int2(dispatchThreadID.x, y)];
}
if (groupThreadID.y >= N - gBlurRadius)
{
// Clamp out of bound samples that occur at image borders.
int y = min(dispatchThreadID.y + gBlurRadius, gInput.Length.y - 1);
gCache[groupThreadID.y + 2 * gBlurRadius] = gInput[int2(dispatchThreadID.x, y)];
}
// Clamp out of bound samples that occur at image borders.
gCache[groupThreadID.y + gBlurRadius] = gInput[min(dispatchThreadID.xy, gInput.Length.xy - 1)];
// Wait for all threads to finish.
GroupMemoryBarrierWithGroupSync();
//
// Now blur each pixel.
//
float4 blurColor = float4(0, 0, 0, 0);
[unroll]
for (int i = -gBlurRadius; i <= gBlurRadius; ++i)
{
int k = groupThreadID.y + gBlurRadius + i;
blurColor += gWeights[i + gBlurRadius] * gCache[k];
}
gOutput[dispatchThreadID.xy] = blurColor;
}
technique11 HorzBlur
{
pass P0
{
SetVertexShader(NULL);
SetPixelShader(NULL);
SetComputeShader(CompileShader(cs_5_0, HorzBlurCS()));
}
}
technique11 VertBlur
{
pass P0
{
SetVertexShader(NULL);
SetPixelShader(NULL);
SetComputeShader(CompileShader(cs_5_0, VertBlurCS()));
}
}
'C++ > DirectX 11' 카테고리의 다른 글
[DirectX] 물방울 책 - Picking, Mapping (1) | 2024.10.29 |
---|---|
[DirectX] 물방울 책 - Tessellation (3) | 2024.10.28 |
[DirectX] 물방울 책 - 조명, 텍스처 (0) | 2024.10.24 |
[DirectX] 물방울 책 - 랜더링 파이프라인, 그리기 연산 (0) | 2024.10.23 |
[DirectX] GPU - ViewPort, Collider, Picking (0) | 2024.10.17 |