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.
This commit is contained in:
Kelsi 2026-02-23 00:43:14 -08:00
parent fb4ff46fe3
commit 1b16bcf71f
4 changed files with 99 additions and 16 deletions

View file

@ -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);
}

Binary file not shown.

View file

@ -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<DrawRange> 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;

View file

@ -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<uintptr_t>(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))