// See-through glass mesh pass: single source for every backend.
//
// The third producer of the engine's transparent pass (`PassId::Transparent`,
// after the SSR resolve and before TAA), alongside `glass.slang` and
// `water.slang`. Where a glass pane is a flat pre-baked world-space quad, this
// draws an IMPORTED mesh whose `Material` is flagged `see_through`: the geometry
// comes from the shared scene vertex / index buffers in LOCAL space, so the
// vertex stage applies the per-draw model matrix and the fragment shades off the
// interpolated per-vertex world normal, which is what lets a curved glass facade
// reflect correctly across its surface.
//
// The pass is ray-traced only, by design: what makes the mesh see-through rather
// than the opaque low-roughness glass of Layer 1 is a real per-pixel reflection
// ray, so there is no probe-only variant to fall back to. When RT is off the
// host leaves these meshes in the opaque pass instead of drawing them here.
// Two fragment entries, differing only in where a reflected hit's surface
// parameters come from:
//
// default - the reflected hit takes its flat per-object material tint.
// RT_TEXTURED - the same trace, with reflected hits taking their albedo /
// normal / emissive maps from the bindless pool.
//
// USE_MSAA is a HOST difference rather than a target one: Vulkan reads the
// multisampled main depth while Metal and DirectX read the resolved copy.
//
// Every binding here is deliberately the one glass.slang and water.slang declare
// at the same slot, because the three share a transparent pass: on DirectX the
// RT root signature serves all of them, on Vulkan one set of descriptor set
// layouts, and on Metal the encoder binds the shared reflection inputs once for
// the whole pass. A slot may not move on one side alone.
#ifndef POOL_SIZE
#define POOL_SIZE 1024
#endif
#ifndef MAX_PROBES
#define MAX_PROBES 8
#endif
#ifndef USE_MSAA
#define USE_MSAA 0
#endif
{PROBE_TYPES}
{RT_TYPES}
// Per-frame view shared by every transparent draw. Layout matches
// `TransparentView` (240 B); identical to glass.slang's copy.
struct TransparentView
{
float4x4 vp; // world -> clip (jittered when TAA is on)
float4x4 inv_vp; // clip -> world
float4 camera_pos; // xyz: world-space camera
float2 viewport; // attachment dimensions in pixels
float time; // seconds since startup
float prefilter_mip_count;
// Rows of the rotation from world space into the sky cube's baked frame;
// identity when the sky does not turn.
float4 sky_rot[3];
// Direction toward the scene's sun (the first directional light) and that
// light's colour times its intensity; both zero when the world declares no
// directional light, which the water glint reads as no sun.
float4 sun_dir;
float4 sun_color;
};
// A world direction in the sky cube's own frame; every sky tap goes through it.
#define SKY_DIR(d) float3(dot(view.sky_rot[0].xyz, (d)), \
dot(view.sky_rot[1].xyz, (d)), \
dot(view.sky_rot[2].xyz, (d)))
// Per-mesh tunables. Layout matches `GlassMeshParams` (96 B); `model` is first
// so its 16-byte alignment is satisfied at offset 0.
struct GlassMeshParams
{
float4x4 model; // local -> world
float4 tint; // colour multiplied into the refracted scene
float opacity;
float refraction_strength;
float fresnel_power;
// The mesh's own copy of the sky prefilter mip count, so the ray-miss
// fallback does not depend on which view block is bound. 0 = no
// EnvironmentMap, and the reflection keeps the white rim.
float prefilter_mip_count;
};
// ---- Resource bindings ----
#ifdef DXIL_ABI
// Pinned to the RT root signature in `directx/transparent.rs`, which the pane
// and water producers share; b1 is visible to every stage there, and this
// vertex stage reads the model matrix out of it.
ConstantBuffer<TransparentView> view : register(b0);
ConstantBuffer<GlassMeshParams> params : register(b1);
Texture2D<float4> scene_color : register(t0);
#if USE_MSAA
Texture2DMS<float> scene_depth : register(t1);
#else
Texture2D<float> scene_depth : register(t1);
#endif
TextureCube<float4> prefilter_cube : register(t2);
SamplerState post_samp : register(s0);
SamplerState cube_sampler : register(s2);
float4 scene_sample(float2 uv) { return scene_color.Sample(post_samp, uv); }
float3 prefilter_level(float3 dir, float lod)
{
return prefilter_cube.SampleLevel(cube_sampler, SKY_DIR(dir), lod).rgb;
}
#else
// Metal buffer(5) / buffer(6): the shared per-frame view and the per-mesh
// params, both written with setBytes by the transparent encoder.
[[vk::binding(0, 0)]] ConstantBuffer<TransparentView> view : register(b5);
[[vk::binding(0, 1)]] ConstantBuffer<GlassMeshParams> params : register(b6);
// Declaration order is the Metal texture index, and these are the transparent
// pass's shared slots: the scene snapshot at 0, the resolved depth at 1, the sky
// prefilter cube at 2, and the probe cubes at 3..2+MAX_PROBES.
[[vk::binding(1, 0)]] Sampler2D<float4> scene_color;
#if USE_MSAA
[[vk::binding(2, 0)]] Texture2DMS<float> scene_depth;
#else
[[vk::binding(2, 0)]] Texture2D<float> scene_depth;
#endif
[[vk::binding(5, 2)]] SamplerCube<float4> prefilter_cube;
float4 scene_sample(float2 uv) { return scene_color.Sample(uv); }
float3 prefilter_level(float3 dir, float lod) { return prefilter_cube.SampleLevel(SKY_DIR(dir), lod).rgb; }
#endif
// The reflection-probe set + cube array, from the forward global set: a glass
// mesh falls back to the same local scene capture the forward IBL specular and
// the SSR / RT miss fallback use, rather than only the foreign sky cube.
#ifdef DXIL_ABI
ConstantBuffer<ProbeSet> probe_set : register(b4);
// The array spans MAX_PROBES registers from its base, so it starts clear of the
// ray-tracing SRVs at t4..t10. Always the RT layout here: this pass has no
// probe-only variant.
TextureCube<float4> probe_cubes[MAX_PROBES] : register(t20);
float3 probe_cube_sample_bias(uint i, float3 dir, float lod)
{
return probe_cubes[i].SampleBias(cube_sampler, dir, lod).rgb;
}
#else
[[vk::binding(7, 2)]] ConstantBuffer<ProbeSet> probe_set : register(b7);
#ifdef METAL_ABI
// Metal reaches the cubes through an argument buffer: a resource array at
// global scope emits with no [[texture(n)]], and the compiler then places it
// at whatever slot happens to be unused.
struct ProbeCubes
{
TextureCube<float4> probe_cubes[MAX_PROBES];
};
ParameterBlock<ProbeCubes> probe_cube_set : register(b11);
SamplerState probe_cube_sampler;
float3 probe_cube_sample_bias(uint i, float3 dir, float lod)
{
return probe_cube_set.probe_cubes[i].SampleBias(probe_cube_sampler, dir, lod).rgb;
}
#else
[[vk::binding(8, 2)]] SamplerCube<float4> probe_cubes[MAX_PROBES];
float3 probe_cube_sample_bias(uint i, float3 dir, float lod)
{
return probe_cubes[i].SampleBias(dir, lod).rgb;
}
#endif
#endif
#define PROBE_SET probe_set
#ifndef DXIL_ABI
// A glass mesh never samples a planar reflection -- it is curved, and it traces
// a sharper reflection than a mirror render anyway. The declaration is here
// because slangc numbers Metal's texture and sampler slots by declaration order,
// and the transparent encoder binds one slot map for all three of its producers:
// dropping this would slide the bindless pool's sampler off the index the
// encoder binds it at. glass.slang declares it at the same point for the same
// reason.
[[vk::binding(1, 1)]] Sampler2D<float4> planar_reflection;
#endif
// The ray-tracing scene resources, at the slots glass.slang uses so the inputs
// the transparent encoder binds once are valid for all three producers. On Metal
// they ride the pass's otherwise-free fragment buffers (0..4 and 8..10, since
// 5/6/7 are the view, the params and the probe set); on Vulkan they are a set of
// their own, past the view / params / global sets; on DirectX they follow the
// registers the pass already occupies.
#ifdef DXIL_ABI
ConstantBuffer<RtParams> rt_params : register(b5);
RaytracingAccelerationStructure scene_tlas : register(t4);
ByteAddressBuffer verts : register(t5);
ByteAddressBuffer indices : register(t6);
ByteAddressBuffer sverts : register(t8);
ByteAddressBuffer sidx : register(t9);
StructuredBuffer<RtGeomEntry> geom : register(t10);
float vert_float(uint i) { return asfloat(verts.Load(i * 4u)); }
float svert_float(uint i) { return asfloat(sverts.Load(i * 4u)); }
uint index_at(uint o) { return indices.Load(o * 4u); }
uint skinned_index_word(uint w) { return sidx.Load(w * 4u); }
#else
[[vk::binding(0, 3)]] ConstantBuffer<RtParams> rt_params : register(b0);
[[vk::binding(1, 3)]] RaytracingAccelerationStructure scene_tlas : register(t4);
[[vk::binding(2, 3)]] StructuredBuffer<RtGeomEntry> geom : register(t3);
[[vk::binding(3, 3)]] StructuredBuffer<float> verts : register(t1);
[[vk::binding(4, 3)]] StructuredBuffer<uint> indices : register(t2);
[[vk::binding(5, 3)]] StructuredBuffer<float> sverts : register(t8);
[[vk::binding(6, 3)]] StructuredBuffer<uint> sidx : register(t9);
float vert_float(uint i) { return verts[i]; }
float svert_float(uint i) { return sverts[i]; }
uint index_at(uint o) { return indices[o]; }
uint skinned_index_word(uint w) { return sidx[w]; }
#endif
#ifdef RT_TEXTURED
// The bindless pool. Metal keeps it at buffer(10): buffer(7), where the main
// pass puts it, is the probe set in the transparent pass.
uint nonuniform_index(uint i)
{
__target_switch
{
case metal:
return i;
default:
return NonUniformResourceIndex(i);
}
}
#if defined(METAL_ABI)
struct TexturePool
{
Texture2D<float4> tex_pool[POOL_SIZE];
};
ParameterBlock<TexturePool> pool;
SamplerState pool_sampler;
float3 pool_sample_level0(uint idx, float2 uv)
{
return pool.tex_pool[nonuniform_index(idx)].SampleLevel(pool_sampler, uv, 0.0).rgb;
}
#elif defined(DXIL_ABI)
Texture2D<float4> tex_pool[] : register(t0, space1);
SamplerState pool_sampler : register(s1);
float3 pool_sample_level0(uint idx, float2 uv)
{
return tex_pool[nonuniform_index(idx)].SampleLevel(pool_sampler, uv, 0.0).rgb;
}
#else
[[vk::binding(1, 4)]] Sampler2D<float4> tex_pool[POOL_SIZE];
float3 pool_sample_level0(uint idx, float2 uv)
{
return tex_pool[nonuniform_index(idx)].SampleLevel(uv, 0.0).rgb;
}
#endif
#endif
{PROBE_COMMON}
{RT_TRACE}
// ---- Stage interface ----
struct GlassMeshVertexIn
{
[[vk::location(0)]] float3 pos : POSITION;
[[vk::location(1)]] float3 normal : NORMAL;
};
struct GlassMeshVertexOut
{
[[vk::location(0)]] float3 world_pos : TEXCOORD0;
[[vk::location(1)]] float3 world_normal : TEXCOORD1;
float4 position : SV_Position;
};
[shader("vertex")]
GlassMeshVertexOut glass_mesh_vertex(GlassMeshVertexIn v)
{
GlassMeshVertexOut o;
float4 world = mul(params.model, float4(v.pos, 1.0));
o.world_pos = world.xyz;
// Rigid / uniform-scale model, so `M * n` needs no inverse-transpose. The
// fragment renormalises after interpolation.
o.world_normal = mul((float3x3)params.model, v.normal);
o.position = mul(view.vp, world);
return o;
}
// Depth stored at this pixel by the main pass, for the manual occlusion test.
float glass_mesh_scene_depth(int2 pixel)
{
#if USE_MSAA
return scene_depth.Load(pixel, 0);
#else
return scene_depth.Load(int3(pixel, 0));
#endif
}
// The mesh surface at this fragment: the view-facing interpolated normal
// (two-sided, so a pane of glass lit from behind still Fresnels correctly), the
// fragment's screen UV and the refracted, tinted background behind it.
struct GlassMeshSurface
{
float3 normal;
float2 frag_uv;
float3 refracted;
};
GlassMeshSurface glass_mesh_surface(GlassMeshVertexOut i, float3 view_dir)
{
GlassMeshSurface s;
s.normal = normalize(i.world_normal);
if (dot(s.normal, view_dir) < 0.0)
{
s.normal = -s.normal;
}
float2 vp_dim = max(view.viewport, float2(1.0));
s.frag_uv = i.position.xy / vp_dim;
float2 refract_uv = clamp(s.frag_uv + s.normal.xy * params.refraction_strength,
float2(0.001), float2(0.999));
s.refracted = scene_sample(refract_uv).rgb * params.tint.rgb;
return s;
}
// Schlick Fresnel (F0 = 0.04 dielectric) mix of the reflection over the
// refraction, identical to `glass_resolve` so a mesh and a pane read the same at
// equal inputs.
float4 glass_mesh_resolve(GlassMeshSurface s, float3 view_dir, float3 reflection)
{
float n_dot_v = saturate(dot(s.normal, view_dir));
float rim = pow(1.0 - n_dot_v, max(params.fresnel_power, 1e-3));
float refl_weight = saturate(RT_F0 + 0.96 * rim);
float3 colour = lerp(s.refracted, reflection, refl_weight);
float alpha = saturate(lerp(params.opacity, 1.0, rim));
return float4(colour, alpha);
}
[shader("fragment")]
float4 glass_mesh_rt_fragment(GlassMeshVertexOut i) : SV_Target
{
float3 view_dir = normalize(view.camera_pos.xyz - i.world_pos);
GlassMeshSurface s = glass_mesh_surface(i, view_dir);
int2 pixel = min(int2(i.position.xy), int2(max(view.viewport, float2(1.0))) - int2(1, 1));
if (glass_mesh_scene_depth(pixel) < i.position.z)
{
discard;
}
// A per-pixel reflection ray off the interpolated world-space surface point,
// so a curved facade mirrors real off-screen geometry across its whole span.
// The mesh is excluded from the BLAS (glass does not reflect glass), so the
// trace never self-hits. Glass is smooth, so the trace is sharp and the miss
// falls back to the probe / sky chain.
bool ibl = params.prefilter_mip_count > 0.5;
float3 r = reflect(-view_dir, s.normal);
float3 reflection;
if (!rt_trace_reflection(i.world_pos + s.normal * 0.02, r, ibl,
params.prefilter_mip_count - 1.0, reflection))
{
if (probe_set.count > 0u && probe_set_covers(i.world_pos))
{
reflection = probe_set_specular(i.world_pos, r, 0.0);
}
else
{
reflection = ibl ? prefilter_level(r, 0.0) : float3(1.0);
}
}
return glass_mesh_resolve(s, view_dir, reflection);
}