From 6a681bcf679fa73f6f6be58c408b96c2d95e7541 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 9 Mar 2026 18:34:26 -0700 Subject: [PATCH] Implement terrain shadow casting in shadow depth pass Add initializeShadow() to TerrainRenderer that creates a depth-only shadow pipeline reusing the existing shadow.vert/frag shaders (same path as WMO/M2/character renderers). renderShadow() draws all terrain chunks with sphere culling against the shadow coverage radius. Wire both init and draw calls into Renderer so terrain now casts shadows alongside buildings and NPCs. --- include/rendering/terrain_renderer.hpp | 29 +++- src/rendering/renderer.cpp | 8 + src/rendering/terrain_renderer.cpp | 203 ++++++++++++++++++++++++- 3 files changed, 234 insertions(+), 6 deletions(-) diff --git a/include/rendering/terrain_renderer.hpp b/include/rendering/terrain_renderer.hpp index 77af9a64..f4994792 100644 --- a/include/rendering/terrain_renderer.hpp +++ b/include/rendering/terrain_renderer.hpp @@ -106,9 +106,22 @@ public: void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera); /** - * Render terrain into shadow depth map (Phase 6 stub) + * Initialize terrain shadow pipeline (must be called after initialize()). + * @param shadowRenderPass Depth-only render pass used for the shadow map. */ - void renderShadow(VkCommandBuffer cmd, const glm::vec3& shadowCenter, float halfExtent); + bool initializeShadow(VkRenderPass shadowRenderPass); + + /** + * Render terrain into the shadow depth map. + * @param cmd Command buffer (inside shadow render pass). + * @param lightSpaceMatrix Orthographic light-space transform. + * @param shadowCenter World-space centre of shadow coverage. + * @param shadowRadius Cull radius around shadowCenter. + */ + void renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMatrix, + const glm::vec3& shadowCenter, float shadowRadius); + + bool hasShadowPipeline() const { return shadowPipeline_ != VK_NULL_HANDLE; } void clear(); @@ -119,7 +132,6 @@ public: void setFogEnabled(bool enabled) { fogEnabled = enabled; } bool isFogEnabled() const { return fogEnabled; } - // Shadow mapping stubs (Phase 6) void setShadowMap(VkDescriptorImageInfo /*depthInfo*/, const glm::mat4& /*lightSpaceMat*/) {} void clearShadowMap() {} @@ -142,12 +154,21 @@ private: VkContext* vkCtx = nullptr; pipeline::AssetManager* assetManager = nullptr; - // Pipeline + // Main pipelines VkPipeline pipeline = VK_NULL_HANDLE; VkPipeline wireframePipeline = VK_NULL_HANDLE; VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; VkDescriptorSetLayout materialSetLayout = VK_NULL_HANDLE; + // Shadow pipeline + VkPipeline shadowPipeline_ = VK_NULL_HANDLE; + VkPipelineLayout shadowPipelineLayout_ = VK_NULL_HANDLE; + VkDescriptorSetLayout shadowParamsLayout_ = VK_NULL_HANDLE; + VkDescriptorPool shadowParamsPool_ = VK_NULL_HANDLE; + VkDescriptorSet shadowParamsSet_ = VK_NULL_HANDLE; + VkBuffer shadowParamsUBO_ = VK_NULL_HANDLE; + VmaAllocation shadowParamsAlloc_ = VK_NULL_HANDLE; + // Descriptor pool for material sets VkDescriptorPool materialDescPool = VK_NULL_HANDLE; static constexpr uint32_t MAX_MATERIAL_SETS = 16384; diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 5a8f23f6..86e997c4 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -4998,6 +4998,11 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s terrainRenderer.reset(); return false; } + if (shadowRenderPass != VK_NULL_HANDLE) { + terrainRenderer->initializeShadow(shadowRenderPass); + } + } else if (!terrainRenderer->hasShadowPipeline() && shadowRenderPass != VK_NULL_HANDLE) { + terrainRenderer->initializeShadow(shadowRenderPass); } // Create water renderer if not already created @@ -5724,6 +5729,9 @@ void Renderer::renderShadowPass() { // Phase 7/8: render shadow casters const float shadowCullRadius = shadowDistance_ * 1.35f; + if (terrainRenderer) { + terrainRenderer->renderShadow(currentCmd, lightSpaceMatrix, shadowCenter, shadowCullRadius); + } if (wmoRenderer) { wmoRenderer->renderShadow(currentCmd, lightSpaceMatrix, shadowCenter, shadowCullRadius); } diff --git a/src/rendering/terrain_renderer.cpp b/src/rendering/terrain_renderer.cpp index fb20ce42..a2d85886 100644 --- a/src/rendering/terrain_renderer.cpp +++ b/src/rendering/terrain_renderer.cpp @@ -314,6 +314,13 @@ void TerrainRenderer::shutdown() { if (materialDescPool) { vkDestroyDescriptorPool(device, materialDescPool, nullptr); materialDescPool = VK_NULL_HANDLE; } if (materialSetLayout) { vkDestroyDescriptorSetLayout(device, materialSetLayout, nullptr); materialSetLayout = VK_NULL_HANDLE; } + // Shadow pipeline cleanup + if (shadowPipeline_) { vkDestroyPipeline(device, shadowPipeline_, nullptr); shadowPipeline_ = VK_NULL_HANDLE; } + if (shadowPipelineLayout_) { vkDestroyPipelineLayout(device, shadowPipelineLayout_, nullptr); shadowPipelineLayout_ = VK_NULL_HANDLE; } + if (shadowParamsPool_) { vkDestroyDescriptorPool(device, shadowParamsPool_, nullptr); shadowParamsPool_ = VK_NULL_HANDLE; shadowParamsSet_ = VK_NULL_HANDLE; } + if (shadowParamsLayout_) { vkDestroyDescriptorSetLayout(device, shadowParamsLayout_, nullptr); shadowParamsLayout_ = VK_NULL_HANDLE; } + if (shadowParamsUBO_) { vmaDestroyBuffer(allocator, shadowParamsUBO_, shadowParamsAlloc_); shadowParamsUBO_ = VK_NULL_HANDLE; shadowParamsAlloc_ = VK_NULL_HANDLE; } + vkCtx = nullptr; } @@ -784,8 +791,200 @@ void TerrainRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, c } -void TerrainRenderer::renderShadow(VkCommandBuffer /*cmd*/, const glm::vec3& /*shadowCenter*/, float /*halfExtent*/) { - // Phase 6 stub +bool TerrainRenderer::initializeShadow(VkRenderPass shadowRenderPass) { + if (!vkCtx || shadowRenderPass == VK_NULL_HANDLE) return false; + if (shadowPipeline_ != VK_NULL_HANDLE) return true; // already initialised + VkDevice device = vkCtx->getDevice(); + VmaAllocator allocator = vkCtx->getAllocator(); + + // ShadowParams UBO — terrain uses no bones, no texture, no alpha test + struct ShadowParamsUBO { + int32_t useBones = 0; + int32_t useTexture = 0; + int32_t alphaTest = 0; + int32_t foliageSway = 0; + float windTime = 0.0f; + float foliageMotionDamp = 1.0f; + }; + + VkBufferCreateInfo bufCI{}; + bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufCI.size = sizeof(ShadowParamsUBO); + bufCI.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + VmaAllocationCreateInfo allocCI{}; + allocCI.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + allocCI.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + VmaAllocationInfo allocInfo{}; + if (vmaCreateBuffer(allocator, &bufCI, &allocCI, + &shadowParamsUBO_, &shadowParamsAlloc_, &allocInfo) != VK_SUCCESS) { + LOG_ERROR("TerrainRenderer: failed to create shadow params UBO"); + return false; + } + ShadowParamsUBO defaultParams{}; + std::memcpy(allocInfo.pMappedData, &defaultParams, sizeof(defaultParams)); + + // Descriptor set layout: binding 0 = combined sampler (unused), binding 1 = ShadowParams UBO + VkDescriptorSetLayoutBinding layoutBindings[2]{}; + layoutBindings[0].binding = 0; + layoutBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + layoutBindings[0].descriptorCount = 1; + layoutBindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + layoutBindings[1].binding = 1; + layoutBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + layoutBindings[1].descriptorCount = 1; + layoutBindings[1].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + + VkDescriptorSetLayoutCreateInfo layoutCI{}; + layoutCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + layoutCI.bindingCount = 2; + layoutCI.pBindings = layoutBindings; + if (vkCreateDescriptorSetLayout(device, &layoutCI, nullptr, &shadowParamsLayout_) != VK_SUCCESS) { + LOG_ERROR("TerrainRenderer: failed to create shadow params set layout"); + return false; + } + + VkDescriptorPoolSize poolSizes[2]{}; + poolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + poolSizes[0].descriptorCount = 1; + poolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + poolSizes[1].descriptorCount = 1; + VkDescriptorPoolCreateInfo poolCI{}; + poolCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolCI.maxSets = 1; + poolCI.poolSizeCount = 2; + poolCI.pPoolSizes = poolSizes; + if (vkCreateDescriptorPool(device, &poolCI, nullptr, &shadowParamsPool_) != VK_SUCCESS) { + LOG_ERROR("TerrainRenderer: failed to create shadow params pool"); + return false; + } + + VkDescriptorSetAllocateInfo setAlloc{}; + setAlloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + setAlloc.descriptorPool = shadowParamsPool_; + setAlloc.descriptorSetCount = 1; + setAlloc.pSetLayouts = &shadowParamsLayout_; + if (vkAllocateDescriptorSets(device, &setAlloc, &shadowParamsSet_) != VK_SUCCESS) { + LOG_ERROR("TerrainRenderer: failed to allocate shadow params set"); + return false; + } + + // Write descriptors — sampler uses whiteTexture as dummy (useTexture=0 so never sampled) + VkDescriptorBufferInfo bufInfo{ shadowParamsUBO_, 0, sizeof(ShadowParamsUBO) }; + VkDescriptorImageInfo imgInfo{}; + imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + imgInfo.imageView = whiteTexture->getImageView(); + imgInfo.sampler = whiteTexture->getSampler(); + + VkWriteDescriptorSet writes[2]{}; + writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[0].dstSet = shadowParamsSet_; + writes[0].dstBinding = 0; + writes[0].descriptorCount = 1; + writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[0].pImageInfo = &imgInfo; + writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[1].dstSet = shadowParamsSet_; + writes[1].dstBinding = 1; + writes[1].descriptorCount = 1; + writes[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + writes[1].pBufferInfo = &bufInfo; + vkUpdateDescriptorSets(device, 2, writes, 0, nullptr); + + // Pipeline layout: set 0 = shadowParamsLayout_, push 128 bytes (lightSpaceMatrix + model) + VkPushConstantRange pc{}; + pc.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + pc.offset = 0; + pc.size = 128; + shadowPipelineLayout_ = createPipelineLayout(device, {shadowParamsLayout_}, {pc}); + if (!shadowPipelineLayout_) { + LOG_ERROR("TerrainRenderer: failed to create shadow pipeline layout"); + return false; + } + + VkShaderModule vertShader, fragShader; + if (!vertShader.loadFromFile(device, "assets/shaders/shadow.vert.spv")) { + LOG_ERROR("TerrainRenderer: failed to load shadow vertex shader"); + return false; + } + if (!fragShader.loadFromFile(device, "assets/shaders/shadow.frag.spv")) { + LOG_ERROR("TerrainRenderer: failed to load shadow fragment shader"); + vertShader.destroy(); + return false; + } + + // Terrain vertex layout: pos(0,off0) normal(1,off12) texCoord(2,off24) layerUV(3,off32) + // stride = sizeof(TerrainVertex) = 44 bytes + // Shadow shader expects: aPos(loc0), aTexCoord(loc1), aBoneWeights(loc2), aBoneIndicesF(loc3) + // Alias unused bone attrs to position (offset 0); useBones=0 so they are never read. + const uint32_t stride = static_cast(sizeof(pipeline::TerrainVertex)); + VkVertexInputBindingDescription vertBind{}; + vertBind.binding = 0; + vertBind.stride = stride; + vertBind.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + std::vector vertAttrs = { + {0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, // aPos -> position + {1, 0, VK_FORMAT_R32G32_SFLOAT, 24}, // aTexCoord -> texCoord (unused) + {2, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0}, // aBoneWeights -> position (unused) + {3, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0}, // aBoneIndices -> position (unused) + }; + + shadowPipeline_ = PipelineBuilder() + .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({vertBind}, vertAttrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL) + .setDepthBias(0.05f, 0.20f) + .setNoColorAttachment() + .setLayout(shadowPipelineLayout_) + .setRenderPass(shadowRenderPass) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device); + + vertShader.destroy(); + fragShader.destroy(); + + if (!shadowPipeline_) { + LOG_ERROR("TerrainRenderer: failed to create shadow pipeline"); + return false; + } + LOG_INFO("TerrainRenderer shadow pipeline initialized"); + return true; +} + +void TerrainRenderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMatrix, + const glm::vec3& shadowCenter, float shadowRadius) { + if (!shadowPipeline_ || !shadowParamsSet_) return; + if (chunks.empty()) return; + + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipeline_); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipelineLayout_, + 0, 1, &shadowParamsSet_, 0, nullptr); + + // Identity model matrix — terrain vertices are already in world space + static const glm::mat4 identity(1.0f); + struct ShadowPush { glm::mat4 lightSpaceMatrix; glm::mat4 model; }; + ShadowPush push{ lightSpaceMatrix, identity }; + vkCmdPushConstants(cmd, shadowPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, + 0, 128, &push); + + const float cullRadiusSq = shadowRadius * shadowRadius; + + for (const auto& chunk : chunks) { + if (!chunk.isValid()) continue; + + // Sphere-cull chunk against shadow region + glm::vec3 diff = chunk.boundingSphereCenter - shadowCenter; + float distSq = glm::dot(diff, diff); + float combinedRadius = shadowRadius + chunk.boundingSphereRadius; + if (distSq > combinedRadius * combinedRadius) continue; + + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &chunk.vertexBuffer, &offset); + vkCmdBindIndexBuffer(cmd, chunk.indexBuffer, 0, VK_INDEX_TYPE_UINT16); + vkCmdDrawIndexed(cmd, chunk.indexCount, 1, 0, 0, 0); + } } void TerrainRenderer::removeTile(int tileX, int tileY) {