Replace pre-encoded quad blend data (v1) with per-pixel voxel data lookups in the pixel shader. The PS reads voxelDataBuffer (SRV t3) to find neighbor materials dynamically, enabling 2 independent blend axes, stair-priority neighbor detection, and winner-takes-all heightmap-driven transitions. Key design decisions validated through 6 iterations (see blending_experiments.md): - Winner-takes-all: material with highest heightmap score wins 100% (sharp but organic transitions, not smooth gradient) - Symmetric bias: bias = 0.5 - weight ensures equal chance at border - Subtractive corner attenuation (param=0.80): xAdj = xEdge - saturate(yEdge - 0.80) reduces blend at corners naturally - Blend zone = 0.25 voxels from each edge (50% of face) - Debug mode (F4) visualizes blend zones as colors
5.5 KiB
5.5 KiB
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 chaquePackedQuadau 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.5pour 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
paramvoxels 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 + biasvsneighScore = 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)vsneigh + w), donnant un avantage de +0.5 au voisin au bord. Fix : bias symetriquemain + 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
- Commencer simple : le gradient lineaire pur a permis de valider que les donnees etaient correctes avant d'ajouter de la complexite
- Le mode debug est indispensable : F4 a immediatement confirme que le
readVoxelMatfonctionnait correctement - La symetrie est critique : tout calcul asymetrique entre les deux cotes d'une frontiere cree une discontinuite visible
- 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
- La zone de blend doit etre petite : 0.25 (50% de la face) vs 0.45 (90%) fait une enorme difference de qualite