diff --git a/CLAUDE.md b/CLAUDE.md index 9441fc8..02f43cf 100644 --- a/CLAUDE.md +++ b/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` (t4) + `StructuredBuffer` (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] diff --git a/shaders/voxelTopingPS.hlsl b/shaders/voxelTopingPS.hlsl new file mode 100644 index 0000000..307eab7 --- /dev/null +++ b/shaders/voxelTopingPS.hlsl @@ -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 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); +} diff --git a/shaders/voxelTopingVS.hlsl b/shaders/voxelTopingVS.hlsl new file mode 100644 index 0000000..a28b524 --- /dev/null +++ b/shaders/voxelTopingVS.hlsl @@ -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 topingVertices : register(t4); +StructuredBuffer 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 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; +} diff --git a/src/voxel/TopingSystem.cpp b/src/voxel/TopingSystem.cpp index eeb1ac2..1625f5f 100644 --- a/src/voxel/TopingSystem.cpp +++ b/src/voxel/TopingSystem.cpp @@ -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 }; -// ── 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& 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) { - 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 }); + // 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 }); + } } // ═════════════════════════════════════════════════════════════════ @@ -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, diff --git a/src/voxel/TopingSystem.h b/src/voxel/TopingSystem.h index 92c7e12..58c5bf1 100644 --- a/src/voxel/TopingSystem.h +++ b/src/voxel/TopingSystem.h @@ -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) diff --git a/src/voxel/VoxelRenderer.cpp b/src/voxel/VoxelRenderer.cpp index c1dde8d..65f29c3 100644 --- a/src/voxel/VoxelRenderer.cpp +++ b/src/voxel/VoxelRenderer.cpp @@ -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 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 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 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 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(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") + "]"; diff --git a/src/voxel/VoxelRenderer.h b/src/voxel/VoxelRenderer.h index e62db20..8bee61a 100644 --- a/src/voxel/VoxelRenderer.h +++ b/src/voxel/VoxelRenderer.h @@ -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, SRV t4 + wi::graphics::GPUBuffer topingInstanceBuffer_; // StructuredBuffer, 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 ───────────