diff --git a/assets/shaders/wmo.frag.glsl b/assets/shaders/wmo.frag.glsl index 3a5c0f21..00852448 100644 --- a/assets/shaders/wmo.frag.glsl +++ b/assets/shaders/wmo.frag.glsl @@ -21,6 +21,7 @@ layout(set = 1, binding = 1) uniform WMOMaterial { int unlit; int isInterior; float specularIntensity; + int isWindow; }; layout(set = 0, binding = 1) uniform sampler2DShadow uShadowMap; @@ -78,5 +79,44 @@ void main() { float fogFactor = clamp((fogParams.y - dist) / (fogParams.y - fogParams.x), 0.0, 1.0); result = mix(fogColor.rgb, result, fogFactor); - outColor = vec4(result, texColor.a); + float alpha = texColor.a; + + // Window glass: opaque but simulates dark tinted glass with reflections. + // No real alpha blending — we darken the base texture and add reflection + // on top so it reads as glass without needing the transparent pipeline. + if (isWindow != 0) { + vec3 viewDir = normalize(viewPos.xyz - FragPos); + float NdotV = abs(dot(norm, viewDir)); + // Fresnel: strong reflection at grazing angles + float fresnel = 0.08 + 0.92 * pow(1.0 - NdotV, 4.0); + + // Glass darkness depends on view angle — bright when sun glints off, + // darker when looking straight on with no sun reflection. + vec3 ldir = normalize(-lightDir.xyz); + vec3 reflectDir = reflect(-viewDir, norm); + float sunGlint = pow(max(dot(reflectDir, ldir), 0.0), 32.0); + + // Base ranges from dark (0.3) to bright (0.9) based on sun reflection + float baseBrightness = mix(0.3, 0.9, sunGlint); + vec3 glass = result * baseBrightness; + + // Reflection: blend sky/ambient color based on Fresnel + vec3 reflectTint = mix(ambientColor.rgb * 1.2, vec3(0.6, 0.75, 1.0), 0.6); + glass = mix(glass, reflectTint, fresnel * 0.8); + + // Sharp sun glint on glass + vec3 halfDir = normalize(ldir + viewDir); + float spec = pow(max(dot(norm, halfDir), 0.0), 256.0); + glass += spec * lightColor.rgb * 0.8; + + // Broad warm sheen when sun is nearby + float specBroad = pow(max(dot(norm, halfDir), 0.0), 12.0); + glass += specBroad * lightColor.rgb * 0.12; + + result = glass; + // Fresnel-based transparency: more transparent at oblique angles + alpha = mix(0.4, 0.95, NdotV); + } + + outColor = vec4(result, alpha); } diff --git a/assets/shaders/wmo.frag.spv b/assets/shaders/wmo.frag.spv index cb133eed..372107a2 100644 Binary files a/assets/shaders/wmo.frag.spv and b/assets/shaders/wmo.frag.spv differ diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index 9deefaa2..fd79f5db 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -310,6 +310,8 @@ private: int32_t unlit; int32_t isInterior; float specularIntensity; + int32_t isWindow; + float pad[2]; // pad to 32 bytes }; /** @@ -346,6 +348,7 @@ private: bool alphaTest = false; bool unlit = false; bool isTransparent = false; // blendMode >= 2 + bool isWindow = false; // F_SIDN or F_WINDOW material // For multi-draw: store index ranges struct DrawRange { uint32_t firstIndex; uint32_t indexCount; }; std::vector draws; @@ -558,6 +561,7 @@ private: // Vulkan pipelines VkPipeline opaquePipeline_ = VK_NULL_HANDLE; VkPipeline transparentPipeline_ = VK_NULL_HANDLE; + VkPipeline glassPipeline_ = VK_NULL_HANDLE; // alpha blend + depth write (windows) VkPipeline wireframePipeline_ = VK_NULL_HANDLE; VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE; diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index b770ac08..dc486647 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -213,6 +213,21 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou core::Logger::getInstance().warning("WMORenderer: transparent pipeline not available"); } + // --- Build glass pipeline (alpha blend WITH depth write for windows) --- + glassPipeline_ = PipelineBuilder() + .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({ vertexBinding }, vertexAttribs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx_->getMsaaSamples()) + .setLayout(pipelineLayout_) + .setRenderPass(mainPass) + .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) + .build(device); + // --- Build wireframe pipeline --- wireframePipeline_ = PipelineBuilder() .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), @@ -299,6 +314,7 @@ void WMORenderer::shutdown() { // Destroy pipelines if (opaquePipeline_) { vkDestroyPipeline(device, opaquePipeline_, nullptr); opaquePipeline_ = VK_NULL_HANDLE; } if (transparentPipeline_) { vkDestroyPipeline(device, transparentPipeline_, nullptr); transparentPipeline_ = VK_NULL_HANDLE; } + if (glassPipeline_) { vkDestroyPipeline(device, glassPipeline_, nullptr); glassPipeline_ = VK_NULL_HANDLE; } if (wireframePipeline_) { vkDestroyPipeline(device, wireframePipeline_, nullptr); wireframePipeline_ = VK_NULL_HANDLE; } if (pipelineLayout_) { vkDestroyPipelineLayout(device, pipelineLayout_, nullptr); pipelineLayout_ = VK_NULL_HANDLE; } if (materialDescPool_) { vkDestroyDescriptorPool(device, materialDescPool_, nullptr); materialDescPool_ = VK_NULL_HANDLE; } @@ -511,9 +527,9 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) { unlit = (matFlags & 0x01) != 0; } - // Skip materials that are sky/window panes (render as grey curtains if drawn opaque) - // 0x20 = F_SIDN (night sky window), 0x40 = F_WINDOW - if (matFlags & 0x60) continue; + // Window materials (F_SIDN=0x20, F_WINDOW=0x40) render as + // slightly transparent reflective glass. + bool isWindow = (matFlags & 0x60) != 0; BatchKey key{ reinterpret_cast(tex), alphaTest, unlit }; auto& mb = batchMap[key]; @@ -523,6 +539,7 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) { mb.alphaTest = alphaTest; mb.unlit = unlit; mb.isTransparent = (blendMode >= 2); + mb.isWindow = isWindow; } GroupResources::MergedBatch::DrawRange dr; dr.firstIndex = batch.startIndex; @@ -552,6 +569,7 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) { matData.unlit = mb.unlit ? 1 : 0; matData.isInterior = isInterior ? 1 : 0; matData.specularIntensity = 0.5f; + matData.isWindow = mb.isWindow ? 1 : 0; if (matBuf.info.pMappedData) { memcpy(matBuf.info.pMappedData, &matData, sizeof(matData)); } @@ -1276,7 +1294,8 @@ void WMORenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_, 0, 1, &perFrameSet, 0, nullptr); - bool inTransparentPipeline = false; + // Track which pipeline is currently bound: 0=opaque, 1=transparent, 2=glass + int currentPipelineKind = 0; for (const auto& dl : drawLists) { if (dl.instanceIndex >= instances.size()) continue; @@ -1307,21 +1326,26 @@ void WMORenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const for (const auto& mb : group.mergedBatches) { if (!mb.materialSet) continue; - // Switch pipeline for transparent batches - if (mb.isTransparent && !inTransparentPipeline && transparentPipeline_) { - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, transparentPipeline_); + // Determine which pipeline this batch needs + int neededPipeline = 0; // opaque + if (mb.isWindow && glassPipeline_) { + neededPipeline = 2; // glass (alpha blend + depth write) + } else if (mb.isTransparent && transparentPipeline_) { + neededPipeline = 1; // transparent (alpha blend, no depth write) + } + + // Switch pipeline if needed + if (neededPipeline != currentPipelineKind) { + VkPipeline targetPipeline = activePipeline; + if (neededPipeline == 1) targetPipeline = transparentPipeline_; + else if (neededPipeline == 2) targetPipeline = glassPipeline_; + + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, targetPipeline); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_, 0, 1, &perFrameSet, 0, nullptr); vkCmdPushConstants(cmd, pipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(GPUPushConstants), &push); - inTransparentPipeline = true; - } else if (!mb.isTransparent && inTransparentPipeline) { - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, activePipeline); - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_, - 0, 1, &perFrameSet, 0, nullptr); - vkCmdPushConstants(cmd, pipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, - 0, sizeof(GPUPushConstants), &push); - inTransparentPipeline = false; + currentPipelineKind = neededPipeline; } // Bind material descriptor set (set 1) @@ -2969,6 +2993,7 @@ void WMORenderer::recreatePipelines() { // Destroy old main-pass pipelines (NOT shadow, NOT pipeline layout) if (opaquePipeline_) { vkDestroyPipeline(device, opaquePipeline_, nullptr); opaquePipeline_ = VK_NULL_HANDLE; } if (transparentPipeline_) { vkDestroyPipeline(device, transparentPipeline_, nullptr); transparentPipeline_ = VK_NULL_HANDLE; } + if (glassPipeline_) { vkDestroyPipeline(device, glassPipeline_, nullptr); glassPipeline_ = VK_NULL_HANDLE; } if (wireframePipeline_) { vkDestroyPipeline(device, wireframePipeline_, nullptr); wireframePipeline_ = VK_NULL_HANDLE; } // --- Load shaders --- @@ -3032,6 +3057,20 @@ void WMORenderer::recreatePipelines() { .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) .build(device); + glassPipeline_ = PipelineBuilder() + .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({ vertexBinding }, vertexAttribs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(vkCtx_->getMsaaSamples()) + .setLayout(pipelineLayout_) + .setRenderPass(mainPass) + .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) + .build(device); + wireframePipeline_ = PipelineBuilder() .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))