화면의 한 픽셀에서 광선을 보내 봅시다
Raster에서는 삼각형이 덮는 픽셀을 찾았습니다. 이번에는 반대로 카메라에서 특정 픽셀 방향으로 광선을 보내고, 그 광선이 처음 만나는 표면의 색을 구한다고 생각해 봅시다. 아무것도 만나지 않으면 회색 배경을 돌려줍니다. 이 작은 예제로 DXR의 가속 구조, 셰이더 연결, Shader Table이 각각 왜 필요한지 살펴보겠습니다.
이 글은 DXR API의 연결을 배우는 선택 강의이며 현재 YamYam에 레이트레이싱 렌더러를 구현한 기록은 아닙니다. Device5·CommandList4, DXC lib_6_3 이상, OPTIONS5의 RaytracingTier 지원을 확인한 환경을 전제로 합니다. 코드 블록은 각 연결부의 발췌이며 버퍼 할당·카메라 업로드·최종 표시를 포함한 완전한 실행 프로그램은 아닙니다.
DXR은 가속 구조를 탐색해 광선과 도형의 교차를 처리하는 기능을 제공합니다. 반사·그림자·전역조명에 활용할 수 있지만, Path Tracing의 재질 평가·샘플링·누적·노이즈 제거까지 자동으로 구현하지는 않습니다.
Compiling Shaders
이 글은 Ray Generation·Intersection·Any Hit·Closest Hit·Miss를 중심으로 설명합니다. DXR에는 Callable Shader도 있으며, 이 예제에서는 사용하지 않습니다.
•
Ray Generation - 시작 광선을 생성합니다. 예를 들어 뷰포트의 시점에서 나오는 광선이 될 수 있습니다.•
Intersection은 절차적 도형의 후보 AABB 안에서 실제 교차를 계산하고 보고하는 셰이더입니다. 삼각형 예제에서는 내장 삼각형 교차를 사용하므로 별도 Intersection Shader를 넣지 않습니다.
•
Any Hit: 탐색 중 후보 교차를 허용하거나 무시할 때 사용합니다. 거리 순으로 모든 교차점이 한 번씩 보고된다고 가정하면 안 됩니다. 알파 테스트 등에 활용합니다.
•
Closest Hit: 허용된 교차 중 광선의 원점에 가장 가까운 교차를 처리합니다. 내부 탐색에서 가장 먼저 발견한 교차와 같은 뜻은 아닙니다.
•
Miss - 광선이 씬의 어떤 것과도 충돌하지 않을 때 발생합니다. 스카이박스의 기여도를 추가하는 데 유용합니다.광선이 맞았을 때와 빗나갔을 때 색을 돌려줍니다
RayGeneration은 출력 픽셀의 가운데를 지나는 광선을 만듭니다. TraceRay는 TLAS를 탐색하고, 허용된 최근접 교차가 있으면 ClosestHit, 없으면 Miss 경로로 결과를 채웁니다. RayPayload는 그 결과를 호출자에게 돌려주는 데이터입니다. 이번에는 float4 색 하나만 담습니다.
// DXC lib_6_3 이상으로 컴파일하는 HLSL 예시.
RaytracingAccelerationStructure Scene : register(t0);
RWTexture2D<float4> tOutput : register(u0);
cbuffer CameraCB : register(b0)
{
row_major float4x4 inverseViewProjection;
float3 cameraOrigin;
float maxRayDistance;
};
struct RayPayload { float4 color; };
struct LocalCB { float time; };
ConstantBuffer<LocalCB> localCB : register(b1);
Texture2D<float4> localTex : register(t1);
SamplerState localSampler : register(s0);
[shader("raygeneration")]
void raygen()
{
float2 uv = (float2(DispatchRaysIndex().xy) + 0.5) / float2(DispatchRaysDimensions().xy);
float2 ndc = float2(uv.x * 2 - 1, 1 - uv.y * 2);
// 일반 Direct3D 깊이·유한 far plane의 원근 카메라 예시.
float4 world = mul(float4(ndc, 1, 1), inverseViewProjection);
world.xyz /= world.w;
RayDesc ray;
ray.Origin = cameraOrigin;
ray.Direction = normalize(world.xyz - cameraOrigin);
ray.TMin = 0.001;
ray.TMax = maxRayDistance;
RayPayload payload = {float4(0, 0, 0, 0)};
TraceRay(Scene, RAY_FLAG_NONE, 0xff, 0, 1, 0, ray, payload);
tOutput[DispatchRaysIndex().xy] = payload.color;
}
[shader("closesthit")]
void closesthit(inout RayPayload payload, in BuiltInTriangleIntersectionAttributes attr)
{
// 무게중심 좌표를 임시 UV로 쓰는 데모입니다. 실제 정점 UV 보간은 별도입니다.
payload.color = localTex.SampleLevel(localSampler,
attr.barycentrics + float2(sin(localCB.time) * 0.1, 0), 0);
}
[shader("miss")]
void miss(inout RayPayload payload) { payload.color = float4(0.5, 0.5, 0.5, 1); }
C++
복사
DispatchRaysIndex는 현재 출력 위치이고 +0.5는 픽셀 가운데를 선택합니다. 0~1 UV를 클립 공간의 x,y로 바꾼 뒤 역 ViewProjection으로 월드의 먼 점을 구합니다. 카메라 위치에서 그 점으로 향하는 방향을 정규화해 RayDesc에 넣습니다. 이 코드는 일반 깊이·유한 far plane의 원근 카메라를 가정합니다. 직교 카메라라면 픽셀별 원점과 공통 방향을 만드는 다른 생성식이 필요합니다.
TMin과 TMax는 광선에서 탐색할 거리 구간입니다. payload는 처음 검정으로 채우고 TraceRay가 돌아오면 결과 색을 UAV의 현재 픽셀에 씁니다. ClosestHit의 barycentrics를 임시 UV로 쓰므로 이 코드는 모델의 실제 UV를 복원하는 구현이 아닙니다. 실제 재질을 붙이려면 정점 UV를 읽고 교차점의 무게중심 값으로 보간해야 합니다. time을 바꾸면 이 임시 UV의 x가 조금 이동합니다.
AnyHit는 후보 교차를 허용하거나 무시하는 데 쓸 수 있지만 모든 교차가 거리 순으로 한 번씩 호출된다고 가정하면 안 됩니다. 이번 BLAS는 OPAQUE 삼각형이며 AnyHit·사용자 Intersection을 사용하지 않습니다. 색 하나를 돌려주는 이 예제만으로 여러 반사·간접광·누적·노이즈 제거가 생기는 것도 아닙니다.
Root Signature
Global Root Signature는 DispatchRays 전에 바인딩할 공통 리소스를 설명합니다. Local Root Signature는 필요한 shader record에 인자를 붙이는 선택적 구성입니다. 모든 셰이더에 local 인자가 필수인 것은 아닙니다.
Global 인자는 이번 Dispatch가 사용할 TLAS·카메라 상수·출력 UAV 같은 공통 바인딩입니다. Dispatch 간 데이터 공유는 리소스와 명령의 수명·갱신 정책으로 결정됩니다.
한 번의 Dispatch에서 함께 쓰는 데이터는 Global로 연결합니다
모든 픽셀이 같은 카메라와 장면을 보므로 카메라 b0, 장면 TLAS t0, 출력 u0를 Global Root Signature로 묶습니다. 이번 descriptor heap에서는 [0]에 UAV, [1]에 카메라 CBV, [2]에 재질 텍스처 SRV를 놓는다고 정합니다.
// 👋 Declare Handles
ID3D12RootSignature* globalRootSignature = nullptr;
// Global Root Signature
// These can be configured prior to DispatchRays
// Output radiance UAV
D3D12_DESCRIPTOR_RANGE1 ranges[2];
ranges[0].BaseShaderRegister = 0;
ranges[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_UAV;
ranges[0].NumDescriptors = 1;
ranges[0].RegisterSpace = 0;
ranges[0].OffsetInDescriptorsFromTableStart = 0;
ranges[0].Flags = D3D12_DESCRIPTOR_RANGE_FLAG_DATA_VOLATILE;
// Camera Matrices CBV
ranges[1].BaseShaderRegister = 0;
ranges[1].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
ranges[1].NumDescriptors = 1;
ranges[1].RegisterSpace = 0;
ranges[1].OffsetInDescriptorsFromTableStart =
D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
ranges[1].Flags = D3D12_DESCRIPTOR_RANGE_FLAG_DATA_VOLATILE |
D3D12_DESCRIPTOR_RANGE_FLAG_DESCRIPTORS_VOLATILE;
D3D12_ROOT_PARAMETER1 rootParameters[2];
// UAV, CBV
rootParameters[0].ParameterType =
D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
rootParameters[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
rootParameters[0].DescriptorTable.NumDescriptorRanges =
_countof(ranges);
rootParameters[0].DescriptorTable.pDescriptorRanges = ranges;
// Acceleration Structures
rootParameters[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_SRV;
rootParameters[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
rootParameters[1].Descriptor = {};
D3D12_VERSIONED_ROOT_SIGNATURE_DESC rootSignatureDesc;
rootSignatureDesc.Version = D3D_ROOT_SIGNATURE_VERSION_1_1;
rootSignatureDesc.Desc_1_1.Flags = D3D12_ROOT_SIGNATURE_FLAG_NONE;
rootSignatureDesc.Desc_1_1.NumParameters = _countof(rootParameters);
rootSignatureDesc.Desc_1_1.pParameters = rootParameters;
rootSignatureDesc.Desc_1_1.NumStaticSamplers = 0;
rootSignatureDesc.Desc_1_1.pStaticSamplers = nullptr;
ComPtr<ID3DBlob> signature;
ComPtr<ID3DBlob> error;
HRESULT hr = D3D12SerializeVersionedRootSignature(
&rootSignatureDesc, signature.GetAddressOf(), error.GetAddressOf());
if (error) OutputDebugStringA(static_cast<const char*>(error->GetBufferPointer()));
ThrowIfFailed(hr);
ThrowIfFailed(device->CreateRootSignature(0, signature->GetBufferPointer(),
signature->GetBufferSize(), IID_PPV_ARGS(&globalRootSignature)));
// Root Signature 1.1 지원을 전제합니다. 미지원이면 1.0 구조/직렬화를 사용합니다.
C++
복사
첫 root parameter는 heap 시작을 받는 table입니다. UAV 범위가 offset 0, CBV 범위는 APPEND이므로 이어지는 offset 1을 읽습니다. 두 번째 root parameter는 t0 TLAS의 GPU 주소를 직접 받는 root SRV입니다. TLAS 주소는 이 table의 세 번째 descriptor로 넣는 것이 아닙니다.
카메라 상수에는 inverseViewProjection, cameraOrigin, maxRayDistance를 HLSL과 같은 순서·패킹으로 쓰고 CBV의 256바이트 정렬도 맞춥니다. Root Signature가 선언되었다고 이 값이 자동 계산·업로드되지는 않습니다. Root Signature 1.1 지원을 전제한 코드이므로 미지원 환경을 다루려면 버전 검사와 1.0 직렬화 경로도 준비합니다.
로컬 루트 시그니처는 레이트레이싱의 각 프로그래밍 가능한 단계가 자체적으로 정의할 수 있다는 점에서 특별합니다.
// 👋 Declare Handles
ID3D12RootSignature* localRootSignature = nullptr;
// Local Root Signature
// Shader tables are able to configure this root signature.
D3D12_ROOT_PARAMETER1 rootParameters[2] = {};
// Viewport Misc Constant
rootParameters[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
rootParameters[0].ParameterType =
D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS;
rootParameters[0].Constants = {};
rootParameters[0].Constants.ShaderRegister = 1;
rootParameters[0].Constants.Num32BitValues =
4;
// Texture Param SRV Table
D3D12_DESCRIPTOR_RANGE1 ranges[1] = {};
ranges[0].BaseShaderRegister = 1;
ranges[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
ranges[0].NumDescriptors = 1;
ranges[0].RegisterSpace = 0;
ranges[0].OffsetInDescriptorsFromTableStart = 2;
ranges[0].Flags = D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC;
rootParameters[1].ParameterType =
D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
rootParameters[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
rootParameters[1].DescriptorTable.pDescriptorRanges = &ranges[0];
rootParameters[1].DescriptorTable.NumDescriptorRanges =
_countof(ranges);
D3D12_STATIC_SAMPLER_DESC sampler = {};
sampler.Filter = D3D12_FILTER_MIN_MAG_MIP_POINT;
sampler.AddressU = D3D12_TEXTURE_ADDRESS_MODE_BORDER;
sampler.AddressV = D3D12_TEXTURE_ADDRESS_MODE_BORDER;
sampler.AddressW = D3D12_TEXTURE_ADDRESS_MODE_BORDER;
sampler.MipLODBias = 0;
sampler.MaxAnisotropy = 1;
sampler.ComparisonFunc = D3D12_COMPARISON_FUNC_NEVER;
sampler.BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK;
sampler.MinLOD = 0.0f;
sampler.MaxLOD = D3D12_FLOAT32_MAX;
sampler.ShaderRegister = 0;
sampler.RegisterSpace = 0;
sampler.ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
D3D12_VERSIONED_ROOT_SIGNATURE_DESC rootSignatureDesc;
rootSignatureDesc.Version = D3D_ROOT_SIGNATURE_VERSION_1_1;
rootSignatureDesc.Desc_1_1.Flags =
D3D12_ROOT_SIGNATURE_FLAG_LOCAL_ROOT_SIGNATURE;
rootSignatureDesc.Desc_1_1.NumParameters = _countof(rootParameters);
rootSignatureDesc.Desc_1_1.pParameters = rootParameters;
rootSignatureDesc.Desc_1_1.NumStaticSamplers = 1;
rootSignatureDesc.Desc_1_1.pStaticSamplers = &sampler;
ComPtr<ID3DBlob> signature;
ComPtr<ID3DBlob> error;
HRESULT hr = D3D12SerializeVersionedRootSignature(
&rootSignatureDesc, signature.GetAddressOf(), error.GetAddressOf());
if (error) OutputDebugStringA(static_cast<const char*>(error->GetBufferPointer()));
ThrowIfFailed(hr);
ThrowIfFailed(device->CreateRootSignature(0, signature->GetBufferPointer(),
signature->GetBufferSize(), IID_PPV_ARGS(&localRootSignature)));
// Root Signature 1.1 지원을 전제합니다. 미지원이면 1.0 구조/직렬화를 사용합니다.
C++
복사
Local root의 첫 인자는 b1에 보낼 32비트 값 네 개입니다. time과 패딩을 Shader Record 안에 값으로 넣습니다. 두 번째 인자는 descriptor table 시작 GPU handle이며 이 예제는 heap의 시작 handle을 전달하고 range offset 2로 t1을 찾습니다. 이미 세 번째 슬롯으로 이동한 handle에 다시 offset 2를 적용하면 잘못된 슬롯을 읽게 됩니다.
이 local 구성은 State Object에서 MyHitGroup에만 연결합니다. RayGen과 Miss는 이 인자를 사용하지 않아 identifier만 있는 record로 충분합니다. local은 “각 셰이더에 반드시 필요하다”는 의미가 아니라, record별로 달라질 데이터를 붙일 수 있다는 의미입니다.
Shader Table
Shader Table의 시작 GPU 주소는 64바이트, record stride는 32바이트 배수로 맞춥니다. 64비트 정렬이 아닙니다. Record는 32바이트 shader identifier 뒤에 local root 인자를 순서대로 배치합니다. Root constants는 지정한 값 자체, root descriptor는 GPU 주소, descriptor table은 GPU descriptor handle을 담습니다. DXR 명세: Shader Tables
Record는 어떤 셰이더를 실행할지와 그 인자를 나란히 담습니다
State Object가 제공하는 shader identifier 32바이트 뒤에 local root 인자를 선언 순서대로 붙입니다. 이번 HitRecord는 identifier 32 + 상수 16 + table handle 8 = 56바이트의 의미 있는 데이터를 갖고, record 정렬 단위 32의 다음 배수인 64바이트를 사용합니다.
// Local Root: b1 root constants 4개(16 bytes) → t1 table handle(8 bytes).
// shared heap의 [0]=u0, [1]=b0, [2]=t1이며 local table offset=2입니다.
struct alignas(D3D12_RAYTRACING_SHADER_RECORD_BYTE_ALIGNMENT) HitRecord
{
unsigned char identifier[D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES]; // 32
float localConstants[4]; // time + padding
D3D12_GPU_DESCRIPTOR_HANDLE heapBase; // table의 시작; offset 2 적용
};
static_assert(sizeof(HitRecord) == 64);
// GetShaderIdentifier(L"MyHitGroup")의 32바이트를 identifier에 복사합니다.
// RayGen/Miss에는 local root를 연결하지 않아 identifier만 담은 record를 사용합니다.
// 각 table은 시작 GPU 주소를 64바이트 정렬해 할당하고 올바른 상태로 유지합니다.
C++
복사
sizeof가 64가 되었다고 GPU table의 시작 주소까지 자동으로 맞는 것은 아닙니다. 각 table의 GPU 시작 주소는 별도로 64바이트 정렬해야 합니다. record stride의 32바이트 배수 조건과 table 시작의 64바이트 조건을 구분합니다. identifier는 C++ 함수 포인터나 HLSL 소스 주소가 아니라 해당 State Object에서 GetShaderIdentifier로 얻은 바이트입니다.
RayGen과 Miss는 각각 32바이트 identifier만 기록하고, HitGroup은 64바이트 record에 time과 heap 시작 handle을 채웁니다. padding까지 0으로 초기화한 메모리를 사용하고, GPU가 읽는 동안 table·descriptor·참조 리소스가 유지되도록 합니다.
Pipeline State
레이 트레이싱 파이프라인은 주어진 씬에서 객체를 찾기 위해 가속 데이터 구조를 탐색하는 하드웨어 가속 방식입니다. DirectX 레이 트레이싱은 래스터나 컴퓨트 파이프라인과 같은 엄격한 레이아웃 대신, 필요한 모든 정보를 가진 서브 오브젝트를 정의하는 좀 더 선언적인 파이프라인 상태 설명 레이아웃을 채택합니다.
레이 트레이싱 파이프라인을 생성하기 위해 최소한 다음 항목들이 필요합니다:
•
레이 디스패치 시 사용할 컴파일된 쉐이더가 포함된 DXIL 쉐이더 라이브러리•
교차점/미스에 대한 히트 페이로드 크기를 정의하는 쉐이더 구성•
레이 탐색을 위한 재귀 깊이 수준을 정의하는 파이프라인 구성•
히트 정보를 저장하는 삼각형 히트 그룹•
선택적 Local Root Signature와 필요한 export/hit group association
•
RWTexture<float4> radiance 버퍼와 같은 전역 리소스를 위한 글로벌 루트 시그니처셰이더 이름과 HitGroup의 관계를 State Object에 등록합니다
라이브러리에 raygen·closesthit·miss 함수가 들어 있어도 DXR이 어떤 조합을 사용할지 연결해야 합니다. MyHitGroup은 삼각형 교차 이후 closesthit를 실행하는 그룹의 이름입니다. Shader Table은 이 그룹 identifier를 사용합니다.
// 👋 Declare handles
ID3D12StateObject* pipelineState = nullptr;
ID3D12StateObjectProperties* stateObjectProperties = nullptr;
// 🗺️ Create the State Object Description
D3D12_STATE_OBJECT_DESC stateObjectDesc = {};
stateObjectDesc.Type = D3D12_STATE_OBJECT_TYPE_RAYTRACING_PIPELINE;
D3D12_STATE_SUBOBJECT subObjects[7];
stateObjectDesc.NumSubobjects = _countof(subObjects);
stateObjectDesc.pSubobjects = subObjects;
D3D12_EXPORT_DESC* exportDesc = nullptr;
D3D12_SHADER_BYTECODE rtShaderByteCode = {rsBytecodeData.data(),
rsBytecodeData.size()};
// 📚 Shader Library
D3D12_DXIL_LIBRARY_DESC dxilLibraryDesc = {rtShaderByteCode, 0u,
exportDesc};
subObjects[0u] = {D3D12_STATE_SUBOBJECT_TYPE_DXIL_LIBRARY,
&dxilLibraryDesc};
// 📕 Active Shaders
static LPCWSTR shaderNames[] = { L"MyHitGroup" };
D3D12_SUBOBJECT_TO_EXPORTS_ASSOCIATION exportAssociations = {
subObjects + 4, 1u, shaderNames};
subObjects[1] = {
D3D12_STATE_SUBOBJECT_TYPE_SUBOBJECT_TO_EXPORTS_ASSOCIATION,
&exportAssociations};
// 💰 Payload Setup
D3D12_RAYTRACING_SHADER_CONFIG shaderConfig = {};
shaderConfig.MaxPayloadSizeInBytes = 4 * sizeof(float); // float4 color
shaderConfig.MaxAttributeSizeInBytes =
2 * sizeof(float); // float2 barycentrics
subObjects[2] = {D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_SHADER_CONFIG,
&shaderConfig};
// 📑 Hit Groups
D3D12_HIT_GROUP_DESC myClosestHitGroup = {
L"MyHitGroup", D3D12_HIT_GROUP_TYPE_TRIANGLES, nullptr,
L"closesthit", nullptr};
subObjects[3] = {D3D12_STATE_SUBOBJECT_TYPE_HIT_GROUP,
&myClosestHitGroup};
// 🚃 Root Signatures
subObjects[4] = {D3D12_STATE_SUBOBJECT_TYPE_LOCAL_ROOT_SIGNATURE,
&localRootSignature};
subObjects[5] = {D3D12_STATE_SUBOBJECT_TYPE_GLOBAL_ROOT_SIGNATURE,
&globalRootSignature};
// 🔁 Recursion Depth
D3D12_RAYTRACING_PIPELINE_CONFIG pipelineConfig = {1u};
pipelineConfig.MaxTraceRecursionDepth = 1;
subObjects[6] = {D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_PIPELINE_CONFIG,
&pipelineConfig};
ThrowIfFailed(device->CreateStateObject(&stateObjectDesc, IID_PPV_ARGS(&pipelineState)));
ThrowIfFailed(pipelineState->QueryInterface(IID_PPV_ARGS(&stateObjectProperties)));
C++
복사
shaderConfig의 payload 크기 16바이트는 앞 RayPayload의 float4와 일치하고, 삼각형 attribute 8바이트는 barycentrics의 float2입니다. MaxTraceRecursionDepth=1은 이 예제의 RayGen→TraceRay 한 단계에 맞춥니다. ClosestHit에서 추가 광선을 재귀적으로 보내는 경로를 넣는다면 그 제한과 스택·비용도 다시 설계해야 합니다.
subObjects[4]의 Local Root Signature를 MyHitGroup에 association하는 부분이 중요합니다. 이 연결이 맞아야 HitRecord의 뒤쪽 바이트가 b1과 t1 table 인자로 해석됩니다. State Object와 shader identifier를 만든 뒤 Shader Table 메모리를 채우는 실제 생성 순서를 지킵니다. 글은 개념 설명을 위해 Record 구조를 먼저 소개한 것입니다.
Acceleration Structures
가속화 데이터 구조(또는 가속화 구조/AS)는 주어진 씬의 순회를 가속화하며, 삼각형이나 축 정렬 경계 상자(AABB)의 집합을 순회하기 위해 하드웨어 벤더가 사용하는 내부 가속화 데이터 구조를 나타냅니다.
Scratch는 BLAS/TLAS 빌드에 사용하는 임시 작업 메모리입니다. 결과 AS 버퍼와 다르며 UNORDERED_ACCESS 상태로 사용합니다. 크기는 PrebuildInfo로 구하고, 재사용 전에 이전 빌드의 사용 완료·순서를 보장합니다.
BLAS에는 모양을, TLAS에는 배치된 인스턴스를 넣습니다
같은 의자 메시를 방에 열 개 놓는다고 생각해 봅시다. BLAS는 의자 메시의 삼각형들을 찾기 위한 구조이고 TLAS는 그 BLAS를 참조하는 열 개 인스턴스의 배치를 표현할 수 있습니다. 아래는 우선 삼각형 geometry 하나와 단위 변환 인스턴스 하나만 사용하는 예제입니다.
// API 구조 발췌: device=ID3D12Device5*, commandList=ID3D12GraphicsCommandList4*.
// 업로드 완료된 VB/IB, R16_UINT 인덱스, 정점 맨 앞 float3 위치를 전제합니다.
D3D12_RAYTRACING_GEOMETRY_DESC geometry = {};
geometry.Type = D3D12_RAYTRACING_GEOMETRY_TYPE_TRIANGLES;
geometry.Flags = D3D12_RAYTRACING_GEOMETRY_FLAG_OPAQUE;
geometry.Triangles.VertexBuffer = {vertexBuffer->GetGPUVirtualAddress(), sizeof(Vertex)};
geometry.Triangles.VertexCount = vertexCount;
geometry.Triangles.VertexFormat = DXGI_FORMAT_R32G32B32_FLOAT;
geometry.Triangles.IndexBuffer = indexBuffer->GetGPUVirtualAddress();
geometry.Triangles.IndexCount = indexCount;
geometry.Triangles.IndexFormat = DXGI_FORMAT_R16_UINT;
D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS blasInputs = {};
blasInputs.Type = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL;
blasInputs.DescsLayout = D3D12_ELEMENTS_LAYOUT_ARRAY;
blasInputs.NumDescs = 1;
blasInputs.pGeometryDescs = &geometry;
blasInputs.Flags = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PREFER_FAST_TRACE;
D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS tlasInputs = {};
tlasInputs.Type = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL;
tlasInputs.DescsLayout = D3D12_ELEMENTS_LAYOUT_ARRAY;
tlasInputs.NumDescs = 1;
tlasInputs.Flags = blasInputs.Flags;
D3D12_RAYTRACING_ACCELERATION_STRUCTURE_PREBUILD_INFO blasInfo = {}, tlasInfo = {};
device->GetRaytracingAccelerationStructurePrebuildInfo(&blasInputs, &blasInfo);
device->GetRaytracingAccelerationStructurePrebuildInfo(&tlasInputs, &tlasInfo);
// 이 지점에서 result/scratch 버퍼를 확보합니다. 아래 설명의 크기·상태를 맞춥니다.
D3D12_RAYTRACING_INSTANCE_DESC instance = {};
instance.Transform[0][0] = instance.Transform[1][1] = instance.Transform[2][2] = 1.0f;
instance.InstanceMask = 0xff;
instance.AccelerationStructure = blasBuffer->GetGPUVirtualAddress();
// instance를 UPLOAD 버퍼에 복사하고 빌드 완료까지 유지합니다.
tlasInputs.InstanceDescs = instanceBuffer->GetGPUVirtualAddress();
D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC blasBuild = {}, tlasBuild = {};
blasBuild.Inputs = blasInputs;
blasBuild.DestAccelerationStructureData = blasBuffer->GetGPUVirtualAddress();
blasBuild.ScratchAccelerationStructureData = blasScratch->GetGPUVirtualAddress();
tlasBuild.Inputs = tlasInputs;
tlasBuild.DestAccelerationStructureData = tlasBuffer->GetGPUVirtualAddress();
tlasBuild.ScratchAccelerationStructureData = tlasScratch->GetGPUVirtualAddress();
commandList->BuildRaytracingAccelerationStructure(&blasBuild, 0, nullptr);
auto barrier = CD3DX12_RESOURCE_BARRIER::UAV(blasBuffer);
commandList->ResourceBarrier(1, &barrier);
commandList->BuildRaytracingAccelerationStructure(&tlasBuild, 0, nullptr);
barrier = CD3DX12_RESOURCE_BARRIER::UAV(tlasBuffer);
commandList->ResourceBarrier(1, &barrier);
// BLAS/TLAS scratch를 따로 준비한 예입니다. 공유하면 재사용 Barrier도 필요합니다.
C++
복사
PrebuildInfo는 결과 AS와 scratch가 각각 얼마나 큰지 계산합니다. result는 이후 TraceRay가 탐색할 결과이고 scratch는 빌드 동안만 쓰는 작업 공간입니다. 같은 버퍼라고 생각해 서로 덮어쓰면 안 됩니다. 아래 연결 조건에 맞게 각각 할당한 뒤 Build 명령을 기록합니다.
BLAS Build 다음의 UAV Barrier는 TLAS Build가 BLAS 결과를 사용할 수 있도록 순서를 연결합니다. TLAS Build 뒤에도 TraceRay 사용 전에 순서를 보장합니다. 이는 CPU가 빌드 완료를 기다리는 Fence와 다릅니다. 같은 Queue에 올바른 순서로 기록하고 CPU 메모리 재사용 시점에는 별도 완료 조건을 확인합니다.
여기서는 R16_UINT 인덱스를 전제하므로 실제 인덱스 배열도 uint16_t여야 합니다. 앞 Raster 강의의 uint32_t 버퍼를 그대로 가져오면 geometry의 IndexFormat도 R32_UINT로 바꿔야 합니다. InstanceMask=0xff는 앞 TraceRay의 마스크와 겹쳐 이 인스턴스가 탐색 대상이 되게 합니다.
Commands
레이트레이싱 파이프라인은 컴퓨트 파이프라인과 유사하지만, 전통적인 컴퓨트 dispatch에서처럼 그룹 단위가 아닌 주어진 커널 크기에 대해 dispatchRays를 수행합니다. 따라서 예를 들어 레디언스 버퍼의 각 픽셀에 대한 광선 집합으로 더 쉽게 이해할 수 있습니다.
DispatchRays는 최종 출력 크기로 실행합니다
앞 Compute의 Dispatch(그룹 수)와 달리 이 Width·Height는 실행할 RayGeneration의 크기입니다. 800×600을 넣으면 이 예제는 출력 위치마다 광선 하나를 만들므로 480,000개의 초기 광선을 보냅니다. 반사 같은 추가 광선은 셰이더에서 TraceRay를 더 호출해야 생깁니다.
// 한 개 RayGen, 한 개 Miss, 한 개 HitGroup record를 담은 예제입니다.
// 각 buffer는 사용한 record 크기만 따로 관리하며 전체 할당 크기로 stride를 정하지 않습니다.
commandList->SetComputeRootSignature(globalRootSignature);
// 🌎 Bind global root signature descriptor data
commandList->SetDescriptorHeaps(1, &srvHeap);
commandList->SetComputeRootDescriptorTable(0, srvHeap->GetGPUDescriptorHandleForHeapStart());
// 🚞 Bind TLAS
commandList->SetComputeRootShaderResourceView(
1, tlasBuffer->GetGPUVirtualAddress());
// ⚡ Setup dispatch description
D3D12_DISPATCH_RAYS_DESC dispatchDesc = {};
dispatchDesc.Width = width;
dispatchDesc.Height = height;
dispatchDesc.Depth = 1;
// 🏓 Shader Binding Tables
dispatchDesc.RayGenerationShaderRecord.StartAddress =
rayGenShaderTable->GetGPUVirtualAddress();
dispatchDesc.RayGenerationShaderRecord.SizeInBytes =
D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES;
dispatchDesc.MissShaderTable.StartAddress =
missShaderTable->GetGPUVirtualAddress();
dispatchDesc.MissShaderTable.SizeInBytes =
D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES;
dispatchDesc.MissShaderTable.StrideInBytes =
dispatchDesc.MissShaderTable.SizeInBytes;
dispatchDesc.HitGroupTable.StartAddress =
hitShaderTable->GetGPUVirtualAddress();
dispatchDesc.HitGroupTable.SizeInBytes = sizeof(HitRecord);
dispatchDesc.HitGroupTable.StrideInBytes =
dispatchDesc.HitGroupTable.SizeInBytes;
// 👨🔧 Setup Pipeline
commandList->SetPipelineState1(pipelineState);
// 🏁 Dispatch Rays
commandList->DispatchRays(&dispatchDesc);
C++
복사
SetComputeRootSignature를 사용하는 것은 DXR이 Compute의 root 바인딩 방식을 사용하기 때문입니다. PSO 선택은 일반 Compute의 SetPipelineState 대신 State Object용 SetPipelineState1을 사용합니다. descriptor table 시작과 TLAS 주소, RayGen/Miss/HitGroup table 주소를 각각 연결한 뒤 DispatchRays를 기록합니다.
실제 화면 표시에는 출력 UAV를 다음 읽기·복사 상태로 전환하고 백버퍼나 ImGui에 연결하는 패스가 더 필요합니다. DispatchRays를 호출한 순간 CPU 이미지가 생기거나 Present가 자동으로 실행되지는 않습니다.
참고자료
회색 배경에서 시작해 한 연결씩 확인합니다
먼저 광선이 아무 도형에도 닿지 않을 때 Miss의 회색이 나오도록 출력·표시 경로를 확인합니다. 이후 삼각형 BLAS와 TLAS 인스턴스를 연결하고 교차한 영역만 ClosestHit 색이 나오는지 봅니다. 이때 화면 전체가 회색이면 카메라 방향뿐 아니라 instance mask, 입력 버퍼 형식, TLAS 주소와 Build 순서를 확인합니다. 교차는 되는데 재질이 잘못되면 local record의 인자 배치와 descriptor offset을 따로 봅니다.
이렇게 광선 생성, 교차 탐색, 재질 선택, 결과 표시를 나누면 긴 API 목록보다 실패 지점을 이해하기 쉽습니다. 다음은 발췌 코드를 실행 프로그램에 연결할 때 빠짐없이 준비해야 할 메모리 조건입니다.
메모리와 실행 연결 조건
DXR Tier는 D3D12_FEATURE_D3D12_OPTIONS5로 확인합니다. BLAS/TLAS 결과는 PrebuildInfo.ResultDataMaxSizeInBytes 이상의 DEFAULT 버퍼, ALLOW_UNORDERED_ACCESS flag, RAYTRACING_ACCELERATION_STRUCTURE 상태로 만듭니다. Scratch는 ScratchDataSizeInBytes 이상의 DEFAULT UAV 버퍼이며 UNORDERED_ACCESS 상태입니다. 주소 정렬과 0 크기 실패를 확인하고 입력 VB/IB·instance 데이터는 빌드에서 읽을 수 있는 상태로 유지합니다. Shader Table 메모리 할당·identifier 복사, CPU 카메라 상수와 local 인자 채우기, UAV 결과 표시, 제출·Fence는 위 발췌 밖의 필수 단계입니다. 엔진에 완성된 기능으로 표시하지 않습니다. Microsoft DXR 명세












