Phase 4.2: GPU toping rendering pipeline + winding/lighting fixes
Add instanced rendering for toping bevels: dedicated shaders (voxelTopingVS/PS), PSO, GPU buffers (t4 vertices, t5 instances), per-group DrawInstanced in a separate render pass with LoadOp::LOAD. Fix inverted face winding (emitTri auto-winding condition flipped for CW front-facing), slope normals (use inward direction not outward), and PS lighting (negate sunDirection like voxelPS). Update CLAUDE.md with Phase 4.1/4.2 documentation.
This commit is contained in:
parent
9e777d653b
commit
bc29a02c35
7 changed files with 414 additions and 62 deletions
45
CLAUDE.md
45
CLAUDE.md
|
|
@ -18,7 +18,8 @@ bvle-voxels/
|
||||||
│ │ ├── VoxelTypes.h # Types fondamentaux (VoxelData, PackedQuad, MaterialDesc, ChunkPos)
|
│ │ ├── VoxelTypes.h # Types fondamentaux (VoxelData, PackedQuad, MaterialDesc, ChunkPos)
|
||||||
│ │ ├── VoxelWorld.h/.cpp # Monde voxel (hashmap de chunks, génération procédurale)
|
│ │ ├── VoxelWorld.h/.cpp # Monde voxel (hashmap de chunks, génération procédurale)
|
||||||
│ │ ├── VoxelMesher.h/.cpp # Binary Greedy Mesher CPU
|
│ │ ├── VoxelMesher.h/.cpp # Binary Greedy Mesher CPU
|
||||||
│ │ └── VoxelRenderer.h/.cpp# Renderer + VoxelRenderPath (sous-classe RenderPath3D)
|
│ │ ├── VoxelRenderer.h/.cpp# Renderer + VoxelRenderPath (sous-classe RenderPath3D)
|
||||||
|
│ │ └── TopingSystem.h/.cpp # Système de topings (biseaux décoratifs sur faces +Y)
|
||||||
│ └── app/
|
│ └── app/
|
||||||
│ └── main.cpp # Point d'entrée Win32 + crash handler SEH
|
│ └── main.cpp # Point d'entrée Win32 + crash handler SEH
|
||||||
├── shaders/ # Sources HLSL des shaders voxel (copiés dans engine/ au build)
|
├── shaders/ # Sources HLSL des shaders voxel (copiés dans engine/ au build)
|
||||||
|
|
@ -26,7 +27,9 @@ bvle-voxels/
|
||||||
│ ├── voxelVS.hlsl # Vertex shader (vertex pulling, triple-mode: CPU/MDI/GPU mesh)
|
│ ├── voxelVS.hlsl # Vertex shader (vertex pulling, triple-mode: CPU/MDI/GPU mesh)
|
||||||
│ ├── voxelPS.hlsl # Pixel shader (triplanar + lighting)
|
│ ├── voxelPS.hlsl # Pixel shader (triplanar + lighting)
|
||||||
│ ├── voxelCullCS.hlsl # Compute shader frustum+backface cull (Phase 2.3)
|
│ ├── voxelCullCS.hlsl # Compute shader frustum+backface cull (Phase 2.3)
|
||||||
│ └── voxelMeshCS.hlsl # Compute shader GPU mesher 1×1 (Phase 2.4-2.5)
|
│ ├── voxelMeshCS.hlsl # Compute shader GPU mesher 1×1 (Phase 2.4-2.5)
|
||||||
|
│ ├── voxelTopingVS.hlsl # Vertex shader topings (instanced vertex pulling, t4/t5)
|
||||||
|
│ └── voxelTopingPS.hlsl # Pixel shader topings (triplanar + directional lighting)
|
||||||
└── CLAUDE.md
|
└── CLAUDE.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -391,12 +394,40 @@ Approche **PS-based** : le pixel shader lit directement les données voxel (pas
|
||||||
- **Mode debug** (F4) : visualise les zones de blend (rouge=U, bleu=V, vert=pas de blend, rouge vif=data mismatch)
|
- **Mode debug** (F4) : visualise les zones de blend (rouge=U, bleu=V, vert=pas de blend, rouge vif=data mismatch)
|
||||||
- **Fonctionne uniquement en GPU mesh path** (1×1 quads) ; CPU/MDI paths ont `blendEnabled=0`
|
- **Fonctionne uniquement en GPU mesh path** (1×1 quads) ; CPU/MDI paths ont `blendEnabled=0`
|
||||||
|
|
||||||
### Phase 4 - Toping [A FAIRE]
|
### Phase 4 - Toping [EN COURS]
|
||||||
|
|
||||||
- TopingSystem avec bitmask d'adjacence 4 bits (16 variantes)
|
Système de biseaux décoratifs (« topings ») sur les faces +Y exposées pour adoucir les transitions entre blocs.
|
||||||
- Instance buffer GPU par chunk
|
|
||||||
- Instanced draw dans le G-buffer
|
#### Phase 4.1 - Infrastructure TopingSystem [FAIT]
|
||||||
- 2-3 types de test (rebord de pierre, bordure d'herbe)
|
|
||||||
|
- **TopingSystem** (`TopingSystem.h/.cpp`) : data structures + mesh generation + instance collection
|
||||||
|
- **4-bit adjacency bitmask** : pour chaque face +Y exposée, vérifie 4 voisins cardinaux (±X, ±Z) pour même matériau avec +Y exposée → 16 variantes
|
||||||
|
- **Wedge cross-section** : chaque bord ouvert génère un mur vertical (outer wall) + une pente (slope) depuis le peak (y=1+h au bord) vers l'intérieur (y=1, w unités vers le centre)
|
||||||
|
- **Priority-based adjacency** : `TopingDef.priority` détermine quel toping cède aux frontières de matériaux. Grass (priority=1) génère des biseaux par-dessus stone (priority=0)
|
||||||
|
- **2 types de test** :
|
||||||
|
- Stone : priority=0, h=0.06, w=0.12, 1 segment (biseau simple)
|
||||||
|
- Grass : priority=1, h=0.12, w=0.18, 4 segments (bosses sinusoïdales)
|
||||||
|
- **16 variantes × 2 types** : 1920 vertices de mesh procédural, ~191K instances pour ~170 chunks
|
||||||
|
|
||||||
|
#### Phase 4.2 - Rendu GPU [FAIT]
|
||||||
|
|
||||||
|
- **Shaders dédiés** : `voxelTopingVS.hlsl` (vertex pulling instancié) + `voxelTopingPS.hlsl` (triplanar + lighting directionnel)
|
||||||
|
- **Vertex pulling** : `StructuredBuffer<TopingVertex>` (t4) + `StructuredBuffer<float3>` (t5 instances)
|
||||||
|
- **Push constants** : `vertexOffset`, `instanceOffset`, `materialID` réutilisent les 3 premiers champs de b999
|
||||||
|
- **Per-group DrawInstanced** : instances triées par (type, variant), un `DrawInstanced` par groupe contigu
|
||||||
|
- **Render pass séparé** avec `LoadOp::LOAD` : les topings se rendent après les voxels, préservent le RT et depth existants
|
||||||
|
- **PSO** : même rasterizer/depth/blend que les voxels (`RSTYPE_FRONT`, `DSSTYPE_DEFAULT`, `BSTYPE_OPAQUE`)
|
||||||
|
- **Pièges résolus** :
|
||||||
|
- **Winding CW** : `emitTri()` auto-corrige le winding en comparant la normale géométrique (cross product) à la normale souhaitée. Si `dot(geom, desired) > 0` → swap B↔C pour CW (front-facing dans Wicked Engine)
|
||||||
|
- **Slope normal = inward + up** : la pente monte vers le bord extérieur, donc sa normale pointe vers l'intérieur et vers le haut. Utiliser `(e.ix, e.iz)` (direction inward), PAS `(e.nx, e.nz)` (direction outward)
|
||||||
|
- **sunDirection dans le PS** : `sunDirection` pointe vers le bas (direction de voyage de la lumière). Le PS doit faire `L = normalize(-sunDirection.xyz)` avant `dot(N, L)`, comme le voxel PS
|
||||||
|
|
||||||
|
#### Phase 4.3 - Polish et extensions [A FAIRE]
|
||||||
|
|
||||||
|
- Plus de types de topings (végétation, neige, etc.)
|
||||||
|
- LOD : supprimer les topings à distance
|
||||||
|
- Animation subtile (vent sur l'herbe)
|
||||||
|
- Optimisation : compute shader pour le instance collection
|
||||||
|
|
||||||
### Phase 5 - Rendu smooth [A FAIRE]
|
### Phase 5 - Rendu smooth [A FAIRE]
|
||||||
|
|
||||||
|
|
|
||||||
42
shaders/voxelTopingPS.hlsl
Normal file
42
shaders/voxelTopingPS.hlsl
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
// BVLE Voxels - Toping Pixel Shader (Phase 4.2)
|
||||||
|
// Simplified version of voxelPS: triplanar texture sampling + basic lighting.
|
||||||
|
// No height-based blending (topings are small decorative meshes).
|
||||||
|
|
||||||
|
#include "voxelCommon.hlsli"
|
||||||
|
|
||||||
|
Texture2DArray<float4> materialTextures : register(t1);
|
||||||
|
SamplerState texSampler : register(s0);
|
||||||
|
|
||||||
|
struct PSInput {
|
||||||
|
float4 position : SV_POSITION;
|
||||||
|
float3 worldPos : WORLDPOS;
|
||||||
|
float3 normal : NORMAL;
|
||||||
|
nointerpolation uint materialID : MATERIALID;
|
||||||
|
};
|
||||||
|
|
||||||
|
[RootSignature(VOXEL_ROOTSIG)]
|
||||||
|
float4 main(PSInput input) : SV_TARGET0 {
|
||||||
|
float3 N = normalize(input.normal);
|
||||||
|
float tiling = textureTiling;
|
||||||
|
|
||||||
|
// Material texture index (materialID 1-5 → array layer 0-4)
|
||||||
|
uint texIdx = clamp(input.materialID - 1u, 0u, 4u);
|
||||||
|
|
||||||
|
// Triplanar sampling (same as voxel PS)
|
||||||
|
float3 blend = abs(N);
|
||||||
|
blend = blend / (blend.x + blend.y + blend.z + 0.001);
|
||||||
|
|
||||||
|
float4 xSample = materialTextures.Sample(texSampler, float3(input.worldPos.yz * tiling, (float)texIdx));
|
||||||
|
float4 ySample = materialTextures.Sample(texSampler, float3(input.worldPos.xz * tiling, (float)texIdx));
|
||||||
|
float4 zSample = materialTextures.Sample(texSampler, float3(input.worldPos.xy * tiling, (float)texIdx));
|
||||||
|
|
||||||
|
float3 texColor = (xSample.rgb * blend.x + ySample.rgb * blend.y + zSample.rgb * blend.z);
|
||||||
|
|
||||||
|
// Basic directional lighting + ambient
|
||||||
|
float3 L = normalize(-sunDirection.xyz);
|
||||||
|
float NdotL = max(dot(N, L), 0.0);
|
||||||
|
float3 ambient = float3(0.15, 0.18, 0.25);
|
||||||
|
float3 lit = texColor * (sunColor.rgb * NdotL + ambient);
|
||||||
|
|
||||||
|
return float4(lit, 1.0);
|
||||||
|
}
|
||||||
53
shaders/voxelTopingVS.hlsl
Normal file
53
shaders/voxelTopingVS.hlsl
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
// BVLE Voxels - Toping Vertex Shader (Phase 4.2)
|
||||||
|
// Instanced vertex pulling: reads mesh vertices from t4, instance positions from t5.
|
||||||
|
// Push constants carry per-draw-group offsets.
|
||||||
|
|
||||||
|
#include "voxelCommon.hlsli"
|
||||||
|
|
||||||
|
// Toping mesh vertex (must match C++ TopingVertex, 24 bytes)
|
||||||
|
struct TopingVtx {
|
||||||
|
float3 position; // local to voxel [0,1]^3
|
||||||
|
float3 normal;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toping instance (just the world position, 12 bytes)
|
||||||
|
struct TopingInst {
|
||||||
|
float3 worldPos;
|
||||||
|
};
|
||||||
|
|
||||||
|
StructuredBuffer<TopingVtx> topingVertices : register(t4);
|
||||||
|
StructuredBuffer<TopingInst> topingInstances : register(t5);
|
||||||
|
|
||||||
|
// Push constants — repurposed fields for toping draws:
|
||||||
|
// chunkIndex → vertexOffset (into t4)
|
||||||
|
// quadOffset → instanceOffset (into t5)
|
||||||
|
// flags → materialID
|
||||||
|
struct TopingPush {
|
||||||
|
uint vertexOffset;
|
||||||
|
uint instanceOffset;
|
||||||
|
uint materialID;
|
||||||
|
uint pad0, pad1, pad2, pad3, pad4, pad5, pad6, pad7, pad8;
|
||||||
|
};
|
||||||
|
[[vk::push_constant]] ConstantBuffer<TopingPush> push : register(b999);
|
||||||
|
|
||||||
|
struct VSOutput {
|
||||||
|
float4 position : SV_POSITION;
|
||||||
|
float3 worldPos : WORLDPOS;
|
||||||
|
float3 normal : NORMAL;
|
||||||
|
nointerpolation uint materialID : MATERIALID;
|
||||||
|
};
|
||||||
|
|
||||||
|
[RootSignature(VOXEL_ROOTSIG)]
|
||||||
|
VSOutput main(uint vertexID : SV_VertexID, uint instanceID : SV_InstanceID) {
|
||||||
|
TopingVtx vtx = topingVertices[push.vertexOffset + vertexID];
|
||||||
|
TopingInst inst = topingInstances[push.instanceOffset + instanceID];
|
||||||
|
|
||||||
|
float3 worldPos = inst.worldPos + vtx.position;
|
||||||
|
|
||||||
|
VSOutput output;
|
||||||
|
output.position = mul(viewProjection, float4(worldPos, 1.0));
|
||||||
|
output.worldPos = worldPos;
|
||||||
|
output.normal = vtx.normal;
|
||||||
|
output.materialID = push.materialID;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
@ -38,16 +38,33 @@ static const EdgeDef kEdges[4] = {
|
||||||
{ 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,-1.0f }, // bit 3: -Z edge
|
{ 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,-1.0f }, // bit 3: -Z edge
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Helper: emit one triangle (3 vertices, shared normal) ───────
|
// ── Helper: emit one triangle with auto-corrected winding ───────
|
||||||
|
// Compares the geometric normal (from vertex cross product) to the
|
||||||
|
// desired shading normal. If they disagree, swaps B and C to flip
|
||||||
|
// the winding so the triangle is front-facing (CCW in Wicked Engine).
|
||||||
static void emitTri(std::vector<TopingVertex>& v,
|
static void emitTri(std::vector<TopingVertex>& v,
|
||||||
float ax, float ay, float az,
|
float ax, float ay, float az,
|
||||||
float bx, float by, float bz,
|
float bx, float by, float bz,
|
||||||
float cx, float cy, float cz,
|
float cx, float cy, float cz,
|
||||||
float nx, float ny, float nz)
|
float nx, float ny, float nz)
|
||||||
{
|
{
|
||||||
|
// Geometric normal = cross(AB, AC)
|
||||||
|
float abx = bx - ax, aby = by - ay, abz = bz - az;
|
||||||
|
float acx = cx - ax, acy = cy - ay, acz = cz - az;
|
||||||
|
float gnx = aby * acz - abz * acy;
|
||||||
|
float gny = abz * acx - abx * acz;
|
||||||
|
float gnz = abx * acy - aby * acx;
|
||||||
|
|
||||||
|
// If geometric normal disagrees with desired normal, swap B↔C
|
||||||
|
if (gnx * nx + gny * ny + gnz * nz > 0.0f) {
|
||||||
|
v.push_back({ ax, ay, az, nx, ny, nz });
|
||||||
|
v.push_back({ cx, cy, cz, nx, ny, nz });
|
||||||
|
v.push_back({ bx, by, bz, nx, ny, nz });
|
||||||
|
} else {
|
||||||
v.push_back({ ax, ay, az, nx, ny, nz });
|
v.push_back({ ax, ay, az, nx, ny, nz });
|
||||||
v.push_back({ bx, by, bz, nx, ny, nz });
|
v.push_back({ bx, by, bz, nx, ny, nz });
|
||||||
v.push_back({ cx, cy, cz, nx, ny, nz });
|
v.push_back({ cx, cy, cz, nx, ny, nz });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -65,10 +82,12 @@ void TopingSystem::registerDefs() {
|
||||||
|
|
||||||
// Type 0: Stone bevel — clean angular ridge along open edges
|
// Type 0: Stone bevel — clean angular ridge along open edges
|
||||||
// Applied to stone (materialID=3), face +Y
|
// Applied to stone (materialID=3), face +Y
|
||||||
|
// Priority 0 (low): yields to grass at material boundaries
|
||||||
{
|
{
|
||||||
TopingDef def{};
|
TopingDef def{};
|
||||||
def.materialID = 3;
|
def.materialID = 3;
|
||||||
def.face = FACE_POS_Y;
|
def.face = FACE_POS_Y;
|
||||||
|
def.priority = 0;
|
||||||
def.height = 0.06f; // subtle bevel
|
def.height = 0.06f; // subtle bevel
|
||||||
def.width = 0.12f;
|
def.width = 0.12f;
|
||||||
def.segments = 1; // single smooth segment per edge
|
def.segments = 1; // single smooth segment per edge
|
||||||
|
|
@ -77,10 +96,12 @@ void TopingSystem::registerDefs() {
|
||||||
|
|
||||||
// Type 1: Grass edge — organic bumpy tufts along open edges
|
// Type 1: Grass edge — organic bumpy tufts along open edges
|
||||||
// Applied to grass (materialID=1), face +Y
|
// Applied to grass (materialID=1), face +Y
|
||||||
|
// Priority 1 (high): generates bevels over stone at boundaries
|
||||||
{
|
{
|
||||||
TopingDef def{};
|
TopingDef def{};
|
||||||
def.materialID = 1;
|
def.materialID = 1;
|
||||||
def.face = FACE_POS_Y;
|
def.face = FACE_POS_Y;
|
||||||
|
def.priority = 1;
|
||||||
def.height = 0.12f; // taller, more visible
|
def.height = 0.12f; // taller, more visible
|
||||||
def.width = 0.18f;
|
def.width = 0.18f;
|
||||||
def.segments = 4; // subdivided for bumpy profile
|
def.segments = 4; // subdivided for bumpy profile
|
||||||
|
|
@ -159,45 +180,33 @@ void TopingSystem::generateVariant(TopingDef& def, uint8_t bitmask) {
|
||||||
|
|
||||||
// ── Outer wall face (vertical, facing outward) ──────
|
// ── Outer wall face (vertical, facing outward) ──────
|
||||||
// Normal points outward: (nx, 0, nz)
|
// Normal points outward: (nx, 0, nz)
|
||||||
// CW winding from outside: peak0→outerBot0→peak1, peak1→outerBot0→outerBot1
|
// Winding: emit both orderings — CW from outside view
|
||||||
|
// Empirically CW = front-facing in our engine (see CLAUDE.md)
|
||||||
emitTri(vertices_,
|
emitTri(vertices_,
|
||||||
x0, pk0y, z0, x0, 1.0f, z0, x1, pk1y, z1,
|
x0, pk0y, z0, x1, pk1y, z1, x0, 1.0f, z0,
|
||||||
e.nx, 0.0f, e.nz);
|
e.nx, 0.0f, e.nz);
|
||||||
emitTri(vertices_,
|
emitTri(vertices_,
|
||||||
x1, pk1y, z1, x0, 1.0f, z0, x1, 1.0f, z1,
|
x1, pk1y, z1, x1, 1.0f, z1, x0, 1.0f, z0,
|
||||||
e.nx, 0.0f, e.nz);
|
e.nx, 0.0f, e.nz);
|
||||||
|
|
||||||
// ── Slope face (from peak down to inner edge) ───────
|
// ── Slope face (from peak down to inner edge) ───────
|
||||||
// Compute normal via cross product of two edge vectors.
|
// Slope normal: the slope rises from inner (y=1) to peak (y=1+h) at the edge.
|
||||||
// v_along_slope = inner - peak = (w*ix, -h, w*iz) at midpoint
|
// Since it tilts outward, the normal points INWARD and UP.
|
||||||
// v_along_strip = strip direction = (dx/segs, 0, dz/segs)
|
// Normal = inward_dir * (h/L) + up * (w/L), where L = sqrt(h²+w²).
|
||||||
const float avgH = (h0 + h1) * 0.5f;
|
const float avgH = (h0 + h1) * 0.5f;
|
||||||
const float asx = w * e.ix;
|
float slopeLen = sqrtf(avgH * avgH + w * w);
|
||||||
const float asy = -avgH;
|
if (slopeLen < 0.0001f) slopeLen = 1.0f;
|
||||||
const float asz = w * e.iz;
|
float cnx = e.ix * (avgH / slopeLen);
|
||||||
const float adx = dx / segs;
|
float cny = w / slopeLen;
|
||||||
const float adz = dz / segs;
|
float cnz = e.iz * (avgH / slopeLen);
|
||||||
|
|
||||||
// normal = cross(v_along_strip, v_along_slope)
|
// Slope: peak → inner, strip direction
|
||||||
float cnx = /* 0*asz - adz*asy = */ adz * avgH;
|
// CW winding from normal direction
|
||||||
float cny = /* adz*asx - adx*asz = */ adz * asx - adx * asz;
|
|
||||||
float cnz = /* adx*asy - 0*asx = */ -adx * avgH;
|
|
||||||
|
|
||||||
float clen = sqrtf(cnx * cnx + cny * cny + cnz * cnz);
|
|
||||||
if (clen > 0.0001f) {
|
|
||||||
cnx /= clen; cny /= clen; cnz /= clen;
|
|
||||||
} else {
|
|
||||||
cnx = 0.0f; cny = 1.0f; cnz = 0.0f;
|
|
||||||
}
|
|
||||||
// Ensure normal points upward (slope is visible from above)
|
|
||||||
if (cny < 0.0f) { cnx = -cnx; cny = -cny; cnz = -cnz; }
|
|
||||||
|
|
||||||
// CW winding from above: peak0→peak1→inner0, inner0→peak1→inner1
|
|
||||||
emitTri(vertices_,
|
emitTri(vertices_,
|
||||||
x0, pk0y, z0, x1, pk1y, z1, in0x, 1.0f, in0z,
|
x0, pk0y, z0, in0x, 1.0f, in0z, x1, pk1y, z1,
|
||||||
cnx, cny, cnz);
|
cnx, cny, cnz);
|
||||||
emitTri(vertices_,
|
emitTri(vertices_,
|
||||||
in0x, 1.0f, in0z, x1, pk1y, z1, in1x, 1.0f, in1z,
|
x1, pk1y, z1, in0x, 1.0f, in0z, in1x, 1.0f, in1z,
|
||||||
cnx, cny, cnz);
|
cnx, cny, cnz);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -246,28 +255,29 @@ void TopingSystem::collectInstances(const VoxelWorld& world) {
|
||||||
if (!world.getVoxel(wx, wy + 1, wz).isEmpty()) continue;
|
if (!world.getVoxel(wx, wy + 1, wz).isEmpty()) continue;
|
||||||
|
|
||||||
// Face exposed. Compute 4-bit adjacency bitmask.
|
// Face exposed. Compute 4-bit adjacency bitmask.
|
||||||
// A neighbor contributes if: same material AND its +Y face is also exposed.
|
// A neighbor "connects" (bit SET → no bevel on that edge) if:
|
||||||
|
// - Same material AND its +Y face is also exposed (same toping type)
|
||||||
|
// - OR different material with a toping of HIGHER or EQUAL priority
|
||||||
|
// AND its +Y face is exposed (the dominant toping wins at the boundary)
|
||||||
uint8_t adj = 0;
|
uint8_t adj = 0;
|
||||||
|
const uint8_t myPriority = def.priority;
|
||||||
|
|
||||||
// bit 0: +X
|
auto checkNeighbor = [&](int nx, int nz) -> bool {
|
||||||
if (world.getVoxel(wx + 1, wy, wz).getMaterialID() == mat &&
|
uint8_t nMat = world.getVoxel(nx, wy, nz).getMaterialID();
|
||||||
world.getVoxel(wx + 1, wy + 1, wz).isEmpty())
|
if (nMat == 0) return false; // empty
|
||||||
adj |= 1;
|
if (!world.getVoxel(nx, wy + 1, nz).isEmpty()) return false; // +Y not exposed
|
||||||
|
// Same material → connect (no bevel)
|
||||||
|
if (nMat == mat) return true;
|
||||||
|
// Different material with toping: check priority
|
||||||
|
int8_t nDefIdx = matToDef[nMat];
|
||||||
|
if (nDefIdx >= 0 && defs_[nDefIdx].priority >= myPriority) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
// bit 1: -X
|
if (checkNeighbor(wx + 1, wz)) adj |= 1; // +X
|
||||||
if (world.getVoxel(wx - 1, wy, wz).getMaterialID() == mat &&
|
if (checkNeighbor(wx - 1, wz)) adj |= 2; // -X
|
||||||
world.getVoxel(wx - 1, wy + 1, wz).isEmpty())
|
if (checkNeighbor(wx, wz + 1)) adj |= 4; // +Z
|
||||||
adj |= 2;
|
if (checkNeighbor(wx, wz - 1)) adj |= 8; // -Z
|
||||||
|
|
||||||
// bit 2: +Z
|
|
||||||
if (world.getVoxel(wx, wy, wz + 1).getMaterialID() == mat &&
|
|
||||||
world.getVoxel(wx, wy + 1, wz + 1).isEmpty())
|
|
||||||
adj |= 4;
|
|
||||||
|
|
||||||
// bit 3: -Z
|
|
||||||
if (world.getVoxel(wx, wy, wz - 1).getMaterialID() == mat &&
|
|
||||||
world.getVoxel(wx, wy + 1, wz - 1).isEmpty())
|
|
||||||
adj |= 8;
|
|
||||||
|
|
||||||
instances_.push_back({
|
instances_.push_back({
|
||||||
(float)wx, (float)wy, (float)wz,
|
(float)wx, (float)wy, (float)wz,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ struct MeshSlice {
|
||||||
struct TopingDef {
|
struct TopingDef {
|
||||||
uint8_t materialID; // voxel material that triggers this toping
|
uint8_t materialID; // voxel material that triggers this toping
|
||||||
uint8_t face; // Face enum (FACE_POS_Y, etc.)
|
uint8_t face; // Face enum (FACE_POS_Y, etc.)
|
||||||
|
uint8_t priority; // higher priority topings generate bevels over lower ones at boundaries
|
||||||
float height; // bevel peak height (voxel units)
|
float height; // bevel peak height (voxel units)
|
||||||
float width; // bevel inward extent (voxel units)
|
float width; // bevel inward extent (voxel units)
|
||||||
int segments; // subdivisions per edge strip (1=smooth, 3+=bumpy)
|
int segments; // subdivisions per edge strip (1=smooth, 3+=bumpy)
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,24 @@ void VoxelRenderer::createPipeline() {
|
||||||
psoDesc.pt = PrimitiveTopology::TRIANGLELIST;
|
psoDesc.pt = PrimitiveTopology::TRIANGLELIST;
|
||||||
|
|
||||||
device_->CreatePipelineState(&psoDesc, &pso_);
|
device_->CreatePipelineState(&psoDesc, &pso_);
|
||||||
|
|
||||||
|
// ── Toping pipeline (Phase 4) ────────────────────────────────
|
||||||
|
wi::renderer::LoadShader(ShaderStage::VS, topingVS_, "voxel/voxelTopingVS.cso");
|
||||||
|
wi::renderer::LoadShader(ShaderStage::PS, topingPS_, "voxel/voxelTopingPS.cso");
|
||||||
|
|
||||||
|
if (topingVS_.IsValid() && topingPS_.IsValid()) {
|
||||||
|
PipelineStateDesc topingPsoDesc;
|
||||||
|
topingPsoDesc.vs = &topingVS_;
|
||||||
|
topingPsoDesc.ps = &topingPS_;
|
||||||
|
topingPsoDesc.rs = wi::renderer::GetRasterizerState(wi::enums::RSTYPE_FRONT);
|
||||||
|
topingPsoDesc.dss = wi::renderer::GetDepthStencilState(wi::enums::DSSTYPE_DEFAULT);
|
||||||
|
topingPsoDesc.bs = wi::renderer::GetBlendState(wi::enums::BSTYPE_OPAQUE);
|
||||||
|
topingPsoDesc.pt = PrimitiveTopology::TRIANGLELIST;
|
||||||
|
device_->CreatePipelineState(&topingPsoDesc, &topingPso_);
|
||||||
|
wi::backlog::post("VoxelRenderer: toping pipeline created");
|
||||||
|
} else {
|
||||||
|
wi::backlog::post("VoxelRenderer: toping shader loading failed", wi::backlog::LogLevel::Warning);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Procedural texture generation ───────────────────────────────
|
// ── Procedural texture generation ───────────────────────────────
|
||||||
|
|
@ -1130,6 +1148,178 @@ void VoxelRenderer::render(
|
||||||
dev->RenderPassEnd(cmd);
|
dev->RenderPassEnd(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Phase 4: Toping GPU upload + rendering ─────────────────────
|
||||||
|
|
||||||
|
void VoxelRenderer::uploadTopingData(const TopingSystem& topingSystem) {
|
||||||
|
if (!device_ || !topingPso_.IsValid()) return;
|
||||||
|
|
||||||
|
// Upload mesh vertices (done once, meshes are static)
|
||||||
|
const auto& verts = topingSystem.getVertices();
|
||||||
|
if (!verts.empty() && !topingVertexBuffer_.IsValid()) {
|
||||||
|
GPUBufferDesc vbDesc;
|
||||||
|
vbDesc.size = verts.size() * sizeof(TopingVertex);
|
||||||
|
vbDesc.bind_flags = BindFlag::SHADER_RESOURCE;
|
||||||
|
vbDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
|
||||||
|
vbDesc.stride = sizeof(TopingVertex);
|
||||||
|
vbDesc.usage = Usage::DEFAULT;
|
||||||
|
device_->CreateBuffer(&vbDesc, verts.data(), &topingVertexBuffer_);
|
||||||
|
|
||||||
|
char msg[128];
|
||||||
|
snprintf(msg, sizeof(msg), "Toping: uploaded %zu vertices (%zu bytes)",
|
||||||
|
verts.size(), verts.size() * sizeof(TopingVertex));
|
||||||
|
wi::backlog::post(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload instance positions (re-upload when world changes)
|
||||||
|
const auto& instances = topingSystem.getInstances();
|
||||||
|
if (instances.empty()) return;
|
||||||
|
|
||||||
|
// GPU instances are just float3 (12 bytes), sorted by (type, variant) for batched draws.
|
||||||
|
// We sort a copy and build a draw group table.
|
||||||
|
struct SortedInst {
|
||||||
|
float wx, wy, wz;
|
||||||
|
uint16_t type, variant;
|
||||||
|
};
|
||||||
|
std::vector<SortedInst> sorted(instances.size());
|
||||||
|
for (size_t i = 0; i < instances.size(); i++) {
|
||||||
|
sorted[i] = { instances[i].wx, instances[i].wy, instances[i].wz,
|
||||||
|
instances[i].topingType, instances[i].variant };
|
||||||
|
}
|
||||||
|
std::sort(sorted.begin(), sorted.end(), [](const SortedInst& a, const SortedInst& b) {
|
||||||
|
if (a.type != b.type) return a.type < b.type;
|
||||||
|
return a.variant < b.variant;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pack GPU instance data (just float3 positions)
|
||||||
|
struct GPUTopingInst { float x, y, z; };
|
||||||
|
uint32_t instCount = (uint32_t)std::min(sorted.size(), (size_t)MAX_TOPING_INSTANCES);
|
||||||
|
std::vector<GPUTopingInst> gpuInsts(instCount);
|
||||||
|
for (uint32_t i = 0; i < instCount; i++) {
|
||||||
|
gpuInsts[i] = { sorted[i].wx, sorted[i].wy, sorted[i].wz };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or recreate instance buffer
|
||||||
|
GPUBufferDesc ibDesc;
|
||||||
|
ibDesc.size = instCount * sizeof(GPUTopingInst);
|
||||||
|
ibDesc.bind_flags = BindFlag::SHADER_RESOURCE;
|
||||||
|
ibDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
|
||||||
|
ibDesc.stride = sizeof(GPUTopingInst);
|
||||||
|
ibDesc.usage = Usage::DEFAULT;
|
||||||
|
device_->CreateBuffer(&ibDesc, gpuInsts.data(), &topingInstanceBuffer_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoxelRenderer::renderTopings(
|
||||||
|
CommandList cmd,
|
||||||
|
const TopingSystem& topingSystem,
|
||||||
|
const Texture& depthBuffer,
|
||||||
|
const Texture& renderTarget
|
||||||
|
) const {
|
||||||
|
if (!topingPso_.IsValid() || !topingVertexBuffer_.IsValid() ||
|
||||||
|
!topingInstanceBuffer_.IsValid()) return;
|
||||||
|
|
||||||
|
const auto& instances = topingSystem.getInstances();
|
||||||
|
const auto& defs = topingSystem.getDefs();
|
||||||
|
if (instances.empty()) return;
|
||||||
|
|
||||||
|
auto* dev = device_;
|
||||||
|
|
||||||
|
// Open render pass with LOAD (preserve voxel render output)
|
||||||
|
RenderPassImage rp[] = {
|
||||||
|
RenderPassImage::RenderTarget(
|
||||||
|
&renderTarget,
|
||||||
|
RenderPassImage::LoadOp::LOAD,
|
||||||
|
RenderPassImage::StoreOp::STORE,
|
||||||
|
ResourceState::SHADER_RESOURCE,
|
||||||
|
ResourceState::SHADER_RESOURCE
|
||||||
|
),
|
||||||
|
RenderPassImage::DepthStencil(
|
||||||
|
&depthBuffer,
|
||||||
|
RenderPassImage::LoadOp::LOAD,
|
||||||
|
RenderPassImage::StoreOp::STORE,
|
||||||
|
ResourceState::DEPTHSTENCIL,
|
||||||
|
ResourceState::DEPTHSTENCIL,
|
||||||
|
ResourceState::DEPTHSTENCIL
|
||||||
|
),
|
||||||
|
};
|
||||||
|
dev->RenderPassBegin(rp, 2, cmd);
|
||||||
|
|
||||||
|
// Viewport & scissor
|
||||||
|
Viewport vp;
|
||||||
|
vp.top_left_x = 0; vp.top_left_y = 0;
|
||||||
|
vp.width = (float)renderTarget.GetDesc().width;
|
||||||
|
vp.height = (float)renderTarget.GetDesc().height;
|
||||||
|
vp.min_depth = 0.0f; vp.max_depth = 1.0f;
|
||||||
|
Rect scissor = { 0, 0, (int)renderTarget.GetDesc().width, (int)renderTarget.GetDesc().height };
|
||||||
|
dev->BindViewports(1, &vp, cmd);
|
||||||
|
dev->BindScissorRects(1, &scissor, cmd);
|
||||||
|
|
||||||
|
// Bind toping pipeline (MUST be before PushConstants!)
|
||||||
|
dev->BindPipelineState(&topingPso_, cmd);
|
||||||
|
dev->BindConstantBuffer(&constantBuffer_, 0, cmd);
|
||||||
|
dev->BindResource(&textureArray_, 1, cmd);
|
||||||
|
dev->BindResource(&topingVertexBuffer_, 4, cmd); // t4
|
||||||
|
dev->BindResource(&topingInstanceBuffer_, 5, cmd); // t5
|
||||||
|
dev->BindSampler(&sampler_, 0, cmd);
|
||||||
|
|
||||||
|
// Build sorted draw groups (same sort order as uploadTopingData)
|
||||||
|
struct DrawGroup {
|
||||||
|
uint16_t type, variant;
|
||||||
|
uint32_t instanceOffset, instanceCount;
|
||||||
|
};
|
||||||
|
struct SortKey { uint16_t type, variant; };
|
||||||
|
std::vector<SortKey> sortedKeys(instances.size());
|
||||||
|
for (size_t i = 0; i < instances.size(); i++) {
|
||||||
|
sortedKeys[i] = { instances[i].topingType, instances[i].variant };
|
||||||
|
}
|
||||||
|
std::sort(sortedKeys.begin(), sortedKeys.end(), [](const SortKey& a, const SortKey& b) {
|
||||||
|
if (a.type != b.type) return a.type < b.type;
|
||||||
|
return a.variant < b.variant;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Identify contiguous groups
|
||||||
|
std::vector<DrawGroup> groups;
|
||||||
|
uint32_t instCount = (uint32_t)std::min(sortedKeys.size(), (size_t)MAX_TOPING_INSTANCES);
|
||||||
|
if (instCount > 0) {
|
||||||
|
DrawGroup g = { sortedKeys[0].type, sortedKeys[0].variant, 0, 1 };
|
||||||
|
for (uint32_t i = 1; i < instCount; i++) {
|
||||||
|
if (sortedKeys[i].type == g.type && sortedKeys[i].variant == g.variant) {
|
||||||
|
g.instanceCount++;
|
||||||
|
} else {
|
||||||
|
groups.push_back(g);
|
||||||
|
g = { sortedKeys[i].type, sortedKeys[i].variant, i, 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
groups.push_back(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue one DrawInstanced per group
|
||||||
|
topingDrawCalls_ = 0;
|
||||||
|
struct TopingPush {
|
||||||
|
uint32_t vertexOffset;
|
||||||
|
uint32_t instanceOffset;
|
||||||
|
uint32_t materialID;
|
||||||
|
uint32_t pad[9];
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& g : groups) {
|
||||||
|
if (g.type >= defs.size()) continue;
|
||||||
|
const TopingDef& def = defs[g.type];
|
||||||
|
const MeshSlice& slice = def.variants[g.variant];
|
||||||
|
if (slice.count == 0) continue; // empty mesh (all neighbors present)
|
||||||
|
|
||||||
|
TopingPush pushData = {};
|
||||||
|
pushData.vertexOffset = slice.offset;
|
||||||
|
pushData.instanceOffset = g.instanceOffset;
|
||||||
|
pushData.materialID = def.materialID;
|
||||||
|
dev->PushConstants(&pushData, sizeof(pushData), cmd);
|
||||||
|
|
||||||
|
dev->DrawInstanced(slice.count, g.instanceCount, 0, 0, cmd);
|
||||||
|
topingDrawCalls_++;
|
||||||
|
}
|
||||||
|
|
||||||
|
dev->RenderPassEnd(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
// ── VoxelRenderPath (custom RenderPath3D) ───────────────────────
|
// ── VoxelRenderPath (custom RenderPath3D) ───────────────────────
|
||||||
|
|
||||||
void VoxelRenderPath::Start() {
|
void VoxelRenderPath::Start() {
|
||||||
|
|
@ -1152,9 +1342,12 @@ void VoxelRenderPath::Start() {
|
||||||
renderer.updateMeshes(world);
|
renderer.updateMeshes(world);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 4: Initialize toping system and collect instances
|
// Phase 4: Initialize toping system, collect instances, upload to GPU
|
||||||
topingSystem.initialize();
|
topingSystem.initialize();
|
||||||
topingSystem.collectInstances(world);
|
topingSystem.collectInstances(world);
|
||||||
|
if (renderer.isInitialized()) {
|
||||||
|
renderer.uploadTopingData(topingSystem);
|
||||||
|
}
|
||||||
{
|
{
|
||||||
char msg[256];
|
char msg[256];
|
||||||
snprintf(msg, sizeof(msg),
|
snprintf(msg, sizeof(msg),
|
||||||
|
|
@ -1351,6 +1544,9 @@ void VoxelRenderPath::Render() const {
|
||||||
|
|
||||||
auto tRender0 = std::chrono::high_resolution_clock::now();
|
auto tRender0 = std::chrono::high_resolution_clock::now();
|
||||||
renderer.render(cmd, *camera, voxelDepth_, voxelRT_);
|
renderer.render(cmd, *camera, voxelDepth_, voxelRT_);
|
||||||
|
|
||||||
|
// Phase 4: render topings (separate render pass, preserves voxel output)
|
||||||
|
renderer.renderTopings(cmd, topingSystem, voxelDepth_, voxelRT_);
|
||||||
auto tRender1 = std::chrono::high_resolution_clock::now();
|
auto tRender1 = std::chrono::high_resolution_clock::now();
|
||||||
profRender_.add(std::chrono::duration<float, std::milli>(tRender1 - tRender0).count());
|
profRender_.add(std::chrono::duration<float, std::milli>(tRender1 - tRender0).count());
|
||||||
}
|
}
|
||||||
|
|
@ -1440,8 +1636,8 @@ void VoxelRenderPath::Compose(CommandList cmd) const {
|
||||||
stats += "GPU Cull: " + std::string(cullStr) + " ms | Draw: " + std::string(drawStr) + " ms\n";
|
stats += "GPU Cull: " + std::string(cullStr) + " ms | Draw: " + std::string(drawStr) + " ms\n";
|
||||||
}
|
}
|
||||||
stats += "Topings: " + std::to_string(topingSystem.getInstanceCount())
|
stats += "Topings: " + std::to_string(topingSystem.getInstanceCount())
|
||||||
+ " instances (" + std::to_string(topingSystem.getDefCount()) + " types, "
|
+ " instances, " + std::to_string(renderer.getTopingDrawCalls())
|
||||||
+ std::to_string(topingSystem.getVertexCount()) + " verts)\n";
|
+ " draws (" + std::to_string(topingSystem.getDefCount()) + " types)\n";
|
||||||
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(animatedTerrain_ ? "ON" : "OFF")
|
stats += "F2: console | F3: anim [" + std::string(animatedTerrain_ ? "ON" : "OFF")
|
||||||
+ "] | F4: dbg [" + std::string(renderer.debugBlend_ ? "ON" : "OFF") + "]";
|
+ "] | F4: dbg [" + std::string(renderer.debugBlend_ ? "ON" : "OFF") + "]";
|
||||||
|
|
|
||||||
|
|
@ -67,12 +67,21 @@ private:
|
||||||
|
|
||||||
wi::graphics::GraphicsDevice* device_ = nullptr;
|
wi::graphics::GraphicsDevice* device_ = nullptr;
|
||||||
|
|
||||||
// Shaders & Pipeline
|
// Shaders & Pipeline (voxels)
|
||||||
wi::graphics::Shader vertexShader_;
|
wi::graphics::Shader vertexShader_;
|
||||||
wi::graphics::Shader pixelShader_;
|
wi::graphics::Shader pixelShader_;
|
||||||
wi::graphics::PipelineState pso_;
|
wi::graphics::PipelineState pso_;
|
||||||
wi::graphics::Shader cullShader_; // Frustum cull compute shader
|
wi::graphics::Shader cullShader_; // Frustum cull compute shader
|
||||||
|
|
||||||
|
// Shaders & Pipeline (topings, Phase 4)
|
||||||
|
wi::graphics::Shader topingVS_;
|
||||||
|
wi::graphics::Shader topingPS_;
|
||||||
|
wi::graphics::PipelineState topingPso_;
|
||||||
|
wi::graphics::GPUBuffer topingVertexBuffer_; // StructuredBuffer<TopingVertex>, SRV t4
|
||||||
|
wi::graphics::GPUBuffer topingInstanceBuffer_; // StructuredBuffer<float3>, SRV t5
|
||||||
|
static constexpr uint32_t MAX_TOPING_INSTANCES = 256 * 1024; // 256K instances max
|
||||||
|
mutable uint32_t topingDrawCalls_ = 0;
|
||||||
|
|
||||||
// Texture array for materials (256x256, 5 layers for prototype)
|
// Texture array for materials (256x256, 5 layers for prototype)
|
||||||
wi::graphics::Texture textureArray_;
|
wi::graphics::Texture textureArray_;
|
||||||
wi::graphics::Sampler sampler_;
|
wi::graphics::Sampler sampler_;
|
||||||
|
|
@ -186,6 +195,16 @@ public:
|
||||||
float getGpuDrawTimeMs() const { return gpuDrawTimeMs_; }
|
float getGpuDrawTimeMs() const { return gpuDrawTimeMs_; }
|
||||||
bool isGpuMeshEnabled() const { return gpuMeshEnabled_ && gpuMesherAvailable_; }
|
bool isGpuMeshEnabled() const { return gpuMeshEnabled_ && gpuMesherAvailable_; }
|
||||||
uint32_t getGpuMeshQuadCount() const { return gpuMeshQuadCount_; }
|
uint32_t getGpuMeshQuadCount() const { return gpuMeshQuadCount_; }
|
||||||
|
|
||||||
|
// Phase 4: Toping rendering
|
||||||
|
void uploadTopingData(const TopingSystem& topingSystem);
|
||||||
|
void renderTopings(
|
||||||
|
wi::graphics::CommandList cmd,
|
||||||
|
const TopingSystem& topingSystem,
|
||||||
|
const wi::graphics::Texture& depthBuffer,
|
||||||
|
const wi::graphics::Texture& renderTarget
|
||||||
|
) const;
|
||||||
|
uint32_t getTopingDrawCalls() const { return topingDrawCalls_; }
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Custom RenderPath that integrates voxel rendering ───────────
|
// ── Custom RenderPath that integrates voxel rendering ───────────
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue