# Experimentations -- Texture Blending (Phase 3)

## Contexte

- Moteur voxel prototype base sur Wicked Engine (DX12)
- Objectif : transitions organiques entre materiaux voxel adjacents (grass/dirt/stone/sand/snow)
- Approche retenue : PS-based voxel data lookup (le pixel shader lit directement les donnees voxel pour determiner les materiaux voisins)

---

## Phase 3 v1 -- Blend pre-encode dans les quads (abandonnee)

- **Approche** : encoder `blendMatID` (8 bits) + `blendEdges` (4 bits) dans chaque `PackedQuad` au moment du meshing GPU
- **Probleme 1** : limite a 1 seul materiau de blend par quad (pas de support 2 axes independants)
- **Probleme 2** : sur les escaliers, le materiau du bloc en-dessous (dirt sous grass) "saignait" vers le haut
- **Probleme 3** : aux jonctions tri-materiaux, les jointures etaient tres visibles
- **Decision** : abandonner cette approche au profit d'un lookup per-pixel dans le PS

---

## Phase 3 v2 -- PS-based neighbor lookup

### Iteration 1 -- Blend lineaire + heightmap boundary shift

- **Approche** : `lerp(main, neigh, weight)` avec weight 0->1, heightmap deplace la frontiere de +/-0.08 voxels
- **Zone de blend** : 0.45 (90% de la face couverte)
- **Resultat** : artefacts massifs -- la zone trop large faisait blender avec des blocs souterrains (dirt sous grass). Le heightmap shift asymetrique creait des discontinuites a la frontiere.

### Iteration 2 -- Cap du weight a 0.5

- **Fix** : `weight *= 0.5` pour garantir la continuite (`lerp(A,B,0.5) == lerp(B,A,0.5)`)
- **Resultat** : jointure encore trop visible -- la modulation heightmap brisait la symetrie (cote A : `heightBlend = f(hA-hB)`, cote B : `heightBlend = f(hB-hA)`, resultats inverses)

### Iteration 3 -- Heightmap comme deplacement de frontiere (pas modulation du montant)

- **Fix** : heightmap shift ajoute a la distance (`uDist + heightShift`), pas au poids
- **Resultat** : artefacts en damier -- le shift deplacait la frontiere de facon erratique car les heightmaps triplanaires donnaient des valeurs incoherentes entre faces adjacentes

### Iteration 4 -- Simplification radicale (gradient lineaire pur)

- **Approche** : retirer TOUT (heightmap, noise, corner attenuation). Juste `lerp(main, neigh, weight)` avec weight 0->0.5.
- **Zone de blend** reduite a 0.25 (50% de la face)
- Ajout d'un mode debug (F4) pour visualiser les zones de blend (rouge=U, bleu=V, vert=pas de blend)
- **Resultat** : **ca fonctionne !** Gradient lisse et continu, pas d'artefacts. Le debug mode a confirme que les donnees voxel etaient correctement lues (pas de rouge = data mismatch).
- **Conclusion** : le probleme n'etait pas les donnees mais les transformations appliquees dessus.

### Iteration 5 -- Corner attenuation

Trois methodes testees avec UI de selection (F5 cycle, F6/F7 ajuste param) :

#### Mode 0 -- Threshold Fade

- **Formule** : `cornerFade = saturate(otherDist / param)` (param defaut : 0.15)
- Fade lineaire dans les `param` voxels du coin
- **Resultat** : coins trop visibles, transition abrupte

#### Mode 1 -- Subtractive (reference Unity) -- RETENU

- **Formule** : `xDist_adj = xEdge - saturate(yEdge - param)` (param defaut : 0.60, optimal : 0.80)
- Quand l'autre axe depasse `param` (proche de son bord), il soustrait de cet axe
- **Resultat** : **le plus naturel** -- l'attenuation est progressive et ne cree pas de forme de coin distincte

#### Mode 2 -- Smoothstep

- **Formule** : `cornerFade = smoothstep(0, param, otherDist)` (param defaut : 0.15)
- Courbe S au lieu de lineaire
- **Resultat** : similaire au threshold mais legerement plus doux, coins encore un peu visibles

### Iteration 6 -- Winner-takes-all heightmap blending

- Abandon du `lerp(main, neigh, weight)` (gradient lisse/boueux)
- Nouveau : comparaison des scores `mainScore = h_main + bias` vs `neighScore = h_neigh - bias`
- `bias = 0.5 - weight` : loin du bord bias=0.5 (main gagne toujours), au bord bias=0 (heightmap decide)
- `blend = saturate((neighScore - mainScore) * sharpness + 0.5)` avec sharpness=16
- **Bug corrige** : le bias initial etait asymetrique (`main + (0.5-w)` vs `neigh + w`), donnant un avantage de +0.5 au voisin au bord. Fix : bias symetrique `main + bias` / `neigh - bias`.
- **Resultat** : **transitions nettes mais organiques** -- la forme de la transition est dessinee par les heightmaps, pas un gradient lineaire

---

## Configuration finale retenue

| Parametre | Valeur |
|-----------|--------|
| Zone de blend | 0.25 voxels depuis chaque bord |
| Corner attenuation | Subtractive avec param=0.80 |
| Blending | Winner-takes-all heightmap (sharpness=16) |
| Bias | Symetrique : `bias = 0.5 - weight` |
| Score main | `mainScore = h_main + bias` |
| Score voisin | `neighScore = h_neigh - bias` |
| Voisin | Stair priority (`pos + edgeDir + normalDir` d'abord, puis fallback `pos + edgeDir`) |
| Mode debug | F4 : visualisation des zones de blend |

---

## Lecons apprises

1. **Commencer simple** : le gradient lineaire pur a permis de valider que les donnees etaient correctes avant d'ajouter de la complexite
2. **Le mode debug est indispensable** : F4 a immediatement confirme que le `readVoxelMat` fonctionnait correctement
3. **La symetrie est critique** : tout calcul asymetrique entre les deux cotes d'une frontiere cree une discontinuite visible
4. **Le heightmap module OU, pas COMBIEN** : deplacer la frontiere (shift) plutot que moduler le poids (multiply) est plus stable, mais winner-takes-all est encore mieux
5. **La zone de blend doit etre petite** : 0.25 (50% de la face) vs 0.45 (90%) fait une enorme difference de qualite
