Introduction
En 2026, HLSL (High-Level Shading Language) reste le pilier des pipelines graphiques DirectX 12, powering les AAA comme ceux d'Unreal Engine 6 ou custom engines. Contrairement à GLSL (OpenGL/Vulkan), HLSL excelle dans les optimisations hardware NVIDIA/AMD récentes, comme Mesh Shaders et Variable Rate Shading (VRS). Ce tutoriel expert vous guide de la structure basique à des techniques avancées : PBR lighting, compute shaders pour simulations physiques, et ray tracing DXR avec amplification shaders. Pourquoi c'est crucial ? Les shaders HLSL réduisent le CPU bottleneck de 40-60% via wave intrinsics et async compute. Imaginez un mentor à vos côtés : on commence par un vertex shader simple, puis on escalade vers du ray marching volumétrique. À la fin, vous compilerez des shaders DXC-ready pour booster vos FPS en 4K RT. (148 mots)
Prérequis
- Visual Studio 2022+ avec DirectX 12 SDK
- Connaissances expertes en C++ et DirectX 12 pipelines
- DXC compiler (fxc/dxc.exe) installé via NuGet
- GPU DX12 Ultimate (RTX 30/40 series recommandé)
- Outils : PIX pour debug, RenderDoc pour capture
Shader vertex basique avec transformation
#include "Common.hlsl"
cbuffer PerObjectCB : register(b0) {
float4x4 gWorldViewProj;
};
struct VSInput {
float3 Pos : POSITION;
float3 Normal : NORMAL;
float2 TexC : TEXCOORD;
};
struct PSInput {
float4 PosH : SV_POSITION;
float3 Normal : NORMAL;
float2 TexC : TEXCOORD;
};
PSInput VSMain(VSInput vin) {
PSInput pout;
pout.PosH = mul(float4(vin.Pos, 1.0f), gWorldViewProj);
pout.Normal = vin.Normal;
pout.TexC = vin.TexC;
return pout;
}Ce vertex shader transforme les positions en espace écran via une matrice WorldViewProj en constant buffer (b0). Il passe les normales et UVs au pixel shader. Piège : Oublier SV_POSITION cause des rendus noirs ; utilisez toujours mul() pour les matrices row-major HLSL.
Comprendre les Semantic et Registers
Les semantics comme SV_POSITION lient les sorties aux inputs du stage suivant, essentiels pour le rasterizer. Les registers (b0 pour buffers, t0 pour textures) évitent les bank conflicts. Analogy : comme des slots PCIe, mal alloués = latence x2. Compilez avec dxc -T vs_6_0 -E VSMain basic_vertex.hlsl -Fo vs.cso.
Pixel shader PBR simplifié
#include "Common.hlsl"
Texture2D gAlbedo : register(t0);
Texture2D gNormal : register(t1);
Texture2D gMetallic : register(t2);
SamplerState gsamLinearWrap : register(s0);
cbuffer PerFrameCB : register(b1) {
float3 gEyePosW;
float3 gLightDir;
float3 gLightColor;
};
struct PSInput {
float4 PosH : SV_POSITION;
float3 Normal : NORMAL;
float2 TexC : TEXCOORD;
float3 PosW : POSITION;
};
float4 PSMain(PSInput pin) : SV_TARGET {
float3 normal = normalize(pin.Normal);
float3 albedo = gAlbedo.Sample(gsamLinearWrap, pin.TexC).rgb;
float metallic = gMetallic.Sample(gsamLinearWrap, pin.TexC).r;
float3 viewDir = normalize(gEyePosW - pin.PosW);
float3 lightDir = -normalize(gLightDir);
float NdotL = max(dot(normal, lightDir), 0.0f);
float3 color = albedo * gLightColor * NdotL;
return float4(color, 1.0f);
}Ce pixel shader sample trois textures (albedo, normal, metallic) pour un lighting Blinn-Phong basique vers PBR. Il calcule NdotL pour la diffusion. Attention : Sample() sans sampler cause des artefacts ; toujours déclarez SamplerState explicitement.
Implémenter le Texturing et Lighting
Les textures se lient via tN/sN registers, avec bilinear filtering par défaut. Pour PBR complet, ajoutez roughness et compute Fresnel. Exemple concret : Sur un sphere mesh, cela donne un rendu métallique réaliste sous une light directionnelle.
Compute shader pour simulation de particules
#include "Common.hlsl"
RWStructuredBuffer<float3> gPositions : register(u0);
RWStructuredBuffer<float3> gVelocities : register(u1);
StructuredBuffer<float3> gTargets : register(t3);
cbuffer SimCB : register(b2) {
float DeltaTime;
float Gravity;
uint NumParticles;
};
[numthreads(64, 1, 1)]
void CSMain(uint3 DTid : SV_DispatchThreadID) {
uint idx = DTid.x;
if (idx >= NumParticles) return;
float3 pos = gPositions[idx];
float3 vel = gVelocities[idx];
float3 target = gTargets[idx];
vel += float3(0, Gravity * DeltaTime, 0);
vel += (target - pos) * DeltaTime * 0.1f;
vel *= 0.99f; // damping
pos += vel * DeltaTime;
gPositions[idx] = pos;
gVelocities[idx] = vel;
}Ce compute shader simule 100k+ particules avec gravité et attraction vers targets, en [numthreads(64,1,1)] pour warp efficiency. Écriture en RWStructuredBuffer (u0/u1). Piège majeur : Oublier bounds check (idx >= NumParticles) crash le GPU.
Harnessing Compute Shaders pour Simulations
Les compute shaders parallélisent les tâches non-graphiques comme la physique. Dispatch(NumParticles/64,1,1). Analogy : Un millier de cœurs GPU calculant indépendamment, comme une usine automatisée.
Amplification shader pour Ray Tracing DXR
#include "Common.hlsl"
RaytracingAccelerationStructure gScene : register(t0);
RWGeometryIndex gOutIndices : register(u0);
cbuffer AmpCB : register(b3) {
float3 gEye;
uint MaxPrims;
};
[shader("amplification")]
void AmpMain(
uint groupIndex : SV_GroupIndex,
uint triangleCount : SV_TriangleCount,
out uint outPrimCount : SV_OutputPrimitiveCount) {
outPrimCount = min(triangleCount * 2, MaxPrims);
for (uint i = groupIndex; i < triangleCount * 2; i += 64) {
gOutIndices[i] = i / 2;
}
}Shader DXR amplification pour densifier les primitives (x2 ici). Utilise SV_GroupIndex pour thread safety. Compilez avec -T lib_6_6 -enable-16bit-types. Erreur commune : Ignorer SV_OutputPrimitiveCount bloque le raygen.
Intégrer Ray Tracing avec DXR
DXR (DirectX Raytracing) en HLSL 6_6+ permet closest hit, any hit shaders. L'amplification cull les primitives invisibles, boostant perf de 30%. Liez à un raygen shader pour shadows réalistes.
Optimisations avec Wave Intrinsics
#include "Common.hlsl"
groupshared float3 gCache[64];
groupshared uint gVoteActive;
[numthreads(64,1,1)]
void CSMain(uint3 DTid : SV_DispatchThreadID, uint3 GTid : SV_GroupThreadID,
uint3 Gid : SV_GroupID) {
if (WaveIsFirstLane()) {
gVoteActive = WaveActiveAllTrue(true);
}
GroupMemoryBarrierWithGroupSync();
float3 sharedData = gCache[GTid.x];
uint ballot = WaveActiveBallot(true);
uint popcnt = WaveActiveCountBits(ballot);
float avg = WaveReadLaneAt(sharedData.x, WaveGetFirstLane());
GroupMemoryBarrierWithGroupSync();
}Utilise WaveIsFirstLane(), WaveActiveBallot() pour synchroniser 32/64 lanes sans barrières coûteuses. Idéal pour reductions (avg ici). Piège : Sur AMD (wave32), adaptez avec WaveGetLaneCount() pour cross-vendor.
Bonnes pratiques
- Profilez toujours avec PIX : Visez <1ms par dispatch.
- Utilisez half/float16 pour bandwidth (-enable-16bit-types).
- Packez les CBVs : Alignez à 16 bytes, min 16 slots.
- Testez cross-GPU : NVIDIA wave64 vs AMD wave32.
- Versionnez : Tls_6_7+ pour VRS et Mesh Shaders.
Erreurs courantes à éviter
- Register overflow : t0-t15 max par stage ; spill = perf -50%.
- No barriers in CS : Race conditions corrompent les RW buffers.
- SV_ semantics manquants : Shaders ne bindent pas au PSO.
- Async compute sans fences : Glitchs graphiques sur multi-queue.
Pour aller plus loin
Maîtrisez Mesh Shaders (Tms_6_0) pour LOD dynamiques. Ressources : Docs MS HLSL, NVIDIA HLSL Best Practices. Formations expertes : Learni Graphisme 3D. Compilez tout avec DXC 1.7+ pour Wave64 support.