1. 3D Direct Application
1.1 Particle System
1.1.1 Particle
1.1.1.1 Particle Algorithm
- 입자를 점으로 저장하되 실제로 렌더링할 때 기하 셰이더에서 그 점을 카메라를 바라보는 사각형으로 확장
- 나무 빌보드에 사용했던 것과 비슷한 전략 사용, 입자 빌보드의 경우 카메라를 정면으로 바라본다는 점이 다름
- 세계 공간 상향 벡터, 빌보드 중심 위치, 시점 위치를 알고 있으면 세계 공간 좌표로 나타낼 수 있음
struct Particle
{
XMFLOAT3 InitialPos; XMFLOAT3 InitialVel;
XMFLOAT2 Size; float Age; uint32 Type;
};
- 입자들이 물리적으로 사실감 있게 움직이게 하는 것
- 단순함을 위해 입자의 알짜 가속도가 고정되어 있다고 가정, 입자들 사이 충돌은 고려하지 않음
- 입자의 궤적, 즉 임의의 시간 t에 입자의 위치는 전적으로 초기 위치, 초기 속도, 상수 가속도에 의해 결정
- 입자가 어디에서 출발해서 어떤 바향으로 얼마나 빠르게 움직이기 시작하는지 알고 시간에 따라 입자에 어떤 가속도가 가해지는 지 알면 입자가 어떤 경로를 따라 나아갈 것인지 예측할 수 있음
1.1.1.2 Random Particle
XMVECTOR MathHelper::RandUnitVec3();
- 입자 시스템의 모든 입자가 비슷하게 움직여야 하지만, 모두가 정확히 동일하게 움직인다면 원하는 효과를 얻기 힘듦
- 입방체의 무작위 점 하나를 생성하여 반구 위, 아래 점이 나올 때까지 시도
- 단위 구에 대한 고른 분포를 얻기 위해 단위 구 바깥의 점을 무시
XMVECTOR One = XMVectorSet(1.0f, 1.0f, 1.0f, 1.0f); XMVECTOR Zero = XMVectorZero();
while(true)
{
XMVECTOR v = XMVectorSet(MathHelper::RandF(-1.0f, 1.0f),
MathHelper::RandF(-1.0f, 1.0f),
MathHelper::RandF(-1.0f, 1.0f), 0.0f);
if( XMVector3Greater( XMVector3LengthSq(v), One) ) continue;
return XMVector3Normalize(v);
}
- 난수는 셰이더 코드 또한 필요, 난수 발생을 위한 내장 함수가 없음
- 따라서 각 픽셀이 부동소수점 성분 네 개로 이루어진 1차원 Texuture를 사용하여 난수 발생기로 사용
- Texture로 부터 각 좌표성분이 [-1, 1] 구간의 난수인 무작위 4차원 벡터들을 채워 넣음
- 셰이더는 Texture의 표본을 추출해서 난수를 얻음
- 무작위 Texture를 추출하는 구체적인 방법 2가지를 설명
ComPtr<ID3D11ShaderResourceView> Utils::CreateRandomTexture1DSRV(ComPtr<ID3D11Device> device);
- 입자들의 x 좌표 성분이 서로 다르다면 그 x 좌표를 Texture 좌표를 사용해서 난수를 얻음
- 현재의 게임 시간 값을 Texture 좌표로 사용, 서로 다른 시간에 생성된 입자들에 대해 서로 다른 난수 값이 배정
- 같은 시간에 생성된 입자들은 같은 난수 값을 가지게 된다는 점을 주의
- 게임 시간에 서로 다른 Texture 좌표 오프셋을 더해 각 입자마다 Texture 맵의 서로 다른 표본을 추출하도록 설정
1.1.1.2.1 Texture, Shader Resource View
vector<XMFLOAT4> randomValues(1024);
for (int32 i = 0; i < 1024; ++i)
{
randomValues[i].x = MathHelper::RandF(-1.0f, 1.0f); randomValues[i].y = MathHelper::RandF(-1.0f, 1.0f);
randomValues[i].z = MathHelper::RandF(-1.0f, 1.0f); randomValues[i].w = MathHelper::RandF(-1.0f, 1.0f);
}
D3D11_SUBRESOURCE_DATA initData; D3D11_TEXTURE1D_DESC texDesc; ComPtr<ID3D11Texture1D> randomTex;
HR(device->CreateTexture1D(&texDesc, &initData, randomTex.GetAddressOf()));
D3D11_SHADER_RESOURCE_VIEW_DESC viewDesc; ComPtr<ID3D11ShaderResourceView> randomTexSRV;
HR(device->CreateShaderResourceView(randomTex.Get(), &viewDesc, randomTexSRV.GetAddressOf()));
1.1.1.2.2 Shader Code
- Game Time + Offset을 무작위 Texture 추출을 위한 Texture 좌표로 사용 후 단위 구로 투영
float3 RandUnitVec3(float offset)
{
float u = (gGameTime + offset);
float3 v = gRandomTex.SampleLevel(samLinear, u, 0).xyz;
return normalize(v);
}
1.1.1.3 Blend Particle
- 입자 시스템에서 입자들을 어떤 형태로든 혼합해서 그리는 경우가 많음
- 불이나 마법 주문 같은 효과의 경우 입자가 있는 장소가 밝아져야 함
- 이를 위해 가산 혼합, 즉 원본 색상에 대상 색상을 더하는 방식의 혼합이 적합
- 입자들을 반투명하게 그리는 경우도 많음, 원본 입자 색상을 불투명도에 맞게 비례할 필요가 있음
- 원본 입자의 색상이 색상들의 합에 기여하는 정도는 원본 입자의 불투명도에 의해 결정
- 입자가 불투명할수록 더 많은 색을 기여하게 되는 것
SrcBlend = SRC_ALPHA; DestBlend = ONE; BlendOp = ADD;
- 이렇게 하지 않으면 미리 Texture에 불투명도를 곱해 Texture 색상이 불투명도에 따라 희석되게 할 수 도 있음
- 실행시점에 희석된 Texture를 입자에 적용, 미리 계산을 통해 Texture 자료 자체에 집어넣었기에 밑의 매개변수 사용
SrcBlend = ONE; DestBlend = ONE; BlendOp = ADD;
- 가산 혼합은 입자들이 있는 영역의 밝기가 입자들의 밀도에 비례한다는 멋진 효과도 제공
- 즉 입자들이 많이 몰려 있는 영역일수록 더욱 밝게 나타나는데 일반적으로 바람직한 효과
- 연기를 시뮬레이션하는 등의 상황에서 가산혼합이 그리 적합하지 않음
- 연기 입자가 많이 몰릴수록 영역이 더 밝아진다는 것은 좀 이상하기 때문
- 연기의 경우 대상 색상에서 원본 색상을 빼는 감산 연산자를 사용
- 연기 입자가 많은 곳일수록 더 어두워져서 연기가 짙게 깔려 있다는 느낌을 줌
- 반대로 연기 입자의 밀도가 낮은 곳은 덜 어두워져 연기가 옅게 깔려 있는 느낌을 줌
- 위의 방식의 경우 검은 연기에는 잘 통하지만 밝은 회색 연기에는 그렇지 않음
- 다른 방법은 연기 입자들이 바누명 물체라고 간주해서 투명도 혼합을 적용
- 투명도 혼합에서 혼합할 물체들을 시점 기준으로 뒤에서 앞의 순서로 적용, 입자들이 많은 경우 정렬 비용이 커짐
- 입자들의 무작위성 때문에 입자들을 굳이 정렬하지 않아도 눈에 띄는 렌더링 결함이 발생하지 않을 수 있음
1.2 Stream Out
1.2.1 Stream Out Stage
- GPU에서 Texture에 자료를 기록, Depth & Stencil Buffer에 자료를 기록하며 후면 버퍼에도 기록
- 스트림 출력 단계를 이용하면 GPU에서 스트림 출력 단계에 묶인 정점 버퍼 V에 기하구조 자체를 직접 기록
- 기하 셰이더에 출력한 정점들이 V에 기록, V에 저장된 기하구조를 렌더링 파이프라인에 입력해서 그리는 것이 가능
- 스트림 출력 기능은 입자 시스템 프레임워크에서 중요한 역할 수행
1.2.1.1 Geometry Shader
- 스트림 출력을 위해 기하 셰이더를 특별한 형태로 작성
- 첫 매개변수의 경우 컴파일된 기하 셰이더 프로그램, 둘째 매개변수는 스트림으로 출력할 정점들의 형식을 서술
- 위에서 생성한 정점 형식에 해당
GeometryShader gsStreamOut = ConstructGSWithSO(CompileShader(gs_5_0, StreamOutGS()),
"POSITION.xyz; VELOCITY.xyz; SIZE.xy; AGE.x; TYPE.x");
SetGeometryShader(gsStreamOut); /* Technique & Pass */
- 특별한 설정이 없는 한 기하 셰이더가 출력한 정점은 스트림으로 출력 & 렌더링 파이프라인의 다음 단계로도 입력
- 자료를 스트림으로 출력하기만 하고 렌더링하지는 않는 기법에서 픽셀 셰이더, Depth & Stencil 버퍼를 비활성화
DepthStencilState DisableDepth { DepthEnable = FALSE; DepthWriteMask = ZERO; };
SetPixelShader(NULL); SetDepthStencilState(DisableDepth, 0); /* Technique & Pass */
- 아니면 렌더링 파이프라인의 출력 병합기 단계에서 널 값에 해당하는 렌더 대상과 Depth & Stencil 버퍼를 묶어 렌더링이 사실상 일어나지 않게 하는 방법도 있음
- 입자 시스템 프레임워크는 스트림 출력 전용의 한 기법을 사용
- 전적으로 입자를 생성하고 파괴하는데에만 쓰임, 즉 입자 시스템을 갱신
- 현재 입자 몰록을 스트림 출력 전용으로 그림, 래스터화기가 비활성화되어 있으므로 렌더링X
- 주어진 조건에 따라 입자들을 생성하거나 파괴
- 갱신된 입자 목록을 스트림 출력을 통해 정점 버퍼에 기록
- 응용 프로그램은 현재 프레임에서 갱신된 입자 목록을 다른 어떤 렌더링 기법을 이용하여 화면에 그림
- 기하 셰이더가 서로 다른 두 종류의 일을 해야 하기 때문에 두 종류의 기법을 사용
- 스트림 출력 전용 기법에서 기하 셰이더는 입자들을 입력받아 갱신한 후 출력
- 화면 렌더링 기법에서 기하 셰이더는 주어진 점을 카메라를 향한 사각형으로 확장
- 위 두 경우 기하 셰이더가 출력하는 기본도형의 종류 자체가 다르므로 기하 셰이더를 두 종류로 만들 필요가 있음
- 입자 시스템을 갱신하는데 쓰이는 스트림 출력 전용, 입자 시스템을 화면에 그리는 데 쓰이는 렌더링 기법
1.2.1.2 Vertex Buffer
- GPU가 정점들을 기록할 정점 버퍼를 스트림 출력 단계에 묶기 위해 정점 버퍼 생성 시 반드시 결속 플래그 지정
- 스트림 출력의 대상으로 쓰이는 정점 버퍼는 이후 파이프라인의 입력으로도 쓰임
D3D11_BIND_STREAM_0UTPUT; D3D11_BIND_VERTEX_BUFFER;
- 버퍼 메로리를 초기화하지 않고 놔둔다는 점을 주목, 정점 자료를 GPU가 기록
- 버퍼의 크기가 유한, 기하 셰이더가 스트림 출력으로 보낸 정점들이 버퍼의 최대 용량을 넘지 않도록 주의
D3D11_BUFFER_DESC vbd;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_STREAM_OUTPUT;
HR(device->CreateBuffer(&vbd, 0, _streamOutVB.GetAddressOf()));
- 정점 버퍼를 스트림 출력 단계에 묶을 때 밑의 함수를 사용
dc->SOSetTargets(1, _streamOutVB.GetAddressOf(), &offset);
- 스트림 출력으로 정점들을 정점 버퍼에 모두 기록한 다음 그 정점들이 정의하는 기본도형들을 실제로 렌더링에 사용
- 하나의 정점 버퍼를 스트림 출력 단계와 입력 조립기 단계에 동시에 묶을 수 없음
- 다른 어떤 버퍼를 정점 버퍼가 묶인 슬롯에 다시 묶음
- 슬롯 0에 NULL 버퍼를 설정함으로써 원래 묶여 있는 정점 버퍼를 떼어냄
ID3D11Buffer* bufferArray[1] = { 0 }; dc->SOSetTargets(1, bufferArray, &offset);
- 기하 셰이더가 스트림으로 정점 버퍼에 기록한 기하구조가 매 프레임마다 다를 수 있음
- 정점들의 개수도 다를 수 잇으므로 내부적으로 정점 개수를 관리하는 함수 사용
void ID3D11DeviceContext::DrawAuto();
- 하나의 정점 버퍼를 출력 병합기 단계와 입력 조립기 단계에 동시에 묶어 둘 수 없음
- 두 개의 버퍼를 핑퐁 방식으로 갱신하는 기법이 유용
- 스트림 출력으로 기본도형들을 정점 버퍼에 기록할 때 정점 버퍼를 입력으로 사용
- 다른 정점 버퍼를 출력 대상으로 사용
- 그 다음 번의 렌더링 프레임에서 두 버퍼의 역할을 맞바꿈
std::swap(_drawVB, _streamOutVB);
'C++ Algorithm & Study > Game Math & DirectX 11' 카테고리의 다른 글
[Direct11] 43. 3D Direct Application - Shadow Mapping #1 (0) | 2023.07.03 |
---|---|
[Direct11] 42. 3D Direct Application - Particle System #2 (0) | 2023.07.03 |
[Direct11] 40. 3D Direct Application - Terrian Rendering #2 (0) | 2023.06.30 |
[Direct11] 39. 3D Direct Application - Terrian Rendering #1 (0) | 2023.06.30 |
[Direct11] 38. 3D Direct Application - Displacement Mapping (0) | 2023.06.30 |