bvle-voxels/TROUBLESHOOTING.md
Samuel Bouchet 626fbaea80 Fix smooth Surface Nets rendering: eliminate faceting, fix blocky junction
- Remove geoN (ddx/ddy) from smooth PS entirely — use smooth interpolated
  normal N for all triplanar sampling (albedo, heightmap, normal map).
  geoN changes discontinuously at triangle edges, causing per-triangle
  faceting in texture weights and normal perturbation.
- Tune consistency-based vertex normal blend to smoothstep(0.70, 0.90):
  snaps to face normal at 90° boundaries (seamless blocky join) while
  preserving smooth normals on curved terrain.
- Unify all 3 edge axes (X/Y/Z) to same smoothstep formula (was mixed
  smoothstep + pow4).
- Remove grass-specific hardcoded shading from both PS (side darkening,
  warm shift, ambient boost) — will be data-driven per-material later.
- Remove CPU SmoothMesher code (GPU-only path).
- Document all findings in TROUBLESHOOTING.md with calibration table.
2026-04-01 20:35:42 +02:00

392 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# BVLE Voxels — Troubleshooting & Pièges techniques
## Table des matières
- [APIs Wicked utilisées](#apis-wicked-utilisées)
- [Coordonnées logiques vs physiques](#coordonnées-logiques-vs-physiques--piège-majeur)
- [Triplanar UDN Normal Mapping](#triplanar-udn-normal-mapping--pièges-majeurs)
- [Shaders custom — Pièges importants](#shaders-custom--pièges-importants)
1. [Root signature obligatoire](#1-root-signature-obligatoire)
2. [Root signature Wicked (HLSL 6.6+)](#2-root-signature-wicked-hlsl-66)
3. [Chemins des shaders](#3-chemins-des-shaders)
4. [dxcompiler.dll manquant](#4-dxcompilerdll-manquant)
5. [CreateBuffer prend void*](#5-createbuffer-prend-void)
6. [Winding des triangles](#6-winding-des-triangles--piège-majeur)
7. [DrawInstancedIndirectCount stride 20 bytes](#7-drawinstancedindirectcount--piège-majeur)
8. [SV_VertexID et startVertexLocation](#8-sv_vertexid-et-startvertexlocation--piège-majeur)
9. [Barriers sur buffers indirect](#9-barriers-sur-buffers-indirect)
10. [PushConstants après BindComputeShader](#10-pushconstants-après-bindcomputeshader--piège-majeur)
- [CreateBuffer avec capacity > data size](#createbuffer-avec-capacity--data-size)
- [BLAS/TLAS per-frame recreation — VRAM leak](#blastlas-per-frame-recreation--vram-leak)
- [Diagnostics et debugging](#diagnostics-et-debugging)
- [Smooth Surface Nets — Rendu facetté et jointure blocky](#smooth-surface-nets--rendu-facetté-et-jointure-blocky)
- [Gestion des resource states DX12 (buffers)](#gestion-des-resource-states-dx12-buffers)
---
## APIs Wicked utilisées
| Besoin | API Wicked |
|--------|-----------|
| Clavier WASD | `wi::input::Down(CHARACTER_RANGE_START + offset)` (pas de `KEYBOARD_BUTTON_W`) |
| Souris delta | `wi::input::GetMouseState().delta_position` |
| Cacher curseur | `wi::input::HidePointer(bool)` |
| Shader loading | `wi::renderer::LoadShader()` - compile auto les .hlsl en .cso si absent |
| PSO states | `wi::renderer::GetRasterizerState()` etc. retournent des pointeurs (pas besoin de `&`) |
| Render pass | `RenderPassImage::RenderTarget(texture, loadOp, storeOp, layoutBefore, layoutAfter, subresource=-1)` |
| Font overlay | `wi::font::Params` est un struct - setter les membres un par un |
| Camera | `CameraComponent::At` est une **direction** (utilisé avec `XMMatrixLookToLH`), pas un point cible |
| Buffer create | `device->CreateBuffer(desc, raw_data_ptr, buffer)` — PAS de `SubresourceData` pour les buffers ! |
| Texture create | `device->CreateTexture(desc, subresourceData_ptr, texture)` — utilise `SubresourceData*` (différent de CreateBuffer) |
| Buffer update | `device->UpdateBuffer(buffer, data, cmd, size, offset)` |
| Push constants | `device->PushConstants(data, size, cmd)` — mappés à `register(b999)`, taille fixe 48 bytes (12 × uint32) |
| Command list | `device->BeginCommandList()` — nouveau cmd list pour render passes séparés |
| Render pass | NE JAMAIS imbriquer ! Un seul render pass actif par command list |
| Debug DX12 | Passer `"debugdevice"` en argument pour activer la couche de debug D3D12 |
| Logging | `wi::backlog::post(message, logLevel)` — préférer au logging fichier |
| Screen size (draw) | **`GetLogicalWidth()`/`GetLogicalHeight()`** pour `wi::font` et `wi::image` (PAS `GetPhysicalWidth`) |
| Solid rect draw | `wi::image::Draw(wi::texturehelper::getWhite(), params, cmd)` — ne PAS passer `nullptr` |
---
## Coordonnées logiques vs physiques — Piège majeur
Wicked Engine distingue deux systèmes de coordonnées écran :
- **Physical** (`GetPhysicalWidth()`/`GetPhysicalHeight()`) : pixels réels du backbuffer. Utilisé pour créer les render targets, viewports, et textures GPU.
- **Logical** (`GetLogicalWidth()`/`GetLogicalHeight()`) : pixels DPI-scaled. **Tout le système 2D de Wicked** (`wi::font::Draw`, `wi::image::Draw`, `wi::image::Params::pos/siz`) travaille en coordonnées logiques.
**Symptôme** : éléments HUD décalés, crosshair excentré, texte hors écran.
```cpp
// ❌ FAUX — décalé si DPI scaling ≠ 100%
float cx = (float)GetPhysicalWidth() * 0.5f;
wi::font::Params fp; fp.posX = cx;
// ✅ CORRECT
float cx = GetLogicalWidth() * 0.5f;
wi::font::Params fp; fp.posX = cx;
```
**Pour dessiner un rectangle solide** (pas de texture) :
```cpp
// ❌ FAUX — ne dessine rien
wi::image::Draw(nullptr, params, cmd);
// ✅ CORRECT — utiliser la texture blanche 1x1 intégrée
#include "wiTextureHelper.h"
wi::image::Draw(wi::texturehelper::getWhite(), params, cmd);
```
La projection 2D est définie dans `wiCanvas.h` :
```cpp
GetProjection() = XMMatrixOrthographicOffCenterLH(0, GetLogicalWidth(), GetLogicalHeight(), 0, -1, 1);
```
---
## Triplanar UDN Normal Mapping — Pièges majeurs
L'implémentation UDN (Unreal Derivative Normal) triplanar pour les normal maps a trois subtilités critiques :
### 1. NE PAS utiliser `abs(normal)` dans la formule UDN
La référence Ben Golus utilise `abs(normal)` car elle cible des terrains (normales toujours vers le haut). Pour des voxels avec 6 directions de faces, `abs()` force la composante dominante à être positive, **inversant l'éclairage sur les faces -X, -Y et -Z**.
```hlsl
// ❌ FAUX — inverse les normales sur 3 faces (le NdotL est faux)
float3 absN = abs(normal);
float3 worldNX = float3(tnX.xy + absN.zy, absN.x).zyx;
// Face -X: absN.x = 1 → résultat pointe vers +X au lieu de -X
// ✅ CORRECT — utiliser le normal brut
float3 worldNX = float3(tnX.xy + normal.zy, normal.x).zyx;
// Face -X: normal.x = -1 → résultat pointe bien vers -X
```
**Diagnostic** : ombres RT correctes (elles utilisent la géométrie) mais éclairage direct inversé sur certaines faces → contradiction visuelle.
### 2. Correction de signe pour les faces négatives
Les UV sont miroir sur les faces négatives. Le `sign(normal)` corrige la composante tangent-space X :
```hlsl
float3 axisSign = sign(normal);
tnX.x *= axisSign.x; // Flip U-tangent pour -X
tnY.x *= axisSign.y; // Flip U-tangent pour -Y
tnZ.x *= axisSign.z; // Flip U-tangent pour -Z
```
### 3. Flip green channel pour les normal maps OpenGL (seulement projection Y)
Les textures `normal_gl` ont le green channel inversé par rapport à DX. En triplanar, seule la **projection Y** (faces horizontales, UV=xz) nécessite le flip — les projections X et Z ont V=world Y qui est naturellement correct.
```hlsl
// ❌ FAUX — casse les faces verticales
tnX.y = -tnX.y; tnY.y = -tnY.y; tnZ.y = -tnZ.y;
// ✅ CORRECT — seulement la projection Y
tnY.y = -tnY.y;
```
**Formule complète correcte** :
```hlsl
float3 axisSign = sign(normal);
float3 tnX = sample(wp.zy).rgb * 2.0 - 1.0;
float3 tnY = sample(wp.xz).rgb * 2.0 - 1.0;
float3 tnZ = sample(wp.xy).rgb * 2.0 - 1.0;
tnY.y = -tnY.y; // GL flip Y-projection only
tnX.x *= axisSign.x; // sign correction
tnY.x *= axisSign.y;
tnZ.x *= axisSign.z;
float3 worldNX = float3(tnX.xy + normal.zy, normal.x).zyx; // RAW normal
float3 worldNY = float3(tnY.xy + normal.xz, normal.y).xzy;
float3 worldNZ = float3(tnZ.xy + normal.xy, normal.z);
return normalize(worldNX * w.x + worldNY * w.y + worldNZ * w.z);
```
---
## Shaders custom — Pièges importants
Les shaders custom doivent respecter le **binding model de Wicked Engine** :
### 1. Root signature obligatoire
Chaque shader DOIT avoir une root signature DX12 intégrée, soit via `#include "globals.hlsli"` (auto), soit via `[RootSignature(MACRO)]` sur le entry point.
### 2. Root signature Wicked (HLSL 6.6+)
- `b999` → push constants (12 × uint32 = 48 bytes max)
- `b0, b1, b2` → CBV root descriptors
- `t0-t15, u0-u15` → dans une descriptor table partagée
- `s0-s7` → samplers dynamiques
- `s100-s109` → static samplers (linear, point, aniso, etc.)
### 3. Chemins des shaders
- `SHADERPATH` = `<exe_dir>/shaders/hlsl6/` — où les `.cso` compilés sont stockés
- `SHADERSOURCEPATH` = `../../engine/WickedEngine/shaders/` — où les `.hlsl` sources sont cherchés
- Les shaders custom doivent être copiés dans `SHADERSOURCEPATH` (sous-dossier `voxel/`)
- `LoadShader(stage, shader, "voxel/voxelVS.cso")` → compile `SHADERSOURCEPATH/voxel/voxelVS.hlsl` si `.cso` absent
### 4. dxcompiler.dll manquant
`dxcompiler.dll` doit être à côté de l'exe sinon la compilation runtime échoue silencieusement.
### 5. CreateBuffer prend void*
`CreateBuffer` prend `void*`, pas `SubresourceData*`. L'API texture (`CreateTexture`) prend bien `SubresourceData*`.
### 6. Winding des triangles — PIÈGE MAJEUR
Wicked Engine utilise `front_counter_clockwise = true` + `CullMode::BACK` (state `RSTYPE_FRONT`). Malgré cela, les quads voxel doivent utiliser un winding **CW** (clockwise) comme défaut, pas CCW. Confirmé empiriquement via `SV_IsFrontFace` : avec des corners CCW standard, DX12 voit tous les triangles comme **back-facing**.
La règle pour nos tangent axes U/V :
- `cross(U,V) = N` (faces +X, -Y, +Z) → corners **CW** pour être front-facing
- `cross(U,V) ≠ N` (faces -X, +Y, -Z) → corners **CCW** pour être front-facing
```
CW corners: (0,0)(0,1)(1,0), (1,0)(0,1)(1,1) ← défaut
CCW corners: (0,0)(1,0)(0,1), (0,1)(1,0)(1,1) ← faces 1,2,5
```
### 7. DrawInstancedIndirectCount — PIÈGE MAJEUR
Les command signatures de Wicked Engine pour `*IndirectCount` incluent un **push constant** (1 × uint32, écrit dans `b999[0]`) AVANT chaque `D3D12_DRAW_ARGUMENTS`. Le stride par draw entry est donc **20 bytes**, pas 16.
Layout mémoire du buffer d'args indirect :
```
[uint32 pushConstant][uint32 vertexCount][uint32 instanceCount][uint32 startVertex][uint32 startInstance]
4 bytes 16 bytes (D3D12_DRAW_ARGUMENTS)
= 20 bytes par draw entry
```
Le push constant est écrit automatiquement par `ExecuteIndirect` dans `b999[0]` (premier champ de la struct push constants, soit `chunkIndex` dans notre cas). Les autres champs de b999 (quadOffset, flags...) restent tels que définis par le `PushConstants()` appelé avant `DrawInstancedIndirectCount`.
**En mode MDI, le push constant est utilisé pour packer `chunkIndex | (faceIndex << 16)`**. Le VS décode ces deux valeurs et reconstruit le quadOffset depuis le `GPUChunkInfo` :
```hlsl
chunkIndex = push.chunkIndex & 0xFFFF;
faceIdx = push.chunkIndex >> 16;
quadIndex = chunkInfo[chunkIndex].quadOffset + faceOffset[faceIdx] + (vertexID / 6);
```
**Source** : `wiGraphicsDevice_DX12.cpp` lignes 3930-3939 — la command signature est créée par PSO avec `D3D12_INDIRECT_ARGUMENT_TYPE_CONSTANT` + `D3D12_INDIRECT_ARGUMENT_TYPE_DRAW`.
### 8. SV_VertexID et startVertexLocation — PIÈGE MAJEUR
Avec `ExecuteIndirect` (DrawInstancedIndirectCount), `SV_VertexID` **n'inclut PAS de manière fiable** `startVertexLocation` de `D3D12_DRAW_ARGUMENTS`. Observé sur AMD RDNA 4 (RX 9070 XT) : SV_VertexID commence toujours à 0 pour chaque draw, ignorant startVertexLocation.
**Solution** : toujours mettre `startVertexLocation = 0` dans les indirect args, et passer l'offset des quads par un autre canal (push constant + GPUChunkInfo lookup). Ne JAMAIS compter sur `startVertexLocation` pour encoder un offset dans le mega-buffer.
### 9. Barriers sur buffers indirect
Les buffers `Usage::DEFAULT` démarrent en COMMON et décayent vers COMMON après chaque exécution de command list. La promotion implicite COMMON → COPY_DST (via UpdateBuffer) et COMMON → INDIRECT_ARGUMENT (via DrawInstancedIndirectCount) fonctionne sans barriers explicites. C'est le même pattern que les SRV buffers (megaQuadBuffer_, chunkInfoBuffer_) qui passent de COPY_DST à SRV usage sans barrier en Phase 2.1.
**Pour la Phase 2.3 (compute cull)**, des barriers explicites SONT nécessaires :
- `drawCountBuffer_` : COPY_DST → UAV (après UpdateBuffer zero) puis UAV → INDIRECT_ARGUMENT (après dispatch)
- `indirectArgsBuffer_` : UNDEFINED → UAV (COMMON après decay, `ResourceState::UNDEFINED = 0` = COMMON en Wicked) puis UAV → INDIRECT_ARGUMENT
- Wicked Engine appelle `DiscardResource()` quand `state_before == UNDEFINED`, ce qui est OK (le compute écrase les données)
### 10. PushConstants après BindComputeShader — PIÈGE MAJEUR
`PushConstants()` dispatche vers `SetGraphicsRoot32BitConstants` ou `SetComputeRoot32BitConstants` selon l'état actif :
- Si `active_pso != nullptr`**GRAPHICS** push constants
- Sinon si `active_cs != nullptr`**COMPUTE** push constants
Après `BindComputeShader` + `Dispatch`, `active_cs` reste actif. Appeler `PushConstants` à ce moment écrit dans les push constants **compute**, pas **graphics**. Le vertex shader ne voit jamais la valeur !
**Règle** : toujours appeler `PushConstants` **APRÈS** `BindPipelineState` (qui set `active_pso`) pour cibler les push constants graphics. L'ordre correct :
```cpp
BindPipelineState(&pso_); // ← active_pso = &pso_
PushConstants(&data, ...); // ← SetGraphicsRoot32BitConstants ✓
Draw*(...);
```
---
## CreateBuffer avec capacity > data size
**Symptôme** : crash dans `memmove` au démarrage (EXCEPTION_ACCESS_VIOLATION, reading past vector bounds).
**Cause racine** : `CreateBuffer(desc, data_ptr, buffer)` dans Wicked Engine copie `desc.size` bytes depuis `data_ptr` via `memmove`. Quand le buffer est pré-alloué avec une capacité plus grande que les données réelles (ex: 25% de headroom), `desc.size > vector.size() * stride` → lecture hors limites.
**Solution** : créer le buffer SANS données initiales (`CreateBuffer(desc, nullptr, buffer)`), puis uploader les données dans `Render()` via `UpdateBuffer(buffer, data, cmd, actual_size)` qui prend une taille explicite.
**Fichiers** : `VoxelRenderer.cpp``uploadTopingData()`, `uploadSmoothData()`, `uploadSmoothDataFast()`
---
## BLAS/TLAS per-frame recreation — VRAM leak
**Symptôme** : VRAM explose dès le début de l'animation F3 (terrain dynamique). Le GPU alloue des centaines de MB par seconde.
**Cause racine** : `CreateRaytracingAccelerationStructure` alloue de la mémoire GPU (scratch + result buffers) pour chaque BLAS/TLAS. Pendant l'animation, le vertex count change à chaque frame → le BLAS est recréé chaque frame. L'ancienne allocation est libérée en différé (3 frames de latence GPU) → accumulation VRAM.
Particulièrement grave pour la toping BLAS (~23M vertices, ~7.7M triangles) — chaque recréation alloue des dizaines de MB.
**Solution — allocation capacity-based** :
1. **BLAS** : Créer avec `vertex_count = capacity` (25% headroom). Avant chaque `BuildRaytracingAccelerationStructure`, mettre à jour `desc.bottom_level.geometries[0].triangles.vertex_count` avec le count réel. Le Build reconstruit le BVH in-place sans réallouer. Ne recréer le BLAS que quand le count dépasse la capacité.
```cpp
// Création (une seule fois, ou quand capacité dépassée)
if (!blas.IsValid() || vertCount > blasCapacity) {
blasCapacity = vertCount + vertCount / 4; // 25% headroom
// ... desc avec vertex_count = blasCapacity ...
dev->CreateRaytracingAccelerationStructure(&desc, &blas);
}
// Build chaque frame (update desc.vertex_count avec le count réel)
blas.desc.bottom_level.geometries[0].triangles.vertex_count = vertCount;
blas.desc.bottom_level.geometries[0].triangles.index_count = vertCount;
dev->BuildRaytracingAccelerationStructure(&blas, cmd, nullptr);
```
2. **TLAS** : Ne recréer que quand le nombre d'instances change (ex: 2→3 quand la toping BLAS devient valide). Sinon, `BuildRaytracingAccelerationStructure` suffit car les pointeurs BLAS restent stables (les BLAS ne sont pas recréés).
3. **Deferred upload pattern** : Les positions BLAS toping sont uploadées via `UpdateBuffer` dans `Render()` (flag `topingBLASDirty_`) avant le BLAS rebuild, car `Update()` n'a pas de CommandList.
**Fichiers** : `VoxelRenderer.cpp``buildAccelerationStructures()`, `VoxelRenderPath::Render()`
---
## Diagnostics et debugging
**Crash handler SEH** (`main.cpp`) : `SetUnhandledExceptionFilter` écrit :
- `bvle_crash.log` : stack trace avec symboles + adresses
- `bvle_crash.dmp` : minidump analysable avec Visual Studio
- Nécessite `dbghelp.lib` et build avec symbols (`RelWithDebInfo` ou `Debug`)
**D3D12 Debug Layer** : lancer avec `BVLEVoxels.exe debugdevice` pour activer. Active aussi DRED (Device Removed Extended Data) pour diagnostiquer les GPU hangs.
**Erreurs GPU courantes** :
- `DXGI_ERROR_INVALID_CALL` → render pass imbriqué ou resource state invalide
- `DXGI_ERROR_DEVICE_HUNG` → shader en boucle infinie ou accès mémoire hors limites
- Dialog bloquant avec `messageBox` → vient de `wi::helper::messageBox()`, ne pas confondre avec un crash
**Détection de crash GPU depuis CLI (Claude Code)** : les crashs GPU (`DXGI_ERROR_INVALID_CALL`, device removed) affichent une **modale Windows bloquante** via `wi::helper::messageBox()`. `timeout` tue le process sans détecter le crash. Pour détecter correctement :
1. **NE PAS utiliser `timeout`** pour tester — demander à l'utilisateur de lancer manuellement
2. Vérifier `bvle_backlog.txt` après exécution (contient les erreurs DX12)
3. Vérifier `bvle_crash.log` et `bvle_crash.dmp` pour les crashs SEH
4. Lancer avec `debugdevice` pour obtenir les messages de validation D3D12 détaillés dans le backlog
5. Un exit code non-zéro n'est PAS fiable : `timeout` renvoie 124, la modale attend indéfiniment
**Backlog Wicked** : `wi::backlog::SetLogFile("bvle_backlog.txt")` redirige les logs vers un fichier. Touche `~` (tilde) pour toggler la console à l'écran.
**Mode debug face-color** : lancer avec `BVLEVoxels.exe debug` pour activer. Génère un monde de test (blocs isolés) et colore chaque face selon sa direction :
- Bright Red / Dark Red = +X / -X
- Bright Green / Dark Green = +Y / -Y
- Bright Blue / Dark Blue = +Z / -Z
---
## Smooth Surface Nets — Rendu facetté et jointure blocky
### Problème 1 : Rendu smooth facetté malgré normales lisses
**Symptôme** : en mode debug (FLAT, NdotL, NORMAL), la surface smooth est parfaitement lisse. Mais en rendu final (ALL), elle apparaît facettée avec des arêtes de triangles visibles.
**Cause racine** : `geoN` (geometric normal via `ddx(worldPos)`/`ddy(worldPos)`) était utilisé pour le triplanar sampling (poids de projection) ET le normal mapping. Cette valeur est la **face normal du triangle à l'écran** — elle change de manière **discontinue** à chaque arête de triangle. Résultat :
1. **Poids triplanar discontinus** → la texture saute aux arêtes (coutures visibles)
2. **Normal map discontinu** → la perturbation normale diffère par triangle → NdotL facetté
Les modes debug étaient lisses car ils utilisaient `flatN` (smooth normal **avant** perturbation normal map), pas le `N` perturbé.
**Correction** : utiliser `N` (smooth interpolated normal) pour **tout** le triplanar dans `voxelSmoothPS.hlsl` :
- Poids triplanar albedo/heightmap → `N` (pas `geoN`)
- Normal map sampling → `N` (pas `geoN`)
- `geoN` n'est plus calculé/utilisé du tout
`N` varie continûment entre vertices → transitions lisses partout.
### Problème 2 : Jointure visible smooth/blocky
**Symptôme** : contraste visible entre faces smooth et blocky adjacentes, quasi-coplanaires.
**Causes racines** (cumulatives) :
1. **Traitements per-material dans un seul PS** — le blocky PS avait un shading spécifique grass (side darkening 60%, warm shift chromatique, ambient boost ×1.15) absent du smooth PS. Pour une face grass +X, ça créait ~40% d'écart de luminosité.
2. **Smooth normals biaisées aux frontières** — les vertex normals aux arêtes 90° (mur smooth → sol) étaient moyennées entre faces perpendiculaires (consistency ≈ 0.707), produisant une normale biaisée vers +Y au lieu de +X pur.
**Correction** :
- **Supprimer les traitements per-material hardcodés** des deux PS. Quand on aura besoin de shading par matériau, le rendre data-driven et l'appliquer identiquement dans les deux shaders.
- **Consistency-based vertex normal blend** dans `voxelSmoothCS.hlsl` : métrique `|Σfn| / Σ|fn|` qui mesure l'accord des face normals incidentes. Les vertices à faible consistency (arêtes nettes, frontières) reçoivent la face normal pure ; les vertices à haute consistency (surfaces courbes) gardent la smooth normal.
### Calibration du seuil de consistency
Le seuil `smoothstep(low, high, consistency)` contrôle le compromis lisse/net :
| Seuil | con=0.707 (90° edge) | con=0.85 (courbe) | con=0.95 (pente) | Résultat |
|---|---|---|---|---|
| `(0.85, 1.0)` | t=0 face ✓ | t=0 face ✗ | t=0.26 ≈ face ✗ | Trop agressif, tout facetté |
| `(0.60, 0.85)` | t=0.27 ≈ 73% face | t=1.0 smooth ✓ | t=1.0 smooth ✓ | Frontière visible, intérieur lisse |
| `(0.70, 0.90)` | t≈0 face ✓ | t=0.84 smooth ✓ | t=1.0 smooth ✓ | **Bon compromis** |
**Valeur retenue : `smoothstep(0.70, 0.90)`** — les arêtes 90° (con ≤ 0.707) reçoivent 100% face normal (jointure nette avec blocky), les courbes modérées (con > 0.85) restent smooth.
### Normal map strength
Le smooth PS utilise `nmStrength * 0.7` (vs `nmStrength * 1.0` pour blocky). Les surfaces courbes nécessitent des normal maps atténuées pour que les perturbations ne cassent pas la continuité visuelle du smooth shading.
### Règles
- **Toute modification de lighting/texturing** dans `voxelPS.hlsl` doit être portée dans `voxelSmoothPS.hlsl` (et vice-versa)
- **Ne JAMAIS utiliser `geoN`** (ddx/ddy) dans le smooth PS pour le triplanar ou le normal mapping — utiliser `N` exclusivement
- Les deux PS doivent produire un résultat identique sur des faces coplanaires de même matériau
**Fichiers** : `shaders/voxelSmoothCS.hlsl` (consistency blend), `shaders/voxelSmoothPS.hlsl` (triplanar + normal map), `shaders/voxelPS.hlsl` (blocky reference)
---
## Gestion des resource states DX12 (buffers)
**Wicked Engine ne fait AUCUN tracking automatique d'état pour les buffers.** Les `GPUBarrier::Buffer(buf, before, after)` sont passées directement à D3D12 sans validation. **Le `state_before` DOIT correspondre à l'état DX12 réel, sinon → DXGI_ERROR_INVALID_CALL.**
**Pièges critiques :**
- `UpdateBuffer()` → appelle `CopyBufferRegion` sans aucune barrier. Le buffer **DOIT** être en COPY_DST (ou COMMON pour promotion implicite sur frame 1).
- Après `DrawInstancedIndirectCount`, les buffers indirect restent en **INDIRECT_ARGUMENT**. Appeler `UpdateBuffer` dessus au frame suivant → crash car pas de transition INDIRECT_ARGUMENT → COPY_DST.
- Les buffers créés avec `Usage::DEFAULT` démarrent en état **COMMON** (D3D12). COMMON supporte la promotion implicite vers COPY_DST, SRV, etc. mais **PAS vers UAV**.
- Solution recommandée : **tracker l'état manuellement** avec un `mutable ResourceState` et faire des barriers explicites entre chaque usage.