Transmission
A glass cube refracts the scene behind it in screen space: the frame is rendered, blurred into a pyramid, and read back through Snell refraction, chromatic dispersion and a Fresnel-weighted environment reflection.
import { env_lod, sample_env } from "./env-common.wgsl";
// The floor is real geometry, not a trick inside the sky shader: a plane mesh rasterized
// with depth, so it occludes the cube when the camera drops below it and, more
// importantly, so the checker it refracts is a surface at a known distance instead of a
// direction at infinity. Its checker is the ruler the whole example is read against.
struct Floor {
view_projection: mat4x4f,
model: mat4x4f,
camera_position: vec3f,
/** Tiles per world unit. */
checker_scale: f32,
sun_direction: vec3f,
/** Half-angle of the floor's reflection cone, in radians. */
reflection_roughness: f32,
horizon_color: vec3f,
/** Angle covered by one texel of the environment map: 2*PI / map_width. */
texel_angle: f32,
base_color: vec3f,
/** Distance where the checker has faded fully into the horizon haze. */
fade_distance: f32,
env_size: vec2f,
};
@group(0) @binding(0) var<uniform> floor_uniforms: Floor;
@group(0) @binding(1) var env_tex: texture_2d<f32>;
@group(0) @binding(2) var env_samp: sampler;
struct VertexOut {
@builtin(position) position: vec4f,
@location(0) world_position: vec3f,
};
@vertex
fn vs_main(@location(0) position: vec3f) -> VertexOut {
let world = floor_uniforms.model * vec4f(position, 1.0);
var out: VertexOut;
out.position = floor_uniforms.view_projection * world;
out.world_position = world.xyz;
return out;
}
// Box-filtered checkerboard (iq): the pattern is integrated over the pixel's footprint
// instead of point-sampled, so the tiles stay antialiased all the way to the horizon
// without a single extra tap.
fn checker_box(p: vec2f, w: vec2f) -> f32 {
let i = 2.0 * (abs(fract((p - 0.5 * w) * 0.5) - 0.5) - abs(fract((p + 0.5 * w) * 0.5) - 0.5)) / w;
return 0.5 - 0.5 * i.x * i.y;
}
@fragment
fn fs_main(in: VertexOut) -> @location(0) vec4f {
let normal = vec3f(0.0, 1.0, 0.0);
let to_camera = floor_uniforms.camera_position - in.world_position;
let view_distance = length(to_camera);
let view = to_camera / max(view_distance, 1e-4);
let tile = in.world_position.xz * floor_uniforms.checker_scale;
let checker = checker_box(tile, fwidth(tile) + vec2f(1e-3));
var color = floor_uniforms.base_color * (0.55 + checker * 1.5);
// Soft key from the same sun the sky bakes, plus a flat sky ambient. Enough shaping to
// read as a lit surface; the interesting light in this scene is the transmitted kind.
let sun = normalize(floor_uniforms.sun_direction);
color *= 0.55 + 0.45 * clamp(sun.y, 0.0, 1.0) * clamp(dot(normal, sun) * 0.5 + 0.5, 0.0, 1.0);
// A dielectric floor is mostly diffuse but never matte: the grazing sky reflection is
// what glues the plane to the environment behind it.
let reflected = reflect(-view, normal);
let lod = env_lod(floor_uniforms.reflection_roughness, dpdx(reflected), dpdy(reflected), floor_uniforms.texel_angle);
let reflection = sample_env(env_tex, env_samp, reflected, lod, floor_uniforms.env_size);
let facing = clamp(dot(view, normal), 0.0, 1.0);
let fresnel = 0.04 + 0.96 * pow(1.0 - facing, 5.0);
color = mix(color, reflection, fresnel * 0.85);
// The plane is finite; fading it into the environment's own horizon haze hides the far
// edge where the mesh stops and the baked sky takes over.
let fade = clamp(view_distance / floor_uniforms.fade_distance, 0.0, 1.0);
color = mix(color, floor_uniforms.horizon_color, fade * fade);
return vec4f(color, 1.0);
}