Phase 8: Real stylized textures with UDN triplanar normal mapping
- Load CC0 FreeStylized textures (6 materials: grass, dirt, stone, sand, snow, smoothstone) as Texture2DArray: t1=albedo+heightmap RGBA, t7=normal maps GL format - Height-based texture blending: winner-takes-all with sharpness=16, 40% blend zone, asymmetric bias (coeff 1.6) for resistBleed materials (grass resists sand bleed) - UDN triplanar normal mapping with 3 critical fixes: * Use raw normal (NOT abs) in UDN formula — abs inverts lighting on -X/-Y/-Z faces * sign(normal) correction on tangent X for back-facing UV mirror * GL green channel flip on Y-projection only (not X/Z where V=worldY is correct) - Dirt material rendered smooth (FLAG_SMOOTH), ground_02 texture darkened 0.75 - Sun orbit debug mode (F7): 10s cycle with sinusoidal altitude - Crosshair + face debug HUD (F8): DDA raycast, camera/target/face/normal info - Screenshot F6 now writes companion .log file with full debug state - Document UDN pitfalls and logical vs physical coordinates in TROUBLESHOOTING.md - Add tools/prepare_textures.py for texture pipeline (ZIP → albedo+height RGBA + normal)
20
CLAUDE.md
|
|
@ -38,6 +38,11 @@ bvle-voxels/
|
||||||
│ ├── voxelShadowCS.hlsl # Compute shader RT shadows + raw AO (inline ray queries, Phase 6.2+6.3)
|
│ ├── voxelShadowCS.hlsl # Compute shader RT shadows + raw AO (inline ray queries, Phase 6.2+6.3)
|
||||||
│ ├── voxelAOBlurCS.hlsl # Compute shader bilateral AO blur (separable H/V, Phase 6.3)
|
│ ├── voxelAOBlurCS.hlsl # Compute shader bilateral AO blur (separable H/V, Phase 6.3)
|
||||||
│ └── voxelAOApplyCS.hlsl # Compute shader AO apply + tone mapping + saturation (Phase 6.3 + 7)
|
│ └── voxelAOApplyCS.hlsl # Compute shader AO apply + tone mapping + saturation (Phase 6.3 + 7)
|
||||||
|
├── assets/
|
||||||
|
│ ├── voxel/ # Textures stylisées (6 albedo+height RGBA + 6 normal GL, 512x512)
|
||||||
|
│ └── raw/ # ZIPs sources FreeStylized.com (CC0)
|
||||||
|
├── tools/
|
||||||
|
│ └── prepare_textures.py # Script: ZIP → albedo+heightmap RGBA + normal PNG (512x512)
|
||||||
├── CLAUDE.md
|
├── CLAUDE.md
|
||||||
└── TROUBLESHOOTING.md # Pièges techniques, debugging, APIs Wicked
|
└── TROUBLESHOOTING.md # Pièges techniques, debugging, APIs Wicked
|
||||||
```
|
```
|
||||||
|
|
@ -92,7 +97,9 @@ build/Release/BVLEVoxels.exe vulkan # Forcer backend Vulkan
|
||||||
- `F3` — toggle animation terrain (30 Hz)
|
- `F3` — toggle animation terrain (30 Hz)
|
||||||
- `F4` — toggle debug blend
|
- `F4` — toggle debug blend
|
||||||
- `F5` — cycle RT shadows/AO (ON → debug shadows → debug AO → OFF)
|
- `F5` — cycle RT shadows/AO (ON → debug shadows → debug AO → OFF)
|
||||||
- `F6` — screenshot in-app (sauvegarde `voxelRT_` en PNG)
|
- `F6` — screenshot in-app (sauvegarde `voxelRT_` en PNG + `.log` compagnon)
|
||||||
|
- `F7` — toggle sun orbit (cycle 10s, altitude sinusoïdale)
|
||||||
|
- `F8` — toggle crosshair + debug face info (camera, target, face, normal map proj)
|
||||||
|
|
||||||
### Post-build automatique (CMakeLists.txt)
|
### Post-build automatique (CMakeLists.txt)
|
||||||
|
|
||||||
|
|
@ -210,6 +217,17 @@ PS-based heightmap blending, winner-takes-all, corner attenuation subtractive. G
|
||||||
|
|
||||||
- **7.1** [FAIT] : Hemisphere ambient, colored shadows, rim light, tone mapping + saturation, screenshot mode
|
- **7.1** [FAIT] : Hemisphere ambient, colored shadows, rim light, tone mapping + saturation, screenshot mode
|
||||||
|
|
||||||
|
### Phase 8 - Textures stylisées réelles [EN COURS]
|
||||||
|
|
||||||
|
- **8.1** [FAIT] : Chargement textures CC0 FreeStylized (6 matériaux, albedo+heightmap RGBA, normal maps GL)
|
||||||
|
- **8.2** [FAIT] : Texture2DArray (t1=albedo+height, t7=normals), triplanar sampling, stb_image loading
|
||||||
|
- **8.3** [FAIT] : Height-based texture blending (winner-takes-all, sharpness=16, corner attenuation)
|
||||||
|
- **8.4** [FAIT] : Asymmetric blend pour resistBleed (coeff 1.6), zone de blend 40%
|
||||||
|
- **8.5** [FAIT] : UDN triplanar normal mapping (sign correction, GL green flip Y-proj only, NO abs)
|
||||||
|
- **8.6** [FAIT] : Dirt rendu smooth (FLAG_SMOOTH), ground_02 texture assombrie 0.75
|
||||||
|
- **8.7** [FAIT] : Sun orbit debug (F7, cycle 10s), crosshair + face debug HUD (F8)
|
||||||
|
- **8.8** [FAIT] : Screenshot F6 avec .log compagnon (camera, target, debug states, RT stats)
|
||||||
|
|
||||||
## Métriques cibles et résultats
|
## Métriques cibles et résultats
|
||||||
|
|
||||||
| Métrique | Cible | Résultat (Ryzen 7 9800X3D + RX 9070 XT) |
|
| Métrique | Cible | Résultat (Ryzen 7 9800X3D + RX 9070 XT) |
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,14 @@ add_custom_command(TARGET BVLEVoxels POST_BUILD
|
||||||
COMMENT "Copying DXC shader compiler DLL"
|
COMMENT "Copying DXC shader compiler DLL"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Copy voxel texture assets to Content/voxel/ next to the exe
|
||||||
|
add_custom_command(TARGET BVLEVoxels POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
${CMAKE_SOURCE_DIR}/assets/voxel
|
||||||
|
$<TARGET_FILE_DIR:BVLEVoxels>/Content/voxel
|
||||||
|
COMMENT "Copying voxel texture assets"
|
||||||
|
)
|
||||||
|
|
||||||
# Copy our custom shader sources into Wicked's shader source tree
|
# Copy our custom shader sources into Wicked's shader source tree
|
||||||
# so LoadShader can find and compile them as "voxel/voxelVS.cso"
|
# so LoadShader can find and compile them as "voxel/voxelVS.cso"
|
||||||
add_custom_command(TARGET BVLEVoxels POST_BUILD
|
add_custom_command(TARGET BVLEVoxels POST_BUILD
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
## Table des matières
|
## Table des matières
|
||||||
|
|
||||||
- [APIs Wicked utilisées](#apis-wicked-utilisées)
|
- [APIs Wicked utilisées](#apis-wicked-utilisées)
|
||||||
|
- [Coordonnées logiques vs physiques](#coordonnées-logiques-vs-physiques--piège-majeur)
|
||||||
|
- [Triplanar UDN Normal Mapping](#triplanar-udn-normal-mapping--pièges-majeurs)
|
||||||
- [Shaders custom — Pièges importants](#shaders-custom--pièges-importants)
|
- [Shaders custom — Pièges importants](#shaders-custom--pièges-importants)
|
||||||
1. [Root signature obligatoire](#1-root-signature-obligatoire)
|
1. [Root signature obligatoire](#1-root-signature-obligatoire)
|
||||||
2. [Root signature Wicked (HLSL 6.6+)](#2-root-signature-wicked-hlsl-66)
|
2. [Root signature Wicked (HLSL 6.6+)](#2-root-signature-wicked-hlsl-66)
|
||||||
|
|
@ -41,6 +43,107 @@
|
||||||
| Render pass | NE JAMAIS imbriquer ! Un seul render pass actif par command list |
|
| Render pass | NE JAMAIS imbriquer ! Un seul render pass actif par command list |
|
||||||
| Debug DX12 | Passer `"debugdevice"` en argument pour activer la couche de debug D3D12 |
|
| Debug DX12 | Passer `"debugdevice"` en argument pour activer la couche de debug D3D12 |
|
||||||
| Logging | `wi::backlog::post(message, logLevel)` — préférer au logging fichier |
|
| Logging | `wi::backlog::post(message, logLevel)` — préférer au logging fichier |
|
||||||
|
| Screen size (draw) | **`GetLogicalWidth()`/`GetLogicalHeight()`** pour `wi::font` et `wi::image` (PAS `GetPhysicalWidth`) |
|
||||||
|
| Solid rect draw | `wi::image::Draw(wi::texturehelper::getWhite(), params, cmd)` — ne PAS passer `nullptr` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coordonnées logiques vs physiques — Piège majeur
|
||||||
|
|
||||||
|
Wicked Engine distingue deux systèmes de coordonnées écran :
|
||||||
|
|
||||||
|
- **Physical** (`GetPhysicalWidth()`/`GetPhysicalHeight()`) : pixels réels du backbuffer. Utilisé pour créer les render targets, viewports, et textures GPU.
|
||||||
|
- **Logical** (`GetLogicalWidth()`/`GetLogicalHeight()`) : pixels DPI-scaled. **Tout le système 2D de Wicked** (`wi::font::Draw`, `wi::image::Draw`, `wi::image::Params::pos/siz`) travaille en coordonnées logiques.
|
||||||
|
|
||||||
|
**Symptôme** : éléments HUD décalés, crosshair excentré, texte hors écran.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// ❌ FAUX — décalé si DPI scaling ≠ 100%
|
||||||
|
float cx = (float)GetPhysicalWidth() * 0.5f;
|
||||||
|
wi::font::Params fp; fp.posX = cx;
|
||||||
|
|
||||||
|
// ✅ CORRECT
|
||||||
|
float cx = GetLogicalWidth() * 0.5f;
|
||||||
|
wi::font::Params fp; fp.posX = cx;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pour dessiner un rectangle solide** (pas de texture) :
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// ❌ FAUX — ne dessine rien
|
||||||
|
wi::image::Draw(nullptr, params, cmd);
|
||||||
|
|
||||||
|
// ✅ CORRECT — utiliser la texture blanche 1x1 intégrée
|
||||||
|
#include "wiTextureHelper.h"
|
||||||
|
wi::image::Draw(wi::texturehelper::getWhite(), params, cmd);
|
||||||
|
```
|
||||||
|
|
||||||
|
La projection 2D est définie dans `wiCanvas.h` :
|
||||||
|
```cpp
|
||||||
|
GetProjection() = XMMatrixOrthographicOffCenterLH(0, GetLogicalWidth(), GetLogicalHeight(), 0, -1, 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Triplanar UDN Normal Mapping — Pièges majeurs
|
||||||
|
|
||||||
|
L'implémentation UDN (Unreal Derivative Normal) triplanar pour les normal maps a trois subtilités critiques :
|
||||||
|
|
||||||
|
### 1. NE PAS utiliser `abs(normal)` dans la formule UDN
|
||||||
|
|
||||||
|
La référence Ben Golus utilise `abs(normal)` car elle cible des terrains (normales toujours vers le haut). Pour des voxels avec 6 directions de faces, `abs()` force la composante dominante à être positive, **inversant l'éclairage sur les faces -X, -Y et -Z**.
|
||||||
|
|
||||||
|
```hlsl
|
||||||
|
// ❌ FAUX — inverse les normales sur 3 faces (le NdotL est faux)
|
||||||
|
float3 absN = abs(normal);
|
||||||
|
float3 worldNX = float3(tnX.xy + absN.zy, absN.x).zyx;
|
||||||
|
// Face -X: absN.x = 1 → résultat pointe vers +X au lieu de -X
|
||||||
|
|
||||||
|
// ✅ CORRECT — utiliser le normal brut
|
||||||
|
float3 worldNX = float3(tnX.xy + normal.zy, normal.x).zyx;
|
||||||
|
// Face -X: normal.x = -1 → résultat pointe bien vers -X
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagnostic** : ombres RT correctes (elles utilisent la géométrie) mais éclairage direct inversé sur certaines faces → contradiction visuelle.
|
||||||
|
|
||||||
|
### 2. Correction de signe pour les faces négatives
|
||||||
|
|
||||||
|
Les UV sont miroir sur les faces négatives. Le `sign(normal)` corrige la composante tangent-space X :
|
||||||
|
|
||||||
|
```hlsl
|
||||||
|
float3 axisSign = sign(normal);
|
||||||
|
tnX.x *= axisSign.x; // Flip U-tangent pour -X
|
||||||
|
tnY.x *= axisSign.y; // Flip U-tangent pour -Y
|
||||||
|
tnZ.x *= axisSign.z; // Flip U-tangent pour -Z
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Flip green channel pour les normal maps OpenGL (seulement projection Y)
|
||||||
|
|
||||||
|
Les textures `normal_gl` ont le green channel inversé par rapport à DX. En triplanar, seule la **projection Y** (faces horizontales, UV=xz) nécessite le flip — les projections X et Z ont V=world Y qui est naturellement correct.
|
||||||
|
|
||||||
|
```hlsl
|
||||||
|
// ❌ FAUX — casse les faces verticales
|
||||||
|
tnX.y = -tnX.y; tnY.y = -tnY.y; tnZ.y = -tnZ.y;
|
||||||
|
|
||||||
|
// ✅ CORRECT — seulement la projection Y
|
||||||
|
tnY.y = -tnY.y;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Formule complète correcte** :
|
||||||
|
```hlsl
|
||||||
|
float3 axisSign = sign(normal);
|
||||||
|
float3 tnX = sample(wp.zy).rgb * 2.0 - 1.0;
|
||||||
|
float3 tnY = sample(wp.xz).rgb * 2.0 - 1.0;
|
||||||
|
float3 tnZ = sample(wp.xy).rgb * 2.0 - 1.0;
|
||||||
|
tnY.y = -tnY.y; // GL flip Y-projection only
|
||||||
|
tnX.x *= axisSign.x; // sign correction
|
||||||
|
tnY.x *= axisSign.y;
|
||||||
|
tnZ.x *= axisSign.z;
|
||||||
|
float3 worldNX = float3(tnX.xy + normal.zy, normal.x).zyx; // RAW normal
|
||||||
|
float3 worldNY = float3(tnY.xy + normal.xz, normal.y).xzy;
|
||||||
|
float3 worldNZ = float3(tnZ.xy + normal.xy, normal.z);
|
||||||
|
return normalize(worldNX * w.x + worldNY * w.y + worldNZ * w.z);
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
BIN
assets/voxel/dirt_albedo.png
Normal file
|
After Width: | Height: | Size: 327 KiB |
BIN
assets/voxel/dirt_normal.png
Normal file
|
After Width: | Height: | Size: 260 KiB |
BIN
assets/voxel/grass_albedo.png
Normal file
|
After Width: | Height: | Size: 472 KiB |
BIN
assets/voxel/grass_normal.png
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
assets/voxel/sand_albedo.png
Normal file
|
After Width: | Height: | Size: 262 KiB |
BIN
assets/voxel/sand_normal.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
assets/voxel/smoothstone_albedo.png
Normal file
|
After Width: | Height: | Size: 431 KiB |
BIN
assets/voxel/smoothstone_normal.png
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
assets/voxel/snow_albedo.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
assets/voxel/snow_normal.png
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
assets/voxel/stone_albedo.png
Normal file
|
After Width: | Height: | Size: 488 KiB |
BIN
assets/voxel/stone_normal.png
Normal file
|
After Width: | Height: | Size: 222 KiB |
|
|
@ -5,6 +5,7 @@
|
||||||
#include "voxelCommon.hlsli"
|
#include "voxelCommon.hlsli"
|
||||||
|
|
||||||
Texture2DArray materialTextures : register(t1);
|
Texture2DArray materialTextures : register(t1);
|
||||||
|
Texture2DArray normalTextures : register(t7);
|
||||||
SamplerState materialSampler : register(s0);
|
SamplerState materialSampler : register(s0);
|
||||||
|
|
||||||
// Voxel data buffer (same as compute mesher uses) — bound at t3 in GPU mesh path
|
// Voxel data buffer (same as compute mesher uses) — bound at t3 in GPU mesh path
|
||||||
|
|
@ -120,6 +121,40 @@ float4 sampleTriplanarRGBA(float3 worldPos, float3 normal, uint texIndex, float
|
||||||
return colX * w.x + colY * w.y + colZ * w.z;
|
return colX * w.x + colY * w.y + colZ * w.z;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Triplanar normal mapping ───────────────────────────────────────
|
||||||
|
// UDN (Unreal Derivative Normal) triplanar blend.
|
||||||
|
// For each projection axis, the tangent-space normal's XY perturbs the
|
||||||
|
// two world-space axes orthogonal to the projection direction.
|
||||||
|
float3 sampleTriplanarNormal(float3 worldPos, float3 normal, uint texIndex, float tiling) {
|
||||||
|
float3 w = triplanarWeights(normal, 4.0);
|
||||||
|
float3 axisSign = sign(normal);
|
||||||
|
|
||||||
|
// Sample tangent-space normals per projection axis (Ben Golus UDN triplanar)
|
||||||
|
float3 tnX = normalTextures.Sample(materialSampler, float3(worldPos.zy * tiling, (float)texIndex)).rgb * 2.0 - 1.0;
|
||||||
|
float3 tnY = normalTextures.Sample(materialSampler, float3(worldPos.xz * tiling, (float)texIndex)).rgb * 2.0 - 1.0;
|
||||||
|
float3 tnZ = normalTextures.Sample(materialSampler, float3(worldPos.xy * tiling, (float)texIndex)).rgb * 2.0 - 1.0;
|
||||||
|
|
||||||
|
// OpenGL normal maps: flip green channel ONLY for Y-projection (horizontal faces).
|
||||||
|
// X/Z projections have texture V = world Y (up), which already matches GL convention.
|
||||||
|
// Y-projection has texture V = world Z, where GL/DX conventions differ.
|
||||||
|
tnY.y = -tnY.y;
|
||||||
|
|
||||||
|
// Sign correction for back-facing projections (Golus reference)
|
||||||
|
// Flips the tangent-space X to account for mirrored UVs on negative faces.
|
||||||
|
tnX.x *= axisSign.x;
|
||||||
|
tnY.x *= axisSign.y;
|
||||||
|
tnZ.x *= axisSign.z;
|
||||||
|
|
||||||
|
// UDN blend using RAW normal (NOT abs!) so that negative faces (-X,-Y,-Z)
|
||||||
|
// produce normals pointing in the correct direction. abs() would force
|
||||||
|
// all dominant components positive, inverting lighting on 3 of 6 faces.
|
||||||
|
float3 worldNX = float3(tnX.xy + normal.zy, normal.x).zyx;
|
||||||
|
float3 worldNY = float3(tnY.xy + normal.xz, normal.y).xzy;
|
||||||
|
float3 worldNZ = float3(tnZ.xy + normal.xy, normal.z);
|
||||||
|
|
||||||
|
return normalize(worldNX * w.x + worldNY * w.y + worldNZ * w.z);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Debug face colors ──────────────────────────────────────────────
|
// ── Debug face colors ──────────────────────────────────────────────
|
||||||
static const float3 faceDebugColors[6] = {
|
static const float3 faceDebugColors[6] = {
|
||||||
float3(1.0, 0.2, 0.2), // 0: +X = RED
|
float3(1.0, 0.2, 0.2), // 0: +X = RED
|
||||||
|
|
@ -158,8 +193,6 @@ PSOutput main(PSInput input)
|
||||||
|
|
||||||
// ── NORMAL MODE: triplanar textured with height-based blending ──
|
// ── NORMAL MODE: triplanar textured with height-based blending ──
|
||||||
float3 N = normalize(input.normal);
|
float3 N = normalize(input.normal);
|
||||||
float3 L = normalize(-sunDirection.xyz);
|
|
||||||
float NdotL = max(dot(N, L), 0.0);
|
|
||||||
|
|
||||||
uint texIndex = clamp(input.materialID - 1u, 0u, 5u);
|
uint texIndex = clamp(input.materialID - 1u, 0u, 5u);
|
||||||
float tiling = textureTiling;
|
float tiling = textureTiling;
|
||||||
|
|
@ -198,8 +231,8 @@ PSOutput main(PSInput input)
|
||||||
uint uNeighborMat = getNeighborMat(voxelCoord, uEdgeDir, normalDir, input.chunkIndex);
|
uint uNeighborMat = getNeighborMat(voxelCoord, uEdgeDir, normalDir, input.chunkIndex);
|
||||||
uint vNeighborMat = getNeighborMat(voxelCoord, vEdgeDir, normalDir, input.chunkIndex);
|
uint vNeighborMat = getNeighborMat(voxelCoord, vEdgeDir, normalDir, input.chunkIndex);
|
||||||
|
|
||||||
// Blend zone: 0.25 voxels from each edge (covers 50% of face total)
|
// Blend zone: 0.40 voxels from each edge (covers 80% of face total)
|
||||||
float blendZone = 0.25;
|
float blendZone = 0.40;
|
||||||
|
|
||||||
// Edge distances normalized to 0..1 (0=center, 1=edge) for corner attenuation
|
// Edge distances normalized to 0..1 (0=center, 1=edge) for corner attenuation
|
||||||
float uEdge = abs(faceFracU - 0.5) * 2.0; // 0 at center, 1 at edge
|
float uEdge = abs(faceFracU - 0.5) * 2.0; // 0 at center, 1 at edge
|
||||||
|
|
@ -213,12 +246,14 @@ PSOutput main(PSInput input)
|
||||||
float uWeight = saturate((uAdj - blendStart) / (1.0 - blendStart)) * 0.5;
|
float uWeight = saturate((uAdj - blendStart) / (1.0 - blendStart)) * 0.5;
|
||||||
float vWeight = saturate((vAdj - blendStart) / (1.0 - blendStart)) * 0.5;
|
float vWeight = saturate((vAdj - blendStart) / (1.0 - blendStart)) * 0.5;
|
||||||
|
|
||||||
// Only blend if neighbor has a different material AND blend flags allow it:
|
// Blend flags:
|
||||||
// - Current material must NOT resist bleed (resistBleedMask)
|
// - mainResists: current material resists being bled onto → no blending from this side
|
||||||
// - Neighbor material must be allowed to bleed (bleedMask)
|
// - neighResists: neighbor resists bleed → asymmetric blend (neighbor dominates at edge)
|
||||||
bool mainResists = (resistBleedMask >> input.materialID) & 1u;
|
bool mainResists = (resistBleedMask >> input.materialID) & 1u;
|
||||||
bool uNeighCanBleed = (bleedMask >> uNeighborMat) & 1u;
|
bool uNeighCanBleed = (bleedMask >> uNeighborMat) & 1u;
|
||||||
bool vNeighCanBleed = (bleedMask >> vNeighborMat) & 1u;
|
bool vNeighCanBleed = (bleedMask >> vNeighborMat) & 1u;
|
||||||
|
bool uNeighResists = (resistBleedMask >> uNeighborMat) & 1u;
|
||||||
|
bool vNeighResists = (resistBleedMask >> vNeighborMat) & 1u;
|
||||||
bool uBlend = (uNeighborMat > 0u && uNeighborMat != input.materialID && uWeight > 0.001
|
bool uBlend = (uNeighborMat > 0u && uNeighborMat != input.materialID && uWeight > 0.001
|
||||||
&& !mainResists && uNeighCanBleed);
|
&& !mainResists && uNeighCanBleed);
|
||||||
bool vBlend = (vNeighborMat > 0u && vNeighborMat != input.materialID && vWeight > 0.001
|
bool vBlend = (vNeighborMat > 0u && vNeighborMat != input.materialID && vWeight > 0.001
|
||||||
|
|
@ -258,9 +293,16 @@ PSOutput main(PSInput input)
|
||||||
uint uTexIdx = clamp(uNeighborMat - 1u, 0u, 5u);
|
uint uTexIdx = clamp(uNeighborMat - 1u, 0u, 5u);
|
||||||
float4 uTex = sampleTriplanarRGBA(input.worldPos, N, uTexIdx, tiling);
|
float4 uTex = sampleTriplanarRGBA(input.worldPos, N, uTexIdx, tiling);
|
||||||
|
|
||||||
// Symmetric proximity bias: at edge (weight=0.5) bias=0 → pure heightmap.
|
// Proximity bias controls heightmap blending:
|
||||||
// Away from edge (weight=0) bias=0.5 → main always wins.
|
// Symmetric: at edge (w=0.5) bias=0 → pure heightmap; center (w=0) bias=0.5 → main wins
|
||||||
float bias = 0.5 - uWeight;
|
// Asymmetric (neighbor resists bleed): at edge bias=-0.15 → neighbor gets +0.3
|
||||||
|
// score advantage (dominates at equal heights); center bias=0.5 → main wins
|
||||||
|
float bias;
|
||||||
|
if (uNeighResists) {
|
||||||
|
bias = 0.5 - uWeight * 1.6;
|
||||||
|
} else {
|
||||||
|
bias = 0.5 - uWeight;
|
||||||
|
}
|
||||||
float mainScore = mainTex.a + bias;
|
float mainScore = mainTex.a + bias;
|
||||||
float neighScore = uTex.a - bias;
|
float neighScore = uTex.a - bias;
|
||||||
|
|
||||||
|
|
@ -272,7 +314,12 @@ PSOutput main(PSInput input)
|
||||||
uint vTexIdx = clamp(vNeighborMat - 1u, 0u, 5u);
|
uint vTexIdx = clamp(vNeighborMat - 1u, 0u, 5u);
|
||||||
float4 vTex = sampleTriplanarRGBA(input.worldPos, N, vTexIdx, tiling);
|
float4 vTex = sampleTriplanarRGBA(input.worldPos, N, vTexIdx, tiling);
|
||||||
|
|
||||||
float bias = 0.5 - vWeight;
|
float bias;
|
||||||
|
if (vNeighResists) {
|
||||||
|
bias = 0.5 - vWeight * 1.6;
|
||||||
|
} else {
|
||||||
|
bias = 0.5 - vWeight;
|
||||||
|
}
|
||||||
float mainScore = mainTex.a + bias;
|
float mainScore = mainTex.a + bias;
|
||||||
float neighScore = vTex.a - bias;
|
float neighScore = vTex.a - bias;
|
||||||
|
|
||||||
|
|
@ -292,7 +339,14 @@ PSOutput main(PSInput input)
|
||||||
albedo = (input.materialID > 0u) ? texColor : baseColor;
|
albedo = (input.materialID > 0u) ? texColor : baseColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Normal map perturbation ──
|
||||||
|
float3 perturbedN = sampleTriplanarNormal(input.worldPos, N, texIndex, tiling);
|
||||||
|
// Blend between flat and perturbed normal (strength control)
|
||||||
|
N = normalize(lerp(N, perturbedN, 0.7));
|
||||||
|
|
||||||
// ── Lighting ──
|
// ── Lighting ──
|
||||||
|
float3 L = normalize(-sunDirection.xyz);
|
||||||
|
float NdotL = max(dot(N, L), 0.0);
|
||||||
float hemiLerp = N.y * 0.5 + 0.5; // 0=down, 1=up
|
float hemiLerp = N.y * 0.5 + 0.5; // 0=down, 1=up
|
||||||
float3 ambient = lerp(groundAmbient.rgb, skyAmbient.rgb, hemiLerp);
|
float3 ambient = lerp(groundAmbient.rgb, skyAmbient.rgb, hemiLerp);
|
||||||
float3 diffuse = sunColor.rgb * NdotL;
|
float3 diffuse = sunColor.rgb * NdotL;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
#include "voxelCommon.hlsli"
|
#include "voxelCommon.hlsli"
|
||||||
|
|
||||||
Texture2DArray<float4> materialTextures : register(t1);
|
Texture2DArray<float4> materialTextures : register(t1);
|
||||||
|
Texture2DArray<float4> normalTextures : register(t7);
|
||||||
StructuredBuffer<GPUChunkInfo> chunkInfoBuffer : register(t2);
|
StructuredBuffer<GPUChunkInfo> chunkInfoBuffer : register(t2);
|
||||||
StructuredBuffer<uint> voxelData : register(t3);
|
StructuredBuffer<uint> voxelData : register(t3);
|
||||||
SamplerState texSampler : register(s0);
|
SamplerState texSampler : register(s0);
|
||||||
|
|
@ -90,6 +91,27 @@ float4 sampleTriplanarRGBA(float3 wp, float3 n, uint texIdx, float tiling) {
|
||||||
return cx * w.x + cy * w.y + cz * w.z;
|
return cx * w.x + cy * w.y + cz * w.z;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Triplanar normal mapping (UDN blend) ────────────────────────
|
||||||
|
float3 sampleTriplanarNormal(float3 wp, float3 n, uint texIdx, float tiling) {
|
||||||
|
float3 w = triplanarWeights(n, 4.0);
|
||||||
|
float3 axisSign = sign(n);
|
||||||
|
// Ben Golus UDN reference — swizzled coordinates + sign corrections
|
||||||
|
float3 tnX = normalTextures.Sample(texSampler, float3(wp.zy * tiling, (float)texIdx)).rgb * 2.0 - 1.0;
|
||||||
|
float3 tnY = normalTextures.Sample(texSampler, float3(wp.xz * tiling, (float)texIdx)).rgb * 2.0 - 1.0;
|
||||||
|
float3 tnZ = normalTextures.Sample(texSampler, float3(wp.xy * tiling, (float)texIdx)).rgb * 2.0 - 1.0;
|
||||||
|
// OpenGL normal maps: flip green channel ONLY for Y-projection
|
||||||
|
tnY.y = -tnY.y;
|
||||||
|
// Sign correction for back-facing projections
|
||||||
|
tnX.x *= axisSign.x;
|
||||||
|
tnY.x *= axisSign.y;
|
||||||
|
tnZ.x *= axisSign.z;
|
||||||
|
// UDN blend using RAW normal (NOT abs!) — preserves sign for negative faces
|
||||||
|
float3 worldNX = float3(tnX.xy + n.zy, n.x).zyx;
|
||||||
|
float3 worldNY = float3(tnY.xy + n.xz, n.y).xzy;
|
||||||
|
float3 worldNZ = float3(tnZ.xy + n.xy, n.z);
|
||||||
|
return normalize(worldNX * w.x + worldNY * w.y + worldNZ * w.z);
|
||||||
|
}
|
||||||
|
|
||||||
// ── MRT Output ──────────────────────────────────────────────────
|
// ── MRT Output ──────────────────────────────────────────────────
|
||||||
struct PSOutput {
|
struct PSOutput {
|
||||||
float4 color : SV_TARGET0;
|
float4 color : SV_TARGET0;
|
||||||
|
|
@ -160,7 +182,7 @@ PSOutput main(PSInput input) {
|
||||||
uint vNeighborMat = getNeighborMat(voxelCoord, vEdgeDir, normalDir, input.chunkIndex);
|
uint vNeighborMat = getNeighborMat(voxelCoord, vEdgeDir, normalDir, input.chunkIndex);
|
||||||
|
|
||||||
// ── Blend weights (SAME params as blocky PS) ──
|
// ── Blend weights (SAME params as blocky PS) ──
|
||||||
float blendZone = 0.25;
|
float blendZone = 0.40;
|
||||||
float uEdge = abs(faceFracU - 0.5) * 2.0;
|
float uEdge = abs(faceFracU - 0.5) * 2.0;
|
||||||
float vEdge = abs(faceFracV - 0.5) * 2.0;
|
float vEdge = abs(faceFracV - 0.5) * 2.0;
|
||||||
|
|
||||||
|
|
@ -175,6 +197,8 @@ PSOutput main(PSInput input) {
|
||||||
bool mainResists = (resistBleedMask >> selfMat) & 1u;
|
bool mainResists = (resistBleedMask >> selfMat) & 1u;
|
||||||
bool uNeighCanBleed = (bleedMask >> uNeighborMat) & 1u;
|
bool uNeighCanBleed = (bleedMask >> uNeighborMat) & 1u;
|
||||||
bool vNeighCanBleed = (bleedMask >> vNeighborMat) & 1u;
|
bool vNeighCanBleed = (bleedMask >> vNeighborMat) & 1u;
|
||||||
|
bool uNeighResists = (resistBleedMask >> uNeighborMat) & 1u;
|
||||||
|
bool vNeighResists = (resistBleedMask >> vNeighborMat) & 1u;
|
||||||
bool uBlend = (uNeighborMat > 0u && uNeighborMat != selfMat && uWeight > 0.001
|
bool uBlend = (uNeighborMat > 0u && uNeighborMat != selfMat && uWeight > 0.001
|
||||||
&& !mainResists && uNeighCanBleed);
|
&& !mainResists && uNeighCanBleed);
|
||||||
bool vBlend = (vNeighborMat > 0u && vNeighborMat != selfMat && vWeight > 0.001
|
bool vBlend = (vNeighborMat > 0u && vNeighborMat != selfMat && vWeight > 0.001
|
||||||
|
|
@ -192,7 +216,12 @@ PSOutput main(PSInput input) {
|
||||||
if (uBlend) {
|
if (uBlend) {
|
||||||
uint uTexIdx = clamp(uNeighborMat - 1u, 0u, 5u);
|
uint uTexIdx = clamp(uNeighborMat - 1u, 0u, 5u);
|
||||||
float4 uTex = sampleTriplanarRGBA(input.worldPos, geoN, uTexIdx, tiling);
|
float4 uTex = sampleTriplanarRGBA(input.worldPos, geoN, uTexIdx, tiling);
|
||||||
float bias = 0.5 - uWeight;
|
float bias;
|
||||||
|
if (uNeighResists) {
|
||||||
|
bias = 0.5 - uWeight * 1.6;
|
||||||
|
} else {
|
||||||
|
bias = 0.5 - uWeight;
|
||||||
|
}
|
||||||
float mainScore = mainTex.a + bias;
|
float mainScore = mainTex.a + bias;
|
||||||
float neighScore = uTex.a - bias;
|
float neighScore = uTex.a - bias;
|
||||||
float blend = saturate((neighScore - mainScore) * sharpness + 0.5);
|
float blend = saturate((neighScore - mainScore) * sharpness + 0.5);
|
||||||
|
|
@ -202,7 +231,12 @@ PSOutput main(PSInput input) {
|
||||||
if (vBlend) {
|
if (vBlend) {
|
||||||
uint vTexIdx = clamp(vNeighborMat - 1u, 0u, 5u);
|
uint vTexIdx = clamp(vNeighborMat - 1u, 0u, 5u);
|
||||||
float4 vTex = sampleTriplanarRGBA(input.worldPos, geoN, vTexIdx, tiling);
|
float4 vTex = sampleTriplanarRGBA(input.worldPos, geoN, vTexIdx, tiling);
|
||||||
float bias = 0.5 - vWeight;
|
float bias;
|
||||||
|
if (vNeighResists) {
|
||||||
|
bias = 0.5 - vWeight * 1.6;
|
||||||
|
} else {
|
||||||
|
bias = 0.5 - vWeight;
|
||||||
|
}
|
||||||
float mainScore = mainTex.a + bias;
|
float mainScore = mainTex.a + bias;
|
||||||
float neighScore = vTex.a - bias;
|
float neighScore = vTex.a - bias;
|
||||||
float blend = saturate((neighScore - mainScore) * sharpness + 0.5);
|
float blend = saturate((neighScore - mainScore) * sharpness + 0.5);
|
||||||
|
|
@ -214,6 +248,10 @@ PSOutput main(PSInput input) {
|
||||||
albedo = sampleTriplanar(input.worldPos, geoN, selfTexIdx, tiling);
|
albedo = sampleTriplanar(input.worldPos, geoN, selfTexIdx, tiling);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Normal map perturbation ──
|
||||||
|
float3 perturbedN = sampleTriplanarNormal(input.worldPos, geoN, selfTexIdx, tiling);
|
||||||
|
N = normalize(lerp(N, perturbedN, 0.5)); // lighter strength on smooth surfaces
|
||||||
|
|
||||||
// Lighting
|
// Lighting
|
||||||
float3 L = normalize(-sunDirection.xyz);
|
float3 L = normalize(-sunDirection.xyz);
|
||||||
float NdotL = max(dot(N, L), 0.0);
|
float NdotL = max(dot(N, L), 0.0);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "Utility/stb_image.h"
|
||||||
|
#include "wiTextureHelper.h"
|
||||||
|
|
||||||
using namespace wi::graphics;
|
using namespace wi::graphics;
|
||||||
|
|
||||||
namespace voxel {
|
namespace voxel {
|
||||||
|
|
@ -26,7 +29,7 @@ void VoxelRenderer::initialize(GraphicsDevice* dev) {
|
||||||
initialized_ = false;
|
initialized_ = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
generateTextures();
|
loadTextures();
|
||||||
|
|
||||||
// Create chunk info buffer (SRV for VS chunk lookup)
|
// Create chunk info buffer (SRV for VS chunk lookup)
|
||||||
GPUBufferDesc infoDesc;
|
GPUBufferDesc infoDesc;
|
||||||
|
|
@ -222,71 +225,68 @@ void VoxelRenderer::createPipeline() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Procedural texture generation ───────────────────────────────
|
// ── Texture loading from PNG files ──────────────────────────────
|
||||||
|
|
||||||
static void generateNoiseTexture(uint8_t* pixels, int w, int h,
|
void VoxelRenderer::loadTextures() {
|
||||||
uint8_t r0, uint8_t g0, uint8_t b0,
|
const int TEX_SIZE = 512;
|
||||||
uint8_t r1, uint8_t g1, uint8_t b1,
|
|
||||||
uint32_t seed, float heightFreq = 1.0f, float heightContrast = 1.0f)
|
|
||||||
{
|
|
||||||
uint32_t s = seed;
|
|
||||||
uint32_t s2 = seed * 7919u + 104729u; // separate seed for heightmap
|
|
||||||
for (int y = 0; y < h; y++) {
|
|
||||||
for (int x = 0; x < w; x++) {
|
|
||||||
s = s * 1664525u + 1013904223u;
|
|
||||||
float noise = (float)(s & 0xFFFF) / 65535.0f;
|
|
||||||
float fx = (float)x / w;
|
|
||||||
float fy = (float)y / h;
|
|
||||||
float pattern = 0.5f + 0.5f * std::sin(fx * 20.0f + noise * 3.0f) *
|
|
||||||
std::cos(fy * 20.0f + noise * 3.0f);
|
|
||||||
float t = noise * 0.6f + pattern * 0.4f;
|
|
||||||
|
|
||||||
int idx = (y * w + x) * 4;
|
|
||||||
pixels[idx + 0] = (uint8_t)(r0 + (r1 - r0) * t);
|
|
||||||
pixels[idx + 1] = (uint8_t)(g0 + (g1 - g0) * t);
|
|
||||||
pixels[idx + 2] = (uint8_t)(b0 + (b1 - b0) * t);
|
|
||||||
|
|
||||||
// Heightmap in alpha: separate noise for height-based material blending
|
|
||||||
s2 = s2 * 1664525u + 1013904223u;
|
|
||||||
float hn = (float)(s2 & 0xFFFF) / 65535.0f;
|
|
||||||
float hPattern = 0.5f + 0.5f * std::sin(fx * 12.0f * heightFreq + hn * 2.0f) *
|
|
||||||
std::cos(fy * 12.0f * heightFreq + hn * 2.0f);
|
|
||||||
float heightVal = hn * 0.5f + hPattern * 0.5f;
|
|
||||||
heightVal = std::clamp(heightVal * heightContrast, 0.0f, 1.0f);
|
|
||||||
pixels[idx + 3] = (uint8_t)(heightVal * 255.0f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void VoxelRenderer::generateTextures() {
|
|
||||||
const int TEX_SIZE = 256;
|
|
||||||
const int NUM_MATERIALS = 6;
|
const int NUM_MATERIALS = 6;
|
||||||
|
|
||||||
std::vector<uint8_t> allPixels(TEX_SIZE * TEX_SIZE * 4 * NUM_MATERIALS);
|
// Material texture files (RGBA PNG: RGB=albedo, A=heightmap)
|
||||||
|
// CWD is the project root (see CLAUDE.md), assets/voxel/ is the source directory
|
||||||
|
static const char* texturePaths[NUM_MATERIALS] = {
|
||||||
|
"assets/voxel/grass_albedo.png", // 1: Grass
|
||||||
|
"assets/voxel/dirt_albedo.png", // 2: Dirt
|
||||||
|
"assets/voxel/stone_albedo.png", // 3: Stone (blocky)
|
||||||
|
"assets/voxel/sand_albedo.png", // 4: Sand
|
||||||
|
"assets/voxel/snow_albedo.png", // 5: Snow
|
||||||
|
"assets/voxel/smoothstone_albedo.png", // 6: SmoothStone
|
||||||
|
};
|
||||||
|
|
||||||
struct MatColor {
|
std::vector<uint8_t> allPixels(TEX_SIZE * TEX_SIZE * 4 * NUM_MATERIALS, 128);
|
||||||
uint8_t r0,g0,b0, r1,g1,b1;
|
|
||||||
uint32_t seed;
|
|
||||||
float heightFreq; // heightmap noise frequency
|
|
||||||
float heightContrast; // heightmap contrast (higher = more defined peaks)
|
|
||||||
};
|
|
||||||
MatColor colors[NUM_MATERIALS] = {
|
|
||||||
{ 50, 140, 35, 80, 180, 55, 101, 1.5f, 0.8f }, // 1: Grass: natural rich green
|
|
||||||
{ 100, 70, 40, 140, 100, 60, 202, 0.8f, 0.6f }, // 2: Dirt: smooth mounds
|
|
||||||
{ 80, 80, 90, 120, 120, 130, 303, 2.5f, 0.5f }, // 3: Stone (blocky): darker blue-gray
|
|
||||||
{ 220, 200, 130, 245, 230, 160, 404, 3.0f, 0.4f }, // 4: Sand: warmer yellow, fine
|
|
||||||
{ 220, 225, 230, 245, 248, 252, 505, 1.0f, 0.5f }, // 5: Snow: smooth, soft
|
|
||||||
{ 100, 100, 110, 145, 145, 155, 606, 2.0f, 0.6f }, // 6: SmoothStone: lighter blue-gray, distinct from blocky stone
|
|
||||||
};
|
|
||||||
|
|
||||||
for (int i = 0; i < NUM_MATERIALS; i++) {
|
for (int i = 0; i < NUM_MATERIALS; i++) {
|
||||||
auto& c = colors[i];
|
std::vector<uint8_t> fileData;
|
||||||
generateNoiseTexture(
|
if (!wi::helper::FileRead(texturePaths[i], fileData)) {
|
||||||
allPixels.data() + i * TEX_SIZE * TEX_SIZE * 4,
|
wi::backlog::post(std::string("VoxelRenderer: failed to read ") + texturePaths[i],
|
||||||
TEX_SIZE, TEX_SIZE,
|
wi::backlog::LogLevel::Warning);
|
||||||
c.r0, c.g0, c.b0, c.r1, c.g1, c.b1, c.seed,
|
continue;
|
||||||
c.heightFreq, c.heightContrast
|
}
|
||||||
|
|
||||||
|
int w, h, channels;
|
||||||
|
uint8_t* pixels = stbi_load_from_memory(
|
||||||
|
fileData.data(), (int)fileData.size(),
|
||||||
|
&w, &h, &channels, 4 // force RGBA
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!pixels) {
|
||||||
|
wi::backlog::post(std::string("VoxelRenderer: failed to decode ") + texturePaths[i],
|
||||||
|
wi::backlog::LogLevel::Warning);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy into the texture array slice, resizing if needed
|
||||||
|
uint8_t* dst = allPixels.data() + i * TEX_SIZE * TEX_SIZE * 4;
|
||||||
|
if (w == TEX_SIZE && h == TEX_SIZE) {
|
||||||
|
memcpy(dst, pixels, TEX_SIZE * TEX_SIZE * 4);
|
||||||
|
} else {
|
||||||
|
// Nearest-neighbor resize (textures should already be 512x512 from prepare_textures.py)
|
||||||
|
for (int dy = 0; dy < TEX_SIZE; dy++) {
|
||||||
|
int sy = dy * h / TEX_SIZE;
|
||||||
|
for (int dx = 0; dx < TEX_SIZE; dx++) {
|
||||||
|
int sx = dx * w / TEX_SIZE;
|
||||||
|
int srcIdx = (sy * w + sx) * 4;
|
||||||
|
int dstIdx = (dy * TEX_SIZE + dx) * 4;
|
||||||
|
dst[dstIdx + 0] = pixels[srcIdx + 0];
|
||||||
|
dst[dstIdx + 1] = pixels[srcIdx + 1];
|
||||||
|
dst[dstIdx + 2] = pixels[srcIdx + 2];
|
||||||
|
dst[dstIdx + 3] = pixels[srcIdx + 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stbi_image_free(pixels);
|
||||||
|
wi::backlog::post(std::string("VoxelRenderer: loaded ") + texturePaths[i] +
|
||||||
|
" (" + std::to_string(w) + "x" + std::to_string(h) + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
TextureDesc texDesc;
|
TextureDesc texDesc;
|
||||||
|
|
@ -307,6 +307,63 @@ void VoxelRenderer::generateTextures() {
|
||||||
}
|
}
|
||||||
|
|
||||||
device_->CreateTexture(&texDesc, subData.data(), &textureArray_);
|
device_->CreateTexture(&texDesc, subData.data(), &textureArray_);
|
||||||
|
|
||||||
|
// ── Normal map texture array (RGB, t7) ──
|
||||||
|
static const char* normalPaths[NUM_MATERIALS] = {
|
||||||
|
"assets/voxel/grass_normal.png",
|
||||||
|
"assets/voxel/dirt_normal.png",
|
||||||
|
"assets/voxel/stone_normal.png",
|
||||||
|
"assets/voxel/sand_normal.png",
|
||||||
|
"assets/voxel/snow_normal.png",
|
||||||
|
"assets/voxel/smoothstone_normal.png",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default normal = (128,128,255) = tangent-space flat normal (0,0,1)
|
||||||
|
std::vector<uint8_t> normalPixels(TEX_SIZE * TEX_SIZE * 4 * NUM_MATERIALS);
|
||||||
|
for (size_t j = 0; j < normalPixels.size(); j += 4) {
|
||||||
|
normalPixels[j + 0] = 128;
|
||||||
|
normalPixels[j + 1] = 128;
|
||||||
|
normalPixels[j + 2] = 255;
|
||||||
|
normalPixels[j + 3] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < NUM_MATERIALS; i++) {
|
||||||
|
std::vector<uint8_t> fileData;
|
||||||
|
if (!wi::helper::FileRead(normalPaths[i], fileData))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int w, h, channels;
|
||||||
|
uint8_t* pixels = stbi_load_from_memory(
|
||||||
|
fileData.data(), (int)fileData.size(), &w, &h, &channels, 3
|
||||||
|
);
|
||||||
|
if (!pixels) continue;
|
||||||
|
|
||||||
|
uint8_t* dst = normalPixels.data() + i * TEX_SIZE * TEX_SIZE * 4;
|
||||||
|
for (int dy = 0; dy < TEX_SIZE; dy++) {
|
||||||
|
int sy = (w == TEX_SIZE) ? dy : dy * h / TEX_SIZE;
|
||||||
|
for (int dx = 0; dx < TEX_SIZE; dx++) {
|
||||||
|
int sx = (w == TEX_SIZE) ? dx : dx * w / TEX_SIZE;
|
||||||
|
int srcIdx = (sy * w + sx) * 3;
|
||||||
|
int dstIdx = (dy * TEX_SIZE + dx) * 4;
|
||||||
|
dst[dstIdx + 0] = pixels[srcIdx + 0]; // R
|
||||||
|
dst[dstIdx + 1] = pixels[srcIdx + 1]; // G
|
||||||
|
dst[dstIdx + 2] = pixels[srcIdx + 2]; // B
|
||||||
|
dst[dstIdx + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stbi_image_free(pixels);
|
||||||
|
wi::backlog::post(std::string("VoxelRenderer: loaded normal ") + normalPaths[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reuse same texDesc but for normal array
|
||||||
|
std::vector<SubresourceData> normalSub(NUM_MATERIALS);
|
||||||
|
for (int i = 0; i < NUM_MATERIALS; i++) {
|
||||||
|
normalSub[i].data_ptr = normalPixels.data() + i * TEX_SIZE * TEX_SIZE * 4;
|
||||||
|
normalSub[i].row_pitch = TEX_SIZE * 4;
|
||||||
|
normalSub[i].slice_pitch = TEX_SIZE * TEX_SIZE * 4;
|
||||||
|
}
|
||||||
|
device_->CreateTexture(&texDesc, normalSub.data(), &normalArray_);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Mega-buffer rebuild ─────────────────────────────────────────
|
// ── Mega-buffer rebuild ─────────────────────────────────────────
|
||||||
|
|
@ -708,15 +765,17 @@ void VoxelRenderer::render(
|
||||||
XMStoreFloat4x4(&cb.inverseViewProjection, invVP);
|
XMStoreFloat4x4(&cb.inverseViewProjection, invVP);
|
||||||
cb.prevViewProjection = rt_.prevViewProjection; // from last frame
|
cb.prevViewProjection = rt_.prevViewProjection; // from last frame
|
||||||
cb.cameraPosition = XMFLOAT4(camera.Eye.x, camera.Eye.y, camera.Eye.z, 1.0f);
|
cb.cameraPosition = XMFLOAT4(camera.Eye.x, camera.Eye.y, camera.Eye.z, 1.0f);
|
||||||
cb.sunDirection = XMFLOAT4(-0.7f, -0.4f, -0.3f, 0.0f); // lower sun = longer cast shadows
|
cb.sunDirection = sunDirection_;
|
||||||
cb.sunColor = XMFLOAT4(1.35f, 1.15f, 0.75f, 1.0f); // warm golden sun
|
cb.sunColor = XMFLOAT4(1.35f, 1.15f, 0.75f, 1.0f); // warm golden sun
|
||||||
cb.chunkSize = (float)CHUNK_SIZE;
|
cb.chunkSize = (float)CHUNK_SIZE;
|
||||||
cb.textureTiling = 0.25f;
|
cb.textureTiling = 0.25f;
|
||||||
cb.blendEnabled = 1.0f; // Phase 3: PS-based blending enabled in GPU mesh path
|
cb.blendEnabled = 1.0f; // Phase 3: PS-based blending enabled in GPU mesh path
|
||||||
cb.debugBlend = debugBlend_ ? 1.0f : 0.0f;
|
cb.debugBlend = debugBlend_ ? 1.0f : 0.0f;
|
||||||
cb.chunkCount = chunkCount_;
|
cb.chunkCount = chunkCount_;
|
||||||
cb.bleedMask = (1u << 1) | (1u << 2) | (1u << 4) | (1u << 5);
|
// bleedMask: bit N = material N can bleed onto neighbors
|
||||||
cb.resistBleedMask = (1u << 1);
|
// resistBleedMask: bit N = material N resists being bled onto
|
||||||
|
cb.bleedMask = (1u << 1) | (1u << 2) | (1u << 4) | (1u << 5) | (1u << 6); // no stone (3)
|
||||||
|
cb.resistBleedMask = (1u << 1); // grass resists bleed
|
||||||
cb.windTime = windTime_;
|
cb.windTime = windTime_;
|
||||||
// Stylized lighting (Phase 7) — Wonderbox-inspired
|
// Stylized lighting (Phase 7) — Wonderbox-inspired
|
||||||
cb.skyAmbient = XMFLOAT4(0.50f, 0.55f, 0.65f, 0.0f); // cool sky fill
|
cb.skyAmbient = XMFLOAT4(0.50f, 0.55f, 0.65f, 0.0f); // cool sky fill
|
||||||
|
|
@ -774,6 +833,7 @@ void VoxelRenderer::render(
|
||||||
dev->BindResource(&textureArray_, 1, cmd);
|
dev->BindResource(&textureArray_, 1, cmd);
|
||||||
dev->BindResource(&chunkInfoBuffer_, 2, cmd);
|
dev->BindResource(&chunkInfoBuffer_, 2, cmd);
|
||||||
dev->BindResource(&voxelDataBuffer_, 3, cmd); // Phase 3: voxel data for PS neighbor lookups
|
dev->BindResource(&voxelDataBuffer_, 3, cmd); // Phase 3: voxel data for PS neighbor lookups
|
||||||
|
dev->BindResource(&normalArray_, 7, cmd); // t7: normal maps
|
||||||
dev->BindSampler(&sampler_, 0, cmd);
|
dev->BindSampler(&sampler_, 0, cmd);
|
||||||
|
|
||||||
// GPU mesh mode: flags=2, MUST be after BindPipelineState
|
// GPU mesh mode: flags=2, MUST be after BindPipelineState
|
||||||
|
|
@ -1019,6 +1079,7 @@ void VoxelRenderer::renderTopings(
|
||||||
dev->BindResource(&textureArray_, 1, cmd);
|
dev->BindResource(&textureArray_, 1, cmd);
|
||||||
dev->BindResource(&topingVertexBuffer_, 4, cmd); // t4
|
dev->BindResource(&topingVertexBuffer_, 4, cmd); // t4
|
||||||
dev->BindResource(&topingInstanceBuf_.gpu, 5, cmd); // t5
|
dev->BindResource(&topingInstanceBuf_.gpu, 5, cmd); // t5
|
||||||
|
dev->BindResource(&normalArray_, 7, cmd); // t7: normal maps
|
||||||
dev->BindSampler(&sampler_, 0, cmd);
|
dev->BindSampler(&sampler_, 0, cmd);
|
||||||
|
|
||||||
// Reuse draw groups built in uploadTopingData (avoids redundant sort)
|
// Reuse draw groups built in uploadTopingData (avoids redundant sort)
|
||||||
|
|
@ -1185,6 +1246,7 @@ void VoxelRenderer::renderSmooth(
|
||||||
dev->BindResource(&chunkInfoBuffer_, 2, cmd); // t2: chunk info for PS voxel lookups
|
dev->BindResource(&chunkInfoBuffer_, 2, cmd); // t2: chunk info for PS voxel lookups
|
||||||
dev->BindResource(&voxelDataBuffer_, 3, cmd); // t3: voxel data for PS neighbor blending
|
dev->BindResource(&voxelDataBuffer_, 3, cmd); // t3: voxel data for PS neighbor blending
|
||||||
dev->BindResource(&smoothBuf, 6, cmd); // t6: smooth vertices (GPU or CPU buffer)
|
dev->BindResource(&smoothBuf, 6, cmd); // t6: smooth vertices (GPU or CPU buffer)
|
||||||
|
dev->BindResource(&normalArray_, 7, cmd); // t7: normal maps
|
||||||
dev->BindSampler(&sampler_, 0, cmd);
|
dev->BindSampler(&sampler_, 0, cmd);
|
||||||
|
|
||||||
// Push constants (unused by smooth VS, but must be valid 48 bytes)
|
// Push constants (unused by smooth VS, but must be valid 48 bytes)
|
||||||
|
|
@ -1437,9 +1499,25 @@ void VoxelRenderPath::Update(float dt) {
|
||||||
// In-app screenshot: saves voxelRT_ directly (immune to HDR/SDR mismatch)
|
// In-app screenshot: saves voxelRT_ directly (immune to HDR/SDR mismatch)
|
||||||
static int screenshotIdx = 0;
|
static int screenshotIdx = 0;
|
||||||
char fname[64];
|
char fname[64];
|
||||||
snprintf(fname, sizeof(fname), "bvle_screenshot_%03d.png", screenshotIdx++);
|
snprintf(fname, sizeof(fname), "bvle_screenshot_%03d", screenshotIdx++);
|
||||||
wi::helper::saveTextureToFile(voxelRT_, fname);
|
std::string pngName = std::string(fname) + ".png";
|
||||||
wi::backlog::post(std::string("Screenshot saved: ") + fname);
|
std::string logName = std::string(fname) + ".log";
|
||||||
|
wi::helper::saveTextureToFile(voxelRT_, pngName);
|
||||||
|
// Write companion .log with debug info
|
||||||
|
{
|
||||||
|
std::string log = buildDebugLog();
|
||||||
|
FILE* f = fopen(logName.c_str(), "w");
|
||||||
|
if (f) { fputs(log.c_str(), f); fclose(f); }
|
||||||
|
}
|
||||||
|
wi::backlog::post(std::string("Screenshot saved: ") + pngName + " + " + logName);
|
||||||
|
}
|
||||||
|
if (wi::input::Press(wi::input::KEYBOARD_BUTTON_F7)) {
|
||||||
|
anim_.sunOrbit = !anim_.sunOrbit;
|
||||||
|
wi::backlog::post(anim_.sunOrbit ? "Sun orbit: ON (10s cycle)" : "Sun orbit: OFF");
|
||||||
|
}
|
||||||
|
if (wi::input::Press(wi::input::KEYBOARD_BUTTON_F8)) {
|
||||||
|
anim_.showCrosshair = !anim_.showCrosshair;
|
||||||
|
wi::backlog::post(anim_.showCrosshair ? "Crosshair + debug: ON" : "Crosshair + debug: OFF");
|
||||||
}
|
}
|
||||||
if (wi::input::Press(wi::input::KEYBOARD_BUTTON_F5)) {
|
if (wi::input::Press(wi::input::KEYBOARD_BUTTON_F5)) {
|
||||||
if (!renderer.rt_.isShadowsEnabled()) {
|
if (!renderer.rt_.isShadowsEnabled()) {
|
||||||
|
|
@ -1461,6 +1539,20 @@ void VoxelRenderPath::Update(float dt) {
|
||||||
anim_.windTime += dt;
|
anim_.windTime += dt;
|
||||||
renderer.windTime_ = anim_.windTime;
|
renderer.windTime_ = anim_.windTime;
|
||||||
|
|
||||||
|
// Sun direction: fixed or orbiting (F7)
|
||||||
|
if (anim_.sunOrbit) {
|
||||||
|
float t = anim_.windTime;
|
||||||
|
constexpr float CYCLE = 10.0f; // 10 seconds per full orbit
|
||||||
|
float angle = t * (2.0f * 3.14159265f / CYCLE);
|
||||||
|
float altitude = 0.3f + 0.25f * std::sin(angle * 0.5f); // oscillates 0.05..0.55
|
||||||
|
float x = -std::cos(angle);
|
||||||
|
float z = -std::sin(angle);
|
||||||
|
float len = std::sqrt(x * x + altitude * altitude + z * z);
|
||||||
|
renderer.sunDirection_ = XMFLOAT4(x / len, -altitude / len, z / len, 0.0f);
|
||||||
|
} else {
|
||||||
|
renderer.sunDirection_ = XMFLOAT4(-0.7f, -0.4f, -0.3f, 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
// Animated terrain: regenerate at 30 Hz with time-shifted noise
|
// Animated terrain: regenerate at 30 Hz with time-shifted noise
|
||||||
if (anim_.tick(dt) && renderer.isInitialized()) {
|
if (anim_.tick(dt) && renderer.isInitialized()) {
|
||||||
// Prepare pack cache for fused regenerate+pack
|
// Prepare pack cache for fused regenerate+pack
|
||||||
|
|
@ -1840,10 +1932,102 @@ void VoxelRenderPath::Compose(CommandList cmd) const {
|
||||||
stats += "WASD+Space/Ctrl: move | Shift: fast | Right-click: capture mouse\n";
|
stats += "WASD+Space/Ctrl: move | Shift: fast | Right-click: capture mouse\n";
|
||||||
stats += "F2: console | F3: anim [" + std::string(anim_.terrainAnimated ? "ON" : "OFF")
|
stats += "F2: console | F3: anim [" + std::string(anim_.terrainAnimated ? "ON" : "OFF")
|
||||||
+ "] | F4: dbg [" + std::string(renderer.debugBlend_ ? "ON" : "OFF")
|
+ "] | F4: dbg [" + std::string(renderer.debugBlend_ ? "ON" : "OFF")
|
||||||
+ "] | F5: shd+ao [" + std::string(renderer.rt_.getShadowDebug() == 1 ? "SHD" : (renderer.rt_.getShadowDebug() == 2 ? "AO" : (renderer.isRTShadowsEnabled() ? "ON" : "OFF"))) + "]";
|
+ "] | F5: shd+ao [" + std::string(renderer.rt_.getShadowDebug() == 1 ? "SHD" : (renderer.rt_.getShadowDebug() == 2 ? "AO" : (renderer.isRTShadowsEnabled() ? "ON" : "OFF")))
|
||||||
|
+ "] | F7: sun [" + std::string(anim_.sunOrbit ? "ORBIT" : "FIXED")
|
||||||
|
+ "] | F8: xhair [" + std::string(anim_.showCrosshair ? "ON" : "OFF") + "]";
|
||||||
|
|
||||||
wi::font::Draw(stats, fp, cmd);
|
wi::font::Draw(stats, fp, cmd);
|
||||||
|
|
||||||
|
// ── Crosshair + face debug info (F8) ──
|
||||||
|
if (anim_.showCrosshair) {
|
||||||
|
float screenW = GetLogicalWidth();
|
||||||
|
float screenH = GetLogicalHeight();
|
||||||
|
float cx = screenW * 0.5f;
|
||||||
|
float cy = screenH * 0.5f;
|
||||||
|
|
||||||
|
// Draw crosshair lines using wi::texturehelper::getWhite() as solid 1x1 texture
|
||||||
|
{
|
||||||
|
const wi::graphics::Texture* whiteTex = wi::texturehelper::getWhite();
|
||||||
|
wi::image::Params cp;
|
||||||
|
cp.color = wi::Color(255, 255, 255, 200);
|
||||||
|
cp.blendFlag = wi::enums::BLENDMODE_ALPHA;
|
||||||
|
// Horizontal bar
|
||||||
|
cp.pos = XMFLOAT3(cx - 12, cy - 1, 0);
|
||||||
|
cp.siz = XMFLOAT2(24, 2);
|
||||||
|
wi::image::Draw(whiteTex, cp, cmd);
|
||||||
|
// Vertical bar
|
||||||
|
cp.pos = XMFLOAT3(cx - 1, cy - 12, 0);
|
||||||
|
cp.siz = XMFLOAT2(2, 24);
|
||||||
|
wi::image::Draw(whiteTex, cp, cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DDA voxel raycast from camera center → update cached crosshairHit_
|
||||||
|
{
|
||||||
|
float cosPitch = std::cos(camera_.pitch);
|
||||||
|
XMFLOAT3 rayDir(
|
||||||
|
std::sin(camera_.yaw) * cosPitch,
|
||||||
|
-std::sin(camera_.pitch),
|
||||||
|
std::cos(camera_.yaw) * cosPitch
|
||||||
|
);
|
||||||
|
XMFLOAT3 rayPos = camera_.pos;
|
||||||
|
|
||||||
|
int mapX = (int)std::floor(rayPos.x);
|
||||||
|
int mapY = (int)std::floor(rayPos.y);
|
||||||
|
int mapZ = (int)std::floor(rayPos.z);
|
||||||
|
int stepX = (rayDir.x >= 0) ? 1 : -1;
|
||||||
|
int stepY = (rayDir.y >= 0) ? 1 : -1;
|
||||||
|
int stepZ = (rayDir.z >= 0) ? 1 : -1;
|
||||||
|
|
||||||
|
float tDeltaX = (rayDir.x != 0.0f) ? std::abs(1.0f / rayDir.x) : 1e30f;
|
||||||
|
float tDeltaY = (rayDir.y != 0.0f) ? std::abs(1.0f / rayDir.y) : 1e30f;
|
||||||
|
float tDeltaZ = (rayDir.z != 0.0f) ? std::abs(1.0f / rayDir.z) : 1e30f;
|
||||||
|
|
||||||
|
float tMaxX = (rayDir.x >= 0) ? ((mapX + 1) - rayPos.x) * tDeltaX : (rayPos.x - mapX) * tDeltaX;
|
||||||
|
float tMaxY = (rayDir.y >= 0) ? ((mapY + 1) - rayPos.y) * tDeltaY : (rayPos.y - mapY) * tDeltaY;
|
||||||
|
float tMaxZ = (rayDir.z >= 0) ? ((mapZ + 1) - rayPos.z) * tDeltaZ : (rayPos.z - mapZ) * tDeltaZ;
|
||||||
|
|
||||||
|
crosshairHit_.valid = false;
|
||||||
|
constexpr int MAX_STEPS = 200;
|
||||||
|
constexpr float MAX_DIST = 200.0f;
|
||||||
|
|
||||||
|
for (int i = 0; i < MAX_STEPS; i++) {
|
||||||
|
int lastAxis = 0;
|
||||||
|
if (tMaxX < tMaxY) {
|
||||||
|
if (tMaxX < tMaxZ) { mapX += stepX; tMaxX += tDeltaX; lastAxis = 0; }
|
||||||
|
else { mapZ += stepZ; tMaxZ += tDeltaZ; lastAxis = 2; }
|
||||||
|
} else {
|
||||||
|
if (tMaxY < tMaxZ) { mapY += stepY; tMaxY += tDeltaY; lastAxis = 1; }
|
||||||
|
else { mapZ += stepZ; tMaxZ += tDeltaZ; lastAxis = 2; }
|
||||||
|
}
|
||||||
|
|
||||||
|
float dist = std::min({tMaxX - tDeltaX, tMaxY - tDeltaY, tMaxZ - tDeltaZ});
|
||||||
|
if (dist > MAX_DIST) break;
|
||||||
|
|
||||||
|
VoxelData v = world.getVoxel(mapX, mapY, mapZ);
|
||||||
|
if (!v.isEmpty()) {
|
||||||
|
crosshairHit_.valid = true;
|
||||||
|
crosshairHit_.x = mapX; crosshairHit_.y = mapY; crosshairHit_.z = mapZ;
|
||||||
|
crosshairHit_.matID = v.getMaterialID();
|
||||||
|
crosshairHit_.smooth = v.isSmooth();
|
||||||
|
if (lastAxis == 0) crosshairHit_.face = (stepX > 0) ? 1 : 0;
|
||||||
|
else if (lastAxis == 1) crosshairHit_.face = (stepY > 0) ? 3 : 2;
|
||||||
|
else crosshairHit_.face = (stepZ > 0) ? 5 : 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display debug info from buildDebugLog()
|
||||||
|
std::string dbg = buildDebugLog();
|
||||||
|
wi::font::Params dbgFp;
|
||||||
|
dbgFp.posX = 10;
|
||||||
|
dbgFp.posY = screenH - 180;
|
||||||
|
dbgFp.size = 18;
|
||||||
|
dbgFp.color = wi::Color(255, 255, 100, 230);
|
||||||
|
dbgFp.shadowColor = wi::Color(0, 0, 0, 200);
|
||||||
|
wi::font::Draw(dbg, dbgFp, cmd);
|
||||||
|
}
|
||||||
|
|
||||||
// Save compose end time for GPU wait measurement
|
// Save compose end time for GPU wait measurement
|
||||||
prof_.lastComposeEnd = std::chrono::high_resolution_clock::now();
|
prof_.lastComposeEnd = std::chrono::high_resolution_clock::now();
|
||||||
prof_.lastComposeEndValid = true;
|
prof_.lastComposeEndValid = true;
|
||||||
|
|
@ -1852,6 +2036,89 @@ void VoxelRenderPath::Compose(CommandList cmd) const {
|
||||||
if (trueFrameMs > 0.1f) prof_.trueFrame.add(trueFrameMs);
|
if (trueFrameMs > 0.1f) prof_.trueFrame.add(trueFrameMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string VoxelRenderPath::buildDebugLog() const {
|
||||||
|
static const char* matNames[] = { "air", "grass", "dirt", "stone", "sand", "snow", "smoothstone" };
|
||||||
|
static const char* faceNames[] = { "+X", "-X", "+Y", "-Y", "+Z", "-Z" };
|
||||||
|
static const char* faceNormals[] = { "(1,0,0)", "(-1,0,0)", "(0,1,0)", "(0,-1,0)", "(0,0,1)", "(0,0,-1)" };
|
||||||
|
|
||||||
|
char buf[512];
|
||||||
|
float cosPitch = std::cos(camera_.pitch);
|
||||||
|
float dirX = std::sin(camera_.yaw) * cosPitch;
|
||||||
|
float dirY = -std::sin(camera_.pitch);
|
||||||
|
float dirZ = std::cos(camera_.yaw) * cosPitch;
|
||||||
|
|
||||||
|
std::string log;
|
||||||
|
|
||||||
|
// Camera info
|
||||||
|
snprintf(buf, sizeof(buf),
|
||||||
|
"Cam: (%.1f, %.1f, %.1f) yaw=%.1f deg pitch=%.1f deg\n"
|
||||||
|
"Dir: (%.3f, %.3f, %.3f)\n"
|
||||||
|
"Sun: (%.3f, %.3f, %.3f)\n",
|
||||||
|
camera_.pos.x, camera_.pos.y, camera_.pos.z,
|
||||||
|
camera_.yaw * 57.2958f, camera_.pitch * 57.2958f,
|
||||||
|
dirX, dirY, dirZ,
|
||||||
|
renderer.sunDirection_.x, renderer.sunDirection_.y, renderer.sunDirection_.z);
|
||||||
|
log += buf;
|
||||||
|
|
||||||
|
// Crosshair target
|
||||||
|
if (crosshairHit_.valid) {
|
||||||
|
const char* mName = (crosshairHit_.matID < 7) ? matNames[crosshairHit_.matID] : "unknown";
|
||||||
|
int f = crosshairHit_.face;
|
||||||
|
const char* fName = (f >= 0 && f < 6) ? faceNames[f] : "?";
|
||||||
|
const char* fNorm = (f >= 0 && f < 6) ? faceNormals[f] : "?";
|
||||||
|
snprintf(buf, sizeof(buf),
|
||||||
|
"Target: (%d, %d, %d) mat=%d (%s) %s\n"
|
||||||
|
"Face: %s Normal: %s\n"
|
||||||
|
"NMap proj: %s-axis\n",
|
||||||
|
crosshairHit_.x, crosshairHit_.y, crosshairHit_.z,
|
||||||
|
crosshairHit_.matID, mName,
|
||||||
|
crosshairHit_.smooth ? "[smooth]" : "[blocky]",
|
||||||
|
fName, fNorm,
|
||||||
|
(f == 0 || f == 1) ? "X (UV=zy)" :
|
||||||
|
(f == 2 || f == 3) ? "Y (UV=xz, GL-flip)" :
|
||||||
|
"Z (UV=xy)");
|
||||||
|
log += buf;
|
||||||
|
} else {
|
||||||
|
log += "Target: none (sky)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug tool states
|
||||||
|
snprintf(buf, sizeof(buf),
|
||||||
|
"--- Debug states ---\n"
|
||||||
|
"FPS: %.1f (%.2f ms)\n"
|
||||||
|
"Chunks: %u/%u Quads: %u GPU Mesh: %u\n"
|
||||||
|
"Smooth verts: %u Toping instances: %zu\n"
|
||||||
|
"Animation: %s | Blend debug: %s | Sun orbit: %s | Crosshair: %s\n"
|
||||||
|
"RT available: %s | RT shadows+AO: %s | RT debug: %s\n"
|
||||||
|
"Debug face mode: %s | Debug smooth: %s\n",
|
||||||
|
smoothFps_, lastDt_ * 1000.0f,
|
||||||
|
renderer.getVisibleChunks(), renderer.getChunkCount(),
|
||||||
|
renderer.getTotalQuads(), renderer.getGpuMeshQuadCount(),
|
||||||
|
renderer.getSmoothVertexCount(), topingSystem.getInstanceCount(),
|
||||||
|
anim_.terrainAnimated ? "ON" : "OFF",
|
||||||
|
renderer.debugBlend_ ? "ON" : "OFF",
|
||||||
|
anim_.sunOrbit ? "ORBIT" : "FIXED",
|
||||||
|
anim_.showCrosshair ? "ON" : "OFF",
|
||||||
|
renderer.isRTAvailable() ? "yes" : "no",
|
||||||
|
renderer.isRTShadowsEnabled() ? "ON" : "OFF",
|
||||||
|
renderer.rt_.getShadowDebug() == 1 ? "SHADOWS" :
|
||||||
|
(renderer.rt_.getShadowDebug() == 2 ? "AO" : "OFF"),
|
||||||
|
debugMode ? "ON" : "OFF",
|
||||||
|
debugSmooth ? "ON" : "OFF");
|
||||||
|
log += buf;
|
||||||
|
|
||||||
|
if (renderer.isRTAvailable() && renderer.isRTReady()) {
|
||||||
|
snprintf(buf, sizeof(buf),
|
||||||
|
"RT tris: blocky=%u smooth=%u topings=%u\n",
|
||||||
|
renderer.getRTBlockyTriCount(),
|
||||||
|
renderer.getRTSmoothTriCount(),
|
||||||
|
renderer.getRTTopingTriCount());
|
||||||
|
log += buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
|
||||||
void VoxelRenderPath::resetAOHistory() {
|
void VoxelRenderPath::resetAOHistory() {
|
||||||
renderer.rt_.aoHistoryValid = false;
|
renderer.rt_.aoHistoryValid = false;
|
||||||
renderer.rt_.frameCounter = 0;
|
renderer.rt_.frameCounter = 0;
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,8 @@ public:
|
||||||
const wi::graphics::Texture& normalTarget
|
const wi::graphics::Texture& normalTarget
|
||||||
) const;
|
) const;
|
||||||
|
|
||||||
// Generate procedural textures for materials
|
// Load material textures from PNG files (RGB=albedo, A=heightmap)
|
||||||
void generateTextures();
|
void loadTextures();
|
||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
uint32_t getTotalQuads() const { return totalQuads_; }
|
uint32_t getTotalQuads() const { return totalQuads_; }
|
||||||
|
|
@ -64,6 +64,7 @@ public:
|
||||||
bool debugFaceColors_ = false;
|
bool debugFaceColors_ = false;
|
||||||
bool debugBlend_ = false;
|
bool debugBlend_ = false;
|
||||||
float windTime_ = 0.0f; // set by VoxelRenderPath::Update each frame
|
float windTime_ = 0.0f; // set by VoxelRenderPath::Update each frame
|
||||||
|
XMFLOAT4 sunDirection_ = { -0.7f, -0.4f, -0.3f, 0.0f }; // set by VoxelRenderPath::Update
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void createPipeline();
|
void createPipeline();
|
||||||
|
|
@ -119,8 +120,9 @@ private:
|
||||||
mutable uint32_t smoothDrawCalls_ = 0;
|
mutable uint32_t smoothDrawCalls_ = 0;
|
||||||
bool smoothDirty_ = true;
|
bool smoothDirty_ = true;
|
||||||
|
|
||||||
// Texture array for materials (256x256, 5 layers for prototype)
|
// Texture arrays for materials (512x512, 6 layers each)
|
||||||
wi::graphics::Texture textureArray_;
|
wi::graphics::Texture textureArray_; // RGBA: RGB=albedo, A=heightmap (t1)
|
||||||
|
wi::graphics::Texture normalArray_; // RGB: tangent-space normal map (t7)
|
||||||
wi::graphics::Sampler sampler_;
|
wi::graphics::Sampler sampler_;
|
||||||
|
|
||||||
// ── Mega-buffer architecture (Phase 2) ──────────────────────
|
// ── Mega-buffer architecture (Phase 2) ──────────────────────
|
||||||
|
|
@ -299,6 +301,8 @@ struct CameraController {
|
||||||
struct AnimationState {
|
struct AnimationState {
|
||||||
float windTime = 0.0f; // continuous, always running
|
float windTime = 0.0f; // continuous, always running
|
||||||
bool terrainAnimated = false; // toggled with F3
|
bool terrainAnimated = false; // toggled with F3
|
||||||
|
bool sunOrbit = false; // toggled with F7: sun orbits in ~10s cycle
|
||||||
|
bool showCrosshair = true; // toggled with F8: crosshair + face debug info
|
||||||
float time = 0.0f; // current animation time offset
|
float time = 0.0f; // current animation time offset
|
||||||
float accum = 0.0f; // accumulator for 30 Hz timer
|
float accum = 0.0f; // accumulator for 30 Hz timer
|
||||||
static constexpr float INTERVAL = 1.0f / 30.0f; // ~33.3ms = 30 Hz
|
static constexpr float INTERVAL = 1.0f / 30.0f; // ~33.3ms = 30 Hz
|
||||||
|
|
@ -396,6 +400,19 @@ private:
|
||||||
|
|
||||||
mutable uint32_t rtBuildSkipCounter_ = 0; // stagger BLAS builds during animation
|
mutable uint32_t rtBuildSkipCounter_ = 0; // stagger BLAS builds during animation
|
||||||
mutable bool rtWasEnabled_ = false; // saved RT state before animation
|
mutable bool rtWasEnabled_ = false; // saved RT state before animation
|
||||||
|
|
||||||
|
// Cached crosshair raycast result (updated each frame in Compose)
|
||||||
|
struct CrosshairHit {
|
||||||
|
bool valid = false;
|
||||||
|
int x = 0, y = 0, z = 0;
|
||||||
|
int face = -1; // 0=+X,1=-X,2=+Y,3=-Y,4=+Z,5=-Z
|
||||||
|
uint8_t matID = 0;
|
||||||
|
bool smooth = false;
|
||||||
|
};
|
||||||
|
mutable CrosshairHit crosshairHit_;
|
||||||
|
|
||||||
|
// Build a full debug log string (used by HUD overlay and screenshot .log)
|
||||||
|
std::string buildDebugLog() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace voxel
|
} // namespace voxel
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,8 @@ void VoxelWorld::generateChunk(Chunk& chunk, float timeOffset) {
|
||||||
surfaceMat = 5; // Snow (smooth)
|
surfaceMat = 5; // Snow (smooth)
|
||||||
surfaceSmooth = true;
|
surfaceSmooth = true;
|
||||||
} else {
|
} else {
|
||||||
surfaceMat = 2; // Dirt
|
surfaceMat = 2; // Dirt (smooth)
|
||||||
|
surfaceSmooth = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache for future animation frames
|
// Cache for future animation frames
|
||||||
|
|
|
||||||
125
tools/prepare_textures.py
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
"""
|
||||||
|
Prepare voxel textures from FreeStylized.com ZIPs.
|
||||||
|
Outputs per material:
|
||||||
|
- *_albedo.png : RGBA (RGB=albedo, A=heightmap)
|
||||||
|
- *_normal.png : RGB normal map (OpenGL convention, Y-up)
|
||||||
|
"""
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
from PIL import Image, ImageEnhance
|
||||||
|
|
||||||
|
# (zip_name, color_pattern, height_pattern, normal_pattern, brightness_factor)
|
||||||
|
# brightness_factor: <1 = darken, >1 = brighten, 1.0 = unchanged
|
||||||
|
MATERIALS = [
|
||||||
|
("grass_01_1k", "color", "height", "normal_gl", 1.0),
|
||||||
|
("ground_02_1k", "color", "height", "normal_gl", 0.75), # dirt: darkened
|
||||||
|
("ground_stones_01_1k", "baseColor", "height", "normal_gl", 1.0),
|
||||||
|
("sand_01_1k", "color", "height", "normal_gl", 1.0),
|
||||||
|
("snow_01_1k", "color", "height", "normal_gl", 1.0),
|
||||||
|
("rock_01_1k", "color", "height", "normal_gl", 1.0),
|
||||||
|
]
|
||||||
|
|
||||||
|
OUTPUT_NAMES = [
|
||||||
|
"grass",
|
||||||
|
"dirt",
|
||||||
|
"stone",
|
||||||
|
"sand",
|
||||||
|
"snow",
|
||||||
|
"smoothstone",
|
||||||
|
]
|
||||||
|
|
||||||
|
TARGET_SIZE = 512
|
||||||
|
RAW_DIR = os.path.join(os.path.dirname(__file__), "..", "assets", "raw")
|
||||||
|
OUT_DIR = os.path.join(os.path.dirname(__file__), "..", "assets", "voxel")
|
||||||
|
|
||||||
|
|
||||||
|
def find_file_in_zip(zf, pattern):
|
||||||
|
"""Find a file in the zip matching a pattern substring."""
|
||||||
|
for name in zf.namelist():
|
||||||
|
basename = os.path.basename(name).lower()
|
||||||
|
if pattern.lower() in basename and basename.endswith(".png"):
|
||||||
|
return name
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def load_image_from_zip(zf, filename, mode="RGB"):
|
||||||
|
data = zf.read(filename)
|
||||||
|
img = Image.open(io.BytesIO(data))
|
||||||
|
# Handle 16-bit heightmaps: Pillow's .convert("L") on I;16 images
|
||||||
|
# doesn't scale properly. We must manually scale 0-65535 → 0-255.
|
||||||
|
if img.mode in ("I;16", "I") and mode == "L":
|
||||||
|
# Convert to 32-bit int first, then scale down
|
||||||
|
img = img.convert("I")
|
||||||
|
img = img.point(lambda v: v / 256)
|
||||||
|
return img.convert("L")
|
||||||
|
return img.convert(mode)
|
||||||
|
|
||||||
|
|
||||||
|
def process_material(zip_path, color_pat, height_pat, normal_pat, brightness, out_name):
|
||||||
|
with zipfile.ZipFile(zip_path, "r") as zf:
|
||||||
|
color_file = find_file_in_zip(zf, color_pat)
|
||||||
|
height_file = find_file_in_zip(zf, height_pat)
|
||||||
|
normal_file = find_file_in_zip(zf, normal_pat)
|
||||||
|
|
||||||
|
if not color_file:
|
||||||
|
print(f" ERROR: no color file matching '{color_pat}' in {zip_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ── Albedo + Heightmap → RGBA ──
|
||||||
|
color_img = load_image_from_zip(zf, color_file, "RGB")
|
||||||
|
|
||||||
|
if brightness != 1.0:
|
||||||
|
color_img = ImageEnhance.Brightness(color_img).enhance(brightness)
|
||||||
|
|
||||||
|
if height_file:
|
||||||
|
height_img = load_image_from_zip(zf, height_file, "L")
|
||||||
|
else:
|
||||||
|
print(f" WARNING: no height map, deriving from luminance")
|
||||||
|
height_img = color_img.convert("L")
|
||||||
|
|
||||||
|
color_img = color_img.resize((TARGET_SIZE, TARGET_SIZE), Image.LANCZOS)
|
||||||
|
height_img = height_img.resize((TARGET_SIZE, TARGET_SIZE), Image.LANCZOS)
|
||||||
|
|
||||||
|
r, g, b = color_img.split()
|
||||||
|
rgba = Image.merge("RGBA", (r, g, b, height_img))
|
||||||
|
|
||||||
|
albedo_path = os.path.join(OUT_DIR, f"{out_name}_albedo.png")
|
||||||
|
rgba.save(albedo_path, "PNG")
|
||||||
|
print(f" OK: {out_name}_albedo.png ({TARGET_SIZE}x{TARGET_SIZE})")
|
||||||
|
|
||||||
|
# ── Normal map → RGB ──
|
||||||
|
if normal_file:
|
||||||
|
normal_img = load_image_from_zip(zf, normal_file, "RGB")
|
||||||
|
normal_img = normal_img.resize((TARGET_SIZE, TARGET_SIZE), Image.LANCZOS)
|
||||||
|
normal_path = os.path.join(OUT_DIR, f"{out_name}_normal.png")
|
||||||
|
normal_img.save(normal_path, "PNG")
|
||||||
|
print(f" OK: {out_name}_normal.png ({TARGET_SIZE}x{TARGET_SIZE})")
|
||||||
|
else:
|
||||||
|
print(f" WARNING: no normal map found")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
os.makedirs(OUT_DIR, exist_ok=True)
|
||||||
|
print(f"Output directory: {os.path.abspath(OUT_DIR)}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
success = 0
|
||||||
|
for i, (zip_name, color_pat, height_pat, normal_pat, brightness) in enumerate(MATERIALS):
|
||||||
|
zip_path = os.path.join(RAW_DIR, zip_name + ".zip")
|
||||||
|
print(f"[{i+1}/6] {OUTPUT_NAMES[i]} <- {zip_name}.zip")
|
||||||
|
|
||||||
|
if not os.path.exists(zip_path):
|
||||||
|
print(f" ERROR: {zip_path} not found")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if process_material(zip_path, color_pat, height_pat, normal_pat, brightness, OUTPUT_NAMES[i]):
|
||||||
|
success += 1
|
||||||
|
|
||||||
|
print(f"\nDone: {success}/6 materials generated in {os.path.abspath(OUT_DIR)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||