Fondements du Raytracing
Contrairement à la rastérisation qui projette la géométrie vers les pixels de l'écran, le raytracing opère selon une logique inversée et plus physique. Le processus se décompose ainsi :
- Émission d'un rayon depuis la caméra à travers chaque pixel de la cible de rendu.
- Parcours de la scène pour tester l'intersection entre le rayon et les maillages.
- Si une intersection est trouvée et valide, mise à jour de la distance maximale (TMax) et des données associées (payload).
- Évaluation au point d'impact pour déterminer si des rayons secondaires (réflexion, réfraction) sont nécessaires.
- Calcul de la couleur finale du pixel à partir du payload retourné.
On peut résumer la différence algorithmique entre les deux approches par une inversion des boucles :
// Rastérisation
for each Triangle in Scene:
for each Pixel on Screen:
evaluate coverage
// Raytracing
for each Pixel on Screen:
for each Triangle in Scene:
evaluate intersection
Architecture du Pipeline DXR
Le pipeline de raytracing sous DirectX 12 s'articule autour de cinq étapes de shaders spécifiques. Le flux général est le suivant :
- Ray Genertaion Shader : Point d'entrée. Il construit les rayons initiaux et invoque la fnoction intrinsèque
TraceRay. Le résultat est écrit dans une texture UAV. - Traversée de l'Accélération : Le runtime parcourt la structure d'accélération (BVH). Si le nœud est un triangle, un test d'intersection intégré est effectué. Sinon, un Intersection Shader est appelé pour les géométries personnalisées (AABB).
- Any Hit Shader : Si le matériau est transparent, ce shader est exécuté pour décider si l'intersection doit être gardée ou ignorée.
- Closest Hit Shader : Une fois la traversée terminée, si une intersection valide existe, ce shader traite l'impact le plus proche pour calculer l'ombrage et potentiellement lancer de nouveaux rayons.
- Miss Shader : Si aucun objet n'a été touché par le rayon, ce shader est exécuté (utile pour un ciel ou un arrière-plan).
TraceRay et Paramètres
La fonction TraceRay est le cœur de DXR. Sa signature est la suivante :
Template<payload_t>
void TraceRay(RaytracingAccelerationStructure AccelStruct,
uint RayFlags,
uint InstanceInclusionMask,
uint RayContributionToHitGroupIndex,
uint MultiplierForGeometryContributionToHitGroupIndex,
uint MissShaderIndex,
RayDesc Ray,
inout payload_t Payload);
Les paramètres critiques incluent :
AccelStruct: La structure d'accélération de haut niveau (TLAS).RayFlags: Modifie le comportement du rayon (ex: forcer l'opacité, éliminer les triangles arrière).InstanceInclusionMask: Masque de visibilité combiné avec celui de l'instance via un ET logique.RayContributionToHitGroupIndexetMultiplier...: Utilisés pour calculer l'index du Hit Group dans la Shader Table, permettant de différencier les types de rayons (ex: ombre vs. éclairage direct).Ray: Structure contenant l'origine, la direction, TMin et TMax.
Gestion des Shaders et Shader Table
Contrairement à la rastérisation où un Draw Call lie un Vertex/Fragment Shader à une géométrie, DXR utilise des Hit Groups. Un Hit Group regroupe l'Intersection Shader, l'Any Hit Shader et le Closest Hit Shader pour un type de géométrie donné.
Pour organiser ces shaders et leurs ressources, DXR introduit la Shader Table. C'est un tableau mémoire aligné sur 64 octets contenant des Shader Records. Chaque Shader Record associe un identifiant de shader (Shader Identifier) à des arguments racines locaux (Root Arguments). Il existe quatre types de tables : Ray Generation, Hit Group, Miss et Callable.
Indexation dans la Hit Group Table
L'indexation du Hit Group pour un objet dépend du nombre de rayons et de la géométrie. L'adresse du Shader Record est calculée via la formule officielle :
HitGroupRecordAddress =
BaseAddress +
Stride * (
RayContributionToHitGroupIndex +
(MultiplierForGeometryContributionToHitGroupIndex * GeometryIndex) +
InstanceContributionToHitGroupIndex
)
Création de la Shader Table
Pour notre triangle initial, nous avons besoin d'une table pour le Ray Generation (avec des constantes racines pour le viewport), une pour le Miss, et une pour le Hit Group.
ComPtr<ID3D12StateObjectProperties> pStateProps;
ThrowIfFailed(m_rtStateObject.As(&pStateProps));
void* pRayGenId = pStateProps->GetShaderIdentifier(L"RayGenEntry");
void* pMissId = pStateProps->GetShaderIdentifier(L"MissEntry");
void* pHitId = pStateProps->GetShaderIdentifier(L"TriangleHitGroup");
// Préparation des données de la table RayGen
std::vector<uint8_t> rayGenRecord(sizeof(RayGenConstants) + D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES);
memcpy(rayGenRecord.data(), pRayGenId, D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES);
memcpy(rayGenRecord.data() + D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES, &m_viewportData, sizeof(RayGenConstants));
m_rayGenTable.Create(L"RayGen_TBL", rayGenRecord.size(), rayGenRecord.size(), rayGenRecord.data());
m_missTable.Create(L"Miss_TBL", D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES, D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES, pMissId);
m_hitTable.Create(L"Hit_TBL", D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES, D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES, pHitId);
Liaison des Ressources (Signatures Racines)
DXR sépare les ressources en signatures racines Globales et Locales. La signature globale est partagée par tous les shaders du pipeline (comme l'output UAV ou la TLAS), tandis que la signature locale est spécifique à un Shader Record (permettant de changer les textures par instance sans modifier la signature globale entière). Une signature locale nécessite le flag D3D12_ROOT_SIGNATURE_FLAG_LOCAL_ROOT_SIGNATURE.
Règles importantes :
- Un pipeline ne peut avoir qu'une seule signature globale, mais plusieurs locales.
- Un shader ne peut être associé qu'à une seule signature locale.
// Signature Locale : Constantes pour la génération de rayons
m_localRootSig.InitAsConstants(0, sizeof(RayGenConstants) / 4);
m_localRootSig.Finalize(L"LocalRootSig", D3D12_ROOT_SIGNATURE_FLAG_LOCAL_ROOT_SIGNATURE);
// Signature Globale : Accélération et Cible de rendu
m_globalRootSig[0].InitAsDescriptorRange(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 0, 1);
m_globalRootSig[1].InitAsBufferSRV(0);
m_globalRootSig.Finalize(L"GlobalRootSig");
Structures d'Accélération (BVH)
DXR utilise une hiérarchie à deux niveaux :
- Bottom Level AS (BLAS) : Contient la géométrie réelle (triangles ou AABB). Le type
D3D12_RAYTRACING_GEOMETRY_DESCdéfinit si la géométrie est opaque ou non, et ses buffers associés. - Top Level AS (TLAS) : Contient des instances pointant vers des BLAS. La structure
D3D12_RAYTRACING_INSTANCE_DESCinclut une transformation monde, un masque, et le décalage dans la Hit Group Table.
Avant de construire une AS, il faut interroger les besoins en mémoire via GetRaytracingAccelerationStructurePrebuildInfo, puis allouer un buffer de destination et un buffer temporaire (scratch). La construction s'effectue via BuildRaytracingAccelerationStructure. Un UAV barrier est obligatoire entre la construction du BLAS et celle du TLAS.
// Description de la géométrie pour le BLAS
D3D12_RAYTRACING_GEOMETRY_TRIANGLES_DESC triDesc{};
triDesc.IndexFormat = DXGI_FORMAT_R32_UINT;
triDesc.VertexFormat = DXGI_FORMAT_R32G32B32_FLOAT;
triDesc.IndexCount = indexCount;
triDesc.VertexCount = vertexCount;
triDesc.IndexBuffer = indexBufferGpuAddr;
triDesc.VertexBuffer.StartAddress = vertexBufferGpuAddr;
triDesc.VertexBuffer.StrideInBytes = sizeof(Vertex);
D3D12_RAYTRACING_GEOMETRY_DESC geomDesc{};
geomDesc.Type = D3D12_RAYTRACING_GEOMETRY_TYPE_TRIANGLES;
geomDesc.Flags = D3D12_RAYTRACING_GEOMETRY_FLAG_OPAQUE;
geomDesc.Triangles = triDesc;
// Configuration BLAS
D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS blasInputs{};
blasInputs.Type = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL;
blasInputs.Flags = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PREFER_FAST_TRACE;
blasInputs.NumDescs = 1;
blasInputs.DescsLayout = D3D12_ELEMENTS_LAYOUT_ARRAY;
blasInputs.pGeometryDescs = &geomDesc;
// ... Allocation mémoire basée sur PrebuildInfo ...
// Configuration Instance pour le TLAS
D3D12_RAYTRACING_INSTANCE_DESC instDesc{};
instDesc.Transform[0][0] = instDesc.Transform[1][1] = instDesc.Transform[2][2] = 1.0f;
instDesc.InstanceMask = 1;
instDesc.AccelerationStructure = bottomLevelAsGpuAddr;
// Construction effective
cmdList->BuildRaytracingAccelerationStructure(&blasDesc, 0, nullptr);
InsertUAVBarrier(cmdList, bottomLevelAs);
cmdList->BuildRaytracingAccelerationStructure(&tlasDesc, 0, nullptr);
Objets d'État (State Objects)
L'équivalent du PSO pour DXR est le State Object. Il se construit à partir de sous-objets (D3D12_STATE_SUBOBJECT). Un pipeline de base nécessite 7 sous-objets :
- DXIL Libray : Le bytecode HLSL 6.3+ et les points d'exportation.
- Hit Group : Association des shaders d'intersection, any hit et closest hit.
- Shader Config : Taille maximale du Payload et des Attributs.
- Local Root Signature : La signature racine locale.
- Association : Lie la signature locale à un export de shader spécifique.
- Global Root Signature : La signature racine globale.
- Pipeline Config : Profondeur de récursion maximale (
MaxTraceRecursionDepth).
std::vector<D3D12_STATE_SUBOBJECT> subobjects(7);
// 1. Library
D3D12_DXIL_LIBRARY_DESC libDesc{};
libDesc.DXILLibrary = shaderBytecode;
// ... configuration des exports ...
// 2. Hit Group
D3D12_HIT_GROUP_DESC hitGrpDesc{};
hitGrpDesc.Type = D3D12_HIT_GROUP_TYPE_TRIANGLES;
hitGrpDesc.HitGroupExport = L"TriangleHitGroup";
hitGrpDesc.ClosestHitShaderImport = L"ClosestHitEntry";
// 3. Shader Config
D3D12_RAYTRACING_SHADER_CONFIG shaderCfg{};
shaderCfg.MaxPayloadSizeInBytes = sizeof(float) * 4;
shaderCfg.MaxAttributeSizeInBytes = sizeof(float) * 2;
// 4 & 5. Local Root Sig & Association
D3D12_LOCAL_ROOT_SIGNATURE localRootSig{};
localRootSig.pLocalRootSignature = m_localRootSig.GetRootSignature();
D3D12_SUBOBJECT_TO_EXPORTS_ASSOCIATION localAssoc{};
localAssoc.pSubobjectToAssociate = &subobjects[3]; // Index de la local root sig
localAssoc.pExports = &rayGenExportName;
// 6. Global Root Sig
D3D12_GLOBAL_ROOT_SIGNATURE globalRootSig{};
globalRootSig.pGlobalRootSignature = m_globalRootSig.GetRootSignature();
// 7. Pipeline Config
D3D12_RAYTRACING_PIPELINE_CONFIG pipeCfg{};
pipeCfg.MaxTraceRecursionDepth = 1;
// Remplissage du tableau et création
D3D12_STATE_OBJECT_DESC rtStateDesc{};
rtStateDesc.Type = D3D12_STATE_OBJECT_TYPE_RAYTRACING_PIPELINE;
rtStateDesc.NumSubobjects = subobjects.size();
rtStateDesc.pSubobjects = subobjects.data();
device->CreateStateObject(&rtStateDesc, IID_PPV_ARGS(&m_rtStateObject));
Exécution et Shaders HLSL
Pour lancer le calcul, on remplit la structure D3D12_DISPATCH_RAYS_DESC avec les adresses des Shader Tables et les dimensions de l'image, puis on appelle DispatchRays.
D3D12_DISPATCH_RAYS_DESC dispatchDesc{};
dispatchDesc.HitGroupTable.StartAddress = m_hitTable.GetGpuVirtualAddress();
dispatchDesc.HitGroupTable.SizeInBytes = m_hitTable.GetSize();
dispatchDesc.HitGroupTable.StrideInBytes = dispatchDesc.HitGroupTable.SizeInBytes;
dispatchDesc.MissShaderTable.StartAddress = m_missTable.GetGpuVirtualAddress();
dispatchDesc.MissShaderTable.SizeInBytes = m_missTable.GetSize();
dispatchDesc.MissShaderTable.StrideInBytes = dispatchDesc.MissShaderTable.SizeInBytes;
dispatchDesc.RayGenerationShaderRecord.StartAddress = m_rayGenTable.GetGpuVirtualAddress();
dispatchDesc.RayGenerationShaderRecord.SizeInBytes = m_rayGenTable.GetSize();
dispatchDesc.Width = screenWidth;
dispatchDesc.Height = screenHeight;
dispatchDesc.Depth = 1;
cmdList->SetPipelineState1(m_rtStateObject.Get());
cmdList->DispatchRays(&dispatchDesc);
Côté HLSL, les shaders pour notre premier triangle sont minimalistes. Le Ray Generation construit un rayon orthogonal, le Closest Hit affiche les coordonnées barycentriques, et le Miss renvoie du noir.
RaytracingAccelerationStructure accStructure : register(t0);
RWTexture2D<float4> outputTexture : register(u0);
ConstantBuffer<ViewportParams> cameraParams : register(b0);
struct TraceResult {
float4 finalColor;
};
struct ViewportParams {
float4 bounds; // left, top, right, bottom
};
[shader("raygeneration")]
void GenerateRays()
{
uint2 pixelCoords = DispatchRaysIndex().xy;
float2 normalizedCoords = (float2)pixelCoords / DispatchRaysDimensions().xy;
float3 rayDir = float3(0.0, 0.0, 1.0);
float3 rayOrigin = float3(
lerp(cameraParams.bounds.x, cameraParams.bounds.z, normalizedCoords.x),
lerp(cameraParams.bounds.y, cameraParams.bounds.w, normalizedCoords.y),
0.0f
);
RayDesc ray;
ray.Origin = rayOrigin;
ray.Direction = rayDir;
ray.TMin = 0.001;
ray.TMax = 10000.0;
TraceResult payload = { float4(0, 0, 0, 0) };
TraceRay(accStructure, RAY_FLAG_CULL_BACK_FACING_TRIANGLES, 0xFF, 0, 1, 0, ray, payload);
outputTexture[pixelCoords] = payload.finalColor;
}
[shader("closesthit")]
void OnClosestImpact(inout TraceResult payload, in BuiltInTriangleIntersectionAttributes attrs)
{
float3 barycentrics = float3(1.0 - attrs.barycentrics.x - attrs.barycentrics.y,
attrs.barycentrics.x,
attrs.barycentrics.y);
payload.finalColor = float4(barycentrics, 1.0);
}
[shader("miss")]
void OnRayMiss(inout TraceResult payload)
{
payload.finalColor = float4(0.0, 0.0, 0.0, 1.0);
}