# Textures stylisees reelles + quilting

Passer des couleurs procedurales a de vraies textures hand-painted dans un style
Wonderbox / Enshrouded. Inclut la technique de quilting Roblox comme optimisation.

## Etat actuel

- `textureArray_` : 5 layers 256x256 generees proceduralement (bruit + couleur unie)
- `MaterialDesc` : champs `albedoTextureIndex`, `normalTextureIndex`, `heightmapTextureIndex`
  deja presents mais pointent vers des textures generees
- Triplanar mapping : fonctionnel dans `voxelPS.hlsl` (blocky) et `voxelSmoothPS.hlsl`
- Height-based blending : fonctionnel (Phase 3), winner-takes-all + corner attenuation
- `sampler_` : deja cree, lineaire avec wrap

L'infrastructure est prete, il manque les textures et l'integration.

## Plan d'implementation

### Phase A : Preparer les textures (art)

**Format cible par materiau :**

| Texture | Format | Contenu | Taille |
|---------|--------|---------|--------|
| Albedo | RGBA8 | RGB = couleur, A = heightmap | 512x512 |
| Normal | RG8 | Normal map tangent-space (BC5) | 512x512 |

La heightmap dans le canal alpha de l'albedo est la convention Roblox et evite une
texture separee. Le height-based blending lit deja un canal height.

**Materiaux a creer (6) :**

1. **Grass** : herbe hand-painted, brins visibles, height map avec pointes hautes
2. **Dirt** : terre seche, crevasses, height map irreguliere
3. **Stone** : pierre grise, fissures, height map avec aretes saillantes
4. **Sand** : sable fin, ondulations, height map douce
5. **Snow** : neige poudreuse, surface quasi-plate, height map tres lisse
6. **Smoothstone** : pierre polie, veines subtiles

**Sources de textures stylisees (libres ou a creer) :**
- Polyhaven (CC0, PBR) : redessiner par-dessus pour le style hand-painted
- Ambientcg (CC0) : bases realistes a simplifier
- Creer from scratch dans Krita/Aseprite en 512x512

### Phase B : Charger les textures dans le texture array

**Fichier :** `VoxelRenderer.cpp`, remplacer `generateTextures()`.

```cpp
void VoxelRenderer::loadTextures() {
    // Charger chaque materiau depuis des fichiers PNG/DDS
    const char* albedoPaths[] = {
        "Content/voxel/grass_albedo.png",
        "Content/voxel/dirt_albedo.png",
        "Content/voxel/stone_albedo.png",
        "Content/voxel/sand_albedo.png",
        "Content/voxel/snow_albedo.png",
        "Content/voxel/smoothstone_albedo.png",
    };

    // Creer un texture array 512x512 x N layers
    TextureDesc desc;
    desc.width = 512;
    desc.height = 512;
    desc.arraySize = NUM_MATERIALS;
    desc.format = Format::R8G8B8A8_UNORM;
    desc.bindFlags = BindFlag::SHADER_RESOURCE;
    desc.mipLevels = 0; // auto mip generation

    // Charger chaque layer via wi::helper::loadTextureFromFile
    // puis copier dans le texture array via CopyTexture
}
```

**Wicked Engine helper :** `wi::resourcemanager::Load()` charge PNG/DDS et genere
les mips automatiquement. On peut aussi utiliser `wi::helper::CreateTexture()` avec
des donnees brutes.

**Post-build :** ajouter une copie des textures dans CMakeLists.txt :
```cmake
add_custom_command(TARGET BVLEVoxels POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_directory
        ${CMAKE_SOURCE_DIR}/assets/voxel
        $<TARGET_FILE_DIR:BVLEVoxels>/Content/voxel
)
```

### Phase C : Adapter les shaders

**Changements dans `voxelPS.hlsl` (blocky) :**

Le shader utilise deja `materialTextures` (texture array) et `triplanarSample()`.
Modifications :

```hlsl
// Actuel : couleur procedurale + texture subtile
float3 color = materialColor * texSample.rgb;

// Nouveau : texture directe, la couleur vient de la texture
float3 albedo = triplanarSample(materialTextures, worldPos, normal, matIndex).rgb;
float height = triplanarSample(materialTextures, worldPos, normal, matIndex).a;
```

Le height est utilise pour le blending inter-materiaux (deja en place).

**Changements dans `voxelSmoothPS.hlsl` :**

Identique. Le triplanar est deja en place, juste remplacer la source de couleur.

### Phase D : Detiling (anti-repetition)

Probleme : le triplanar avec des textures 512x512 montre de la repetition visible
tous les 16 voxels (si tiling = 1 texel/voxel).

**Technique Roblox (p.25) :** rotation + shift pseudo-random par vertex.

```hlsl
// Dans le VS, calculer un seed de detiling a partir de la position
uint detileSeed = hash(uint3(floor(worldPos)));

// Dans le PS, appliquer une rotation/shift aux UVs
float angle = (detileSeed & 0x3) * (3.14159 / 2.0); // 0, 90, 180, 270 degres
float2 rotatedUV = rotate2D(uv, angle);
float2 shiftedUV = rotatedUV + float2(
    ((detileSeed >> 2) & 0xF) / 16.0,
    ((detileSeed >> 6) & 0xF) / 16.0
);
```

Cela casse la repetition sans ajouter de samples supplementaires.
Le seed est passe du VS au PS via un interpolant (1 uint).

**Alternative plus simple :** varier le tiling scale par axe triplanar (1.0, 0.97, 1.03).
Casse deja pas mal la repetition pour un cout quasi nul.

### Phase E : Quilting (optimisation optionnelle)

Si le triplanar (3 fetches par texture * N textures) devient un bottleneck :

**Technique Roblox (p.22) :** choisir UN plan de projection par vertex parmi 18 plans
(6 axes * 3 rotations de 30 degres). Encode le plan ID dans le vertex data (5 bits).

```
18 plans = 6 faces * 3 rotations :
  +X face : 0deg, 30deg, 60deg
  -X face : 0deg, 30deg, 60deg
  +Y face : ...
  ...
```

Le PS n'echantillonne qu'une seule fois par materiau au lieu de 3 (triplanar).
Reduction : de 9 fetches (3 materiaux * 3 axes) a 3 fetches (3 materiaux * 1 plan).

**Pour le blocky :** pas necessaire. Les quads ont une face unique, le triplanar est
deja reduit a 1 axe dominant. Le quilting n'apporte rien.

**Pour le smooth :** potentiellement utile si on blend 3+ materiaux. A mesurer d'abord
si le triplanar est reellement un bottleneck (peu probable avec 6 materiaux).

**Verdict :** reporter le quilting apres avoir mesure. Le triplanar standard devrait
suffire avec notre nombre de materiaux.

### Phase F : Normal mapping

Ajouter une deuxieme texture array pour les normal maps (ou un 2eme set de layers).

```hlsl
// Triplanar normal map sampling
float3 normalMap = triplanarSampleNormal(normalTextures, worldPos, geometricNormal, matIndex);
// Perturber la normale geometrique
float3 finalNormal = normalize(geometricNormal + normalMap * normalStrength);
```

Le triplanar normal mapping necessite de reconstruire le TBN par axe de projection.
C'est un calcul supplementaire mais classique.

**Approche simplifiee :** pour un style hand-painted, les normal maps ne sont pas
obligatoires. L'albedo porte la majeure partie du detail visuel. A evaluer
visuellement avant d'investir du temps.

## Structure des assets

```
assets/
  voxel/
    grass_albedo.png      # RGBA : RGB=couleur, A=heightmap
    dirt_albedo.png
    stone_albedo.png
    sand_albedo.png
    snow_albedo.png
    smoothstone_albedo.png
    # (optionnel) grass_normal.png, etc.
```

## Estimation d'effort

| Phase | Effort | Dependance |
|-------|--------|------------|
| A. Creer textures (art) | 4-8h | Aucune (parallelisable) |
| B. Loader texture array | 3h | A |
| C. Adapter shaders | 2h | B |
| D. Detiling | 2h | C |
| E. Quilting | 4h | C (optionnel) |
| F. Normal maps | 3h | C (optionnel) |
| **Total minimum** | **~11h** | A+B+C+D |

## Risques

- **Style incoherent** : les textures doivent toutes avoir le meme style hand-painted.
  Mieux vaut commencer par 2 materiaux (grass+stone) et valider le look avant de
  faire les 6.
- **Mip bleeding** : dans un texture array, les mips peuvent bleed entre layers.
  Solution : padding 4px autour de chaque texture, ou utiliser des formats compresses
  (BC7) avec mips explicites.
- **Tiling visible** : le detiling resout ca, mais necessitee un tuning par materiau.
  Les textures doivent etre tileable de base.

## References

- Roblox SIGGRAPH 2020, p.21-29 (quilting, detiling, height-based blend)
- DreamCat Games, Smooth Voxel Mapping : https://bonsairobo.medium.com/smooth-voxel-mapping-a-technical-deep-dive-on-real-time-surface-nets-and-texturing-ef06d0f8ca14
- Real-time Image Quilting (Hugh Malan, SIGGRAPH 2011)
