Skip to content
Learni
Voir tous les tutoriels
Graphisme 3D

Comment maîtriser les shaders HLSL avancés en 2026

Read in English

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

basic_vertex.hlsl
#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é

pbr_pixel.hlsl
#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

particles_compute.hlsl
#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

ray_amplification.hlsl
#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

wave_optimized.hlsl
#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.

Maîtriser HLSL shaders avancés 2026 (55 chars) | Learni