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)
|
||||
│ │ ├── VoxelWorld.h/.cpp # Monde voxel (hashmap de chunks, génération procédurale)
|
||||
│ │ ├── 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/
|
||||
│ └── main.cpp # Point d'entrée Win32 + crash handler SEH
|
||||
├── 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)
|
||||
│ ├── voxelPS.hlsl # Pixel shader (triplanar + lighting)
|
||||
│ ├── 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
|
||||
```
|
||||
|
||||
|
|
@ -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)
|
||||
- **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)
|
||||
- Instance buffer GPU par chunk
|
||||
- Instanced draw dans le G-buffer
|
||||
- 2-3 types de test (rebord de pierre, bordure d'herbe)
|
||||
Système de biseaux décoratifs (« topings ») sur les faces +Y exposées pour adoucir les transitions entre blocs.
|
||||
|
||||
#### Phase 4.1 - Infrastructure TopingSystem [FAIT]
|
||||
|
||||
- **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]
|
||||
|
||||
|
|
|
|||
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,17 +38,34 @@ 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
|
||||
};
|
||||
|
||||
// ── 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,
|
||||
float ax, float ay, float az,
|
||||
float bx, float by, float bz,
|
||||
float cx, float cy, float cz,
|
||||
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({ bx, by, bz, nx, ny, nz });
|
||||
v.push_back({ cx, cy, cz, nx, ny, nz });
|
||||
}
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// TopingSystem implementation
|
||||
|
|
@ -65,10 +82,12 @@ void TopingSystem::registerDefs() {
|
|||
|
||||
// Type 0: Stone bevel — clean angular ridge along open edges
|
||||
// Applied to stone (materialID=3), face +Y
|
||||
// Priority 0 (low): yields to grass at material boundaries
|
||||
{
|
||||
TopingDef def{};
|
||||
def.materialID = 3;
|
||||
def.face = FACE_POS_Y;
|
||||
def.priority = 0;
|
||||
def.height = 0.06f; // subtle bevel
|
||||
def.width = 0.12f;
|
||||
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
|
||||
// Applied to grass (materialID=1), face +Y
|
||||
// Priority 1 (high): generates bevels over stone at boundaries
|
||||
{
|
||||
TopingDef def{};
|
||||
def.materialID = 1;
|
||||
def.face = FACE_POS_Y;
|
||||
def.priority = 1;
|
||||
def.height = 0.12f; // taller, more visible
|
||||
def.width = 0.18f;
|
||||
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) ──────
|
||||
// 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_,
|
||||
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);
|
||||
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);
|
||||
|
||||
// ── Slope face (from peak down to inner edge) ───────
|
||||
// Compute normal via cross product of two edge vectors.
|
||||
// v_along_slope = inner - peak = (w*ix, -h, w*iz) at midpoint
|
||||
// v_along_strip = strip direction = (dx/segs, 0, dz/segs)
|
||||
// Slope normal: the slope rises from inner (y=1) to peak (y=1+h) at the edge.
|
||||
// Since it tilts outward, the normal points INWARD and UP.
|
||||
// Normal = inward_dir * (h/L) + up * (w/L), where L = sqrt(h²+w²).
|
||||
const float avgH = (h0 + h1) * 0.5f;
|
||||
const float asx = w * e.ix;
|
||||
const float asy = -avgH;
|
||||
const float asz = w * e.iz;
|
||||
const float adx = dx / segs;
|
||||
const float adz = dz / segs;
|
||||
float slopeLen = sqrtf(avgH * avgH + w * w);
|
||||
if (slopeLen < 0.0001f) slopeLen = 1.0f;
|
||||
float cnx = e.ix * (avgH / slopeLen);
|
||||
float cny = w / slopeLen;
|
||||
float cnz = e.iz * (avgH / slopeLen);
|
||||
|
||||
// normal = cross(v_along_strip, v_along_slope)
|
||||
float cnx = /* 0*asz - adz*asy = */ adz * avgH;
|
||||
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
|
||||
// Slope: peak → inner, strip direction
|
||||
// CW winding from normal direction
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -246,28 +255,29 @@ void TopingSystem::collectInstances(const VoxelWorld& world) {
|
|||
if (!world.getVoxel(wx, wy + 1, wz).isEmpty()) continue;
|
||||
|
||||
// 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;
|
||||
const uint8_t myPriority = def.priority;
|
||||
|
||||
// bit 0: +X
|
||||
if (world.getVoxel(wx + 1, wy, wz).getMaterialID() == mat &&
|
||||
world.getVoxel(wx + 1, wy + 1, wz).isEmpty())
|
||||
adj |= 1;
|
||||
auto checkNeighbor = [&](int nx, int nz) -> bool {
|
||||
uint8_t nMat = world.getVoxel(nx, wy, nz).getMaterialID();
|
||||
if (nMat == 0) return false; // empty
|
||||
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 (world.getVoxel(wx - 1, wy, wz).getMaterialID() == mat &&
|
||||
world.getVoxel(wx - 1, wy + 1, wz).isEmpty())
|
||||
adj |= 2;
|
||||
|
||||
// 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;
|
||||
if (checkNeighbor(wx + 1, wz)) adj |= 1; // +X
|
||||
if (checkNeighbor(wx - 1, wz)) adj |= 2; // -X
|
||||
if (checkNeighbor(wx, wz + 1)) adj |= 4; // +Z
|
||||
if (checkNeighbor(wx, wz - 1)) adj |= 8; // -Z
|
||||
|
||||
instances_.push_back({
|
||||
(float)wx, (float)wy, (float)wz,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ struct MeshSlice {
|
|||
struct TopingDef {
|
||||
uint8_t materialID; // voxel material that triggers this toping
|
||||
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 width; // bevel inward extent (voxel units)
|
||||
int segments; // subdivisions per edge strip (1=smooth, 3+=bumpy)
|
||||
|
|
|
|||
|
|
@ -177,6 +177,24 @@ void VoxelRenderer::createPipeline() {
|
|||
psoDesc.pt = PrimitiveTopology::TRIANGLELIST;
|
||||
|
||||
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 ───────────────────────────────
|
||||
|
|
@ -1130,6 +1148,178 @@ void VoxelRenderer::render(
|
|||
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) ───────────────────────
|
||||
|
||||
void VoxelRenderPath::Start() {
|
||||
|
|
@ -1152,9 +1342,12 @@ void VoxelRenderPath::Start() {
|
|||
renderer.updateMeshes(world);
|
||||
}
|
||||
|
||||
// Phase 4: Initialize toping system and collect instances
|
||||
// Phase 4: Initialize toping system, collect instances, upload to GPU
|
||||
topingSystem.initialize();
|
||||
topingSystem.collectInstances(world);
|
||||
if (renderer.isInitialized()) {
|
||||
renderer.uploadTopingData(topingSystem);
|
||||
}
|
||||
{
|
||||
char msg[256];
|
||||
snprintf(msg, sizeof(msg),
|
||||
|
|
@ -1351,6 +1544,9 @@ void VoxelRenderPath::Render() const {
|
|||
|
||||
auto tRender0 = std::chrono::high_resolution_clock::now();
|
||||
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();
|
||||
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 += "Topings: " + std::to_string(topingSystem.getInstanceCount())
|
||||
+ " instances (" + std::to_string(topingSystem.getDefCount()) + " types, "
|
||||
+ std::to_string(topingSystem.getVertexCount()) + " verts)\n";
|
||||
+ " instances, " + std::to_string(renderer.getTopingDrawCalls())
|
||||
+ " draws (" + std::to_string(topingSystem.getDefCount()) + " types)\n";
|
||||
stats += "WASD+Space/Ctrl: move | Shift: fast | Right-click: capture mouse\n";
|
||||
stats += "F2: console | F3: anim [" + std::string(animatedTerrain_ ? "ON" : "OFF")
|
||||
+ "] | F4: dbg [" + std::string(renderer.debugBlend_ ? "ON" : "OFF") + "]";
|
||||
|
|
|
|||
|
|
@ -67,12 +67,21 @@ private:
|
|||
|
||||
wi::graphics::GraphicsDevice* device_ = nullptr;
|
||||
|
||||
// Shaders & Pipeline
|
||||
// Shaders & Pipeline (voxels)
|
||||
wi::graphics::Shader vertexShader_;
|
||||
wi::graphics::Shader pixelShader_;
|
||||
wi::graphics::PipelineState pso_;
|
||||
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)
|
||||
wi::graphics::Texture textureArray_;
|
||||
wi::graphics::Sampler sampler_;
|
||||
|
|
@ -186,6 +195,16 @@ public:
|
|||
float getGpuDrawTimeMs() const { return gpuDrawTimeMs_; }
|
||||
bool isGpuMeshEnabled() const { return gpuMeshEnabled_ && gpuMesherAvailable_; }
|
||||
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 ───────────
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue