# 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