bvle-voxels/docs/plan-stylized-textures.md

7.9 KiB

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().

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 :

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 :

// 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.

// 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).

// 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