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) {