From 1b16bcf71f79ebf0c485f218ebd6ce551c718d5d Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 23 Feb 2026 00:43:14 -0800 Subject: [PATCH] Add glass pipeline for WMO windows with Fresnel-based transparency Dedicated Vulkan pipeline with alpha blending AND depth writes so windows look transparent at oblique angles without see-through artifacts. Fresnel alpha ranges from 0.4 (grazing) to 0.95 (head-on) with sun glint, reflections, and silver-lining specular. --- assets/shaders/wmo.frag.glsl | 42 +++++++++++++++++- assets/shaders/wmo.frag.spv | Bin 7276 -> 10820 bytes include/rendering/wmo_renderer.hpp | 4 ++ src/rendering/wmo_renderer.cpp | 69 ++++++++++++++++++++++------- 4 files changed, 99 insertions(+), 16 deletions(-) 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 cb133eedc29b61aae3dded56abbd24dac4ae8806..372107a2a212de232df3b694f9e4f9d722214380 100644 GIT binary patch literal 10820 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 Qxvl95w7&0B^-|=204ylx@Bjb+ delta 2236 zcmZvcTZolq6o$XOXZD=87b7tY(^!rK(KOLbF(d_cQAEWk1(6{!W!gY6(iqZ3>_P%B zN{GLUF2Wl-pUkGR!&q8YnpSqwtTd;irqyVfrzU%z?Z0dj{d>QxcfD(UYkjBx-*D~z z{&HP$Mq^5a)SAj^$^+#a3Jt4=Itrr?6|O2?@mxyjrlG+ROe*nv4SUBmw+_x9UUK!| zBDMOo7j6E~$l}PEfGikZc-i31i-&KAJFX!+tu(Uo;ZkwB6?#nNxt!7Z(?KEk8?v>H z^9qewS5xO`e^Y&3-xCCipqJzZ>?+W*edWyXU&0@5UwTnzYEEmiJ6mT|UO`y{&Sb=^ zSRb{Sxo=A!EA(PF0^94iV7Gu$jeh{%RO3&=*|BQ<5(DdU-}?APR$D3`?{V);Q|`mM zPqqF;8a+5=Zc}AB%`PPND0l>%4`#Dewa0Q_P27{`J`OkE_$5qW1y*eqaeF-p)ZDy_ z>F>rp1=PpLX~#Z&Qma&t<1j^@MesSs0E5o}wIeJ^ZXqZ2Jm|0SSHlOvgA}&W3&0%p zrMWf=_t=fyn`Be-26x2+)Hxly z7O0J7<6ToLZ@~V$A8(?m$NsE?+a;#F9&XM$Fejh#TW~A;g!VRE%~{8}ZGf9=j5?K5 z$?qT-^hRILf?e{vz~!hLce(dr)sK=HXP}MAz6UpF78RCZH-THg{a_=Ay!YXoyyb0N zVVi9R&h$Lsfmq0-Xu3J;?(UlR0gC0e082GuKLqLnRPq{plxylfR&`mCzZKpLBL8E! z+SY8&w2litfmuN{Ci^MeMTP$v{5IgNUV!~P8}FXlxlIP5?smA^wyZp@qwfotieqCY zJK(i5`4V4khch|rE108Hqx0AByFql`S?#htfhnpZ~sXf5Ba8FEC*d=mD;H0&6_(*xzs`ISFE~ zPr%h87+}cj80c&iM8TW&K E00aUf-2eap 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))