223 lines
7.9 KiB
Markdown
223 lines
7.9 KiB
Markdown
|
|
# 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)
|