The features added in Rounds A–D were correct but expensive. The hot
path per frame was sky_color() called from apply_fog for EVERY distant
pixel — 4-octave cloud fbm + star hash + sun/moon disc per fragment,
hundreds of thousands of pixels per frame. Profile-driven cuts that
keep all features but stop paying for them in the wrong places:
1. apply_fog now mixes terrain toward sky_dome (cheap gradient) not
sky_color (gradient + clouds + sun + moon + stars). Distant terrain
still fades to the right-direction sky color at every time of day;
the per-pixel cost drops by ~80%. Full sky_color still runs for
the SKY BACKGROUND pass where it's actually paid for.
2. Sky pipeline draws AFTER terrain with depth_compare = LessEqual.
The full-screen sky was previously written first then over-painted
by terrain — sky's expensive fragment shader ran on every screen
pixel. Now it only runs on pixels with no terrain in front of them
(depth = 1.0 cleared), which on most views is 30–60% of the screen
instead of 100%.
3. fbm2 reduced from 4 → 3 octaves. Negligible visual change at the
scales we sample, ~25% cheaper per cloud-pixel.
4. Cloud branch skips entirely when day_strength < 0.05 (full night).
Clouds invisible at night anyway, fbm + smoothstep + mix skipped.
5. In-game FPS HUD (top-right corner):
- Telemetry struct gains frame_dt_ms (EMA-smoothed in app.rs
with coefficient 0.85 so the number is readable, not flickery).
- wasm bridge: get_frame_dt_ms().
- main.js setupFpsHud() polls it at 5Hz, color-coded:
green ≤ 18ms (≥55fps), amber 18-33ms, red beyond.
- Reads what THE GAME measures, not the browser's
requestAnimationFrame which gets throttled to 1 Hz on
unfocused windows.
No features removed. God rays, FXAA, ACES tonemap, bounce baking,
specular materials, leaf translucency — all still there. Tests:
63 passing. Wasm release clean.
449 lines
17 KiB
WebGPU Shading Language
449 lines
17 KiB
WebGPU Shading Language
// Voxel-game world/sky/outline pipelines.
|
||
//
|
||
// The constants `DAY_PERIOD` and `SUN_OFFSET` are NOT defined here —
|
||
// they are injected at the top of this file at module-load time by
|
||
// `crate::shader_source::wgsl_constants_header()`. The same values
|
||
// drive `crate::sim::lighting`, so the visual sun direction can never
|
||
// disagree with the mechanical one consumed by mob burn / plant growth /
|
||
// shade pathfinding.
|
||
//
|
||
// Layout (top to bottom):
|
||
// 1. Camera uniform + accessors
|
||
// 2. Sky horizon math (one source of truth: sky_dome)
|
||
// 3. Atmosphere extras (clouds, sun/moon discs, stars) → sky_color
|
||
// 4. Terrain lighting decomposition (ambient, sun, fog, leaf jitter)
|
||
// 5. Pipelines: terrain (vs_main/fs_main), sky background, outline.
|
||
|
||
// ---------------- 1. Camera ----------------
|
||
|
||
// Camera uniform — layout mirrored in `render::uniform::CameraUniform`.
|
||
// The `frame` vec4 is the "per-frame scalars" slot; the accessor
|
||
// functions below name each component so callsites read as intent
|
||
// rather than `frame.x` / `frame.y` / etc.
|
||
//
|
||
// frame.x scene_time seconds since session start
|
||
// frame.y exposure_bias tonemap multiplier (default 1.0)
|
||
// frame.z reserved for future fog density / weather etc.
|
||
// frame.w reserved
|
||
struct Camera {
|
||
view_proj: mat4x4<f32>,
|
||
inv_view_proj: mat4x4<f32>,
|
||
eye: vec4<f32>,
|
||
frame: vec4<f32>,
|
||
};
|
||
|
||
@group(0) @binding(0) var<uniform> camera: Camera;
|
||
|
||
fn scene_time() -> f32 { return camera.frame.x; }
|
||
fn exposure_bias() -> f32 { return camera.frame.y; }
|
||
fn eye_world() -> vec3<f32> { return camera.eye.xyz; }
|
||
|
||
// ---------------- 2. Sky horizon math (shared with ambient) ----------------
|
||
|
||
fn sun_direction(t: f32) -> vec3<f32> {
|
||
let a = (t / DAY_PERIOD + SUN_OFFSET) * 6.28318530718;
|
||
return normalize(vec3<f32>(cos(a), sin(a), 0.25));
|
||
}
|
||
|
||
fn day_strength(sun: vec3<f32>) -> f32 {
|
||
return smoothstep(-0.05, 0.20, sun.y);
|
||
}
|
||
|
||
fn twilight_amount(sun: vec3<f32>) -> f32 {
|
||
let above = smoothstep(-0.10, 0.05, sun.y);
|
||
let high = smoothstep(0.05, 0.30, sun.y);
|
||
return above - high;
|
||
}
|
||
|
||
fn sun_tint(sun: vec3<f32>) -> vec3<f32> {
|
||
let twi = twilight_amount(sun);
|
||
return mix(vec3<f32>(1.00, 0.95, 0.85), vec3<f32>(1.00, 0.55, 0.30), twi);
|
||
}
|
||
|
||
// Horizon → zenith gradient. Used by both:
|
||
// - the sky background fragment shader (with clouds/sun layered on)
|
||
// - terrain hemisphere ambient (sample the dome in surface-normal dir
|
||
// so vertical faces inherit the bright daytime horizon).
|
||
//
|
||
// One function = one source of truth for "what color is the sky at this
|
||
// angle?" — when the palette is tuned, both consumers update together.
|
||
fn sky_dome(dir: vec3<f32>, sun: vec3<f32>) -> vec3<f32> {
|
||
let day = day_strength(sun);
|
||
let twi = twilight_amount(sun);
|
||
let zenith_day = vec3<f32>(0.30, 0.55, 0.88);
|
||
let zenith_night = vec3<f32>(0.02, 0.03, 0.10);
|
||
// Dusky-purple zenith during twilight — in reality the entire sky
|
||
// tints warm at sunset, not just the horizon. Without this, top
|
||
// faces stay cold blue while the horizon visibly burns orange.
|
||
let zenith_twi = vec3<f32>(0.45, 0.28, 0.40);
|
||
let horizon_day = vec3<f32>(0.82, 0.92, 0.99);
|
||
let horizon_twi = vec3<f32>(1.00, 0.55, 0.28);
|
||
let horizon_night = vec3<f32>(0.03, 0.04, 0.10);
|
||
let zenith_base = mix(zenith_night, zenith_day, day);
|
||
let zenith = mix(zenith_base, zenith_twi, twi * 0.65);
|
||
let horizon = mix(mix(horizon_night, horizon_day, day), horizon_twi, twi);
|
||
let up = clamp(dir.y, -1.0, 1.0);
|
||
let gradient_t = pow(max(up, 0.0), 0.55);
|
||
return mix(horizon, zenith, gradient_t);
|
||
}
|
||
|
||
// ---------------- 3. Atmosphere extras ----------------
|
||
|
||
fn hash21(p: vec2<f32>) -> f32 {
|
||
return fract(sin(dot(p, vec2<f32>(127.1, 311.7))) * 43758.5453);
|
||
}
|
||
|
||
fn noise2(p: vec2<f32>) -> f32 {
|
||
let i = floor(p);
|
||
let f = fract(p);
|
||
let u = f * f * (3.0 - 2.0 * f);
|
||
let a = hash21(i);
|
||
let b = hash21(i + vec2<f32>(1.0, 0.0));
|
||
let c = hash21(i + vec2<f32>(0.0, 1.0));
|
||
let d = hash21(i + vec2<f32>(1.0, 1.0));
|
||
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||
}
|
||
|
||
fn fbm2(p_in: vec2<f32>) -> f32 {
|
||
var p = p_in;
|
||
var v = 0.0;
|
||
var amp = 0.5;
|
||
// 3 octaves (was 4): noticeable per-pixel cost reduction with
|
||
// negligible visual difference at the scales we sample.
|
||
for (var i = 0; i < 3; i = i + 1) {
|
||
v = v + amp * noise2(p);
|
||
p = p * 2.07;
|
||
amp = amp * 0.5;
|
||
}
|
||
return v;
|
||
}
|
||
|
||
// High-frequency hash on view direction, threshold to keep ~0.2% lit.
|
||
fn star_field(dir: vec3<f32>) -> f32 {
|
||
if (dir.y <= 0.0) { return 0.0; }
|
||
let cell = floor(dir * 220.0);
|
||
let h = fract(sin(dot(cell, vec3<f32>(12.9898, 78.233, 37.719))) * 43758.5453);
|
||
return step(0.997, h);
|
||
}
|
||
|
||
// Composite sky: dome + below-horizon dim + stars + clouds + sun + moon.
|
||
// `dir` is the view direction from camera into the scene (unit vector).
|
||
fn sky_color(dir: vec3<f32>) -> vec3<f32> {
|
||
let t = scene_time();
|
||
let sun = sun_direction(t);
|
||
let day = day_strength(sun);
|
||
let twi = twilight_amount(sun);
|
||
let night = clamp(1.0 - day, 0.0, 1.0);
|
||
|
||
var sky = sky_dome(dir, sun);
|
||
|
||
// Below-horizon slight darken so the world below the player still feels grounded.
|
||
let up = clamp(dir.y, -1.0, 1.0);
|
||
let below = step(up, 0.0) * 0.2;
|
||
sky = sky * (1.0 - below);
|
||
|
||
// Stars: only visible once the sun is well below the horizon. The
|
||
// old `1 - day` gate showed stars during twilight (when day < 1 but
|
||
// the sky was still bright). The new gate is tied to sun altitude
|
||
// directly so stars fade in *after* civil twilight, not during it.
|
||
let star_fade = 1.0 - smoothstep(-0.22, 0.04, sun.y);
|
||
if (star_fade > 0.05) {
|
||
let st = star_field(dir);
|
||
let twinkle = 0.7 + 0.3 * sin(t * 6.0 + dir.x * 100.0 + dir.z * 130.0);
|
||
sky = sky + vec3<f32>(st * star_fade * twinkle);
|
||
}
|
||
|
||
// Cloud layer — fbm scrolled across an imaginary plane high above.
|
||
// Skip entirely at night: clouds are invisible without sun light,
|
||
// and saving the fbm + smoothstep + mix on every dark sky pixel
|
||
// is a real perf win at midnight.
|
||
if (dir.y > 0.05 && day > 0.05) {
|
||
let proj = dir.xz / dir.y;
|
||
let scroll = vec2<f32>(t * 0.004, t * 0.0015);
|
||
let n = fbm2(proj * 0.50 + scroll);
|
||
let mask = smoothstep(0.50, 0.78, n);
|
||
let cloud_lit = mix(vec3<f32>(0.30, 0.30, 0.35), vec3<f32>(1.00, 0.97, 0.92), day);
|
||
let cloud_twi = vec3<f32>(1.00, 0.60, 0.45);
|
||
let cloud_col = mix(cloud_lit, cloud_twi, twi * 0.7);
|
||
sky = mix(sky, cloud_col, mask * (0.55 + 0.25 * day));
|
||
}
|
||
|
||
// Sun disc + halo. The disc softens and spreads as the sun nears
|
||
// the horizon — atmospheric scattering blooms the apparent disc at
|
||
// low angles. Sharp pin-point at zenith, big soft circle at dusk.
|
||
let sun_col = sun_tint(sun);
|
||
let cos_s = max(dot(dir, sun), 0.0);
|
||
let alt = clamp(sun.y, 0.0, 1.0);
|
||
let disc_sharpness = mix(160.0, 800.0, alt);
|
||
let disc_intensity = mix(2.2, 1.5, alt);
|
||
let disc = pow(cos_s, disc_sharpness) * disc_intensity * smoothstep(-0.05, 0.05, sun.y);
|
||
let halo = pow(cos_s, mix(3.0, 5.0, alt)) * mix(0.35, 0.20, alt) * day;
|
||
sky = sky + sun_col * (disc + halo);
|
||
|
||
// Moon disc — opposite the sun, faint white, night only.
|
||
let moon = -sun;
|
||
let cos_m = max(dot(dir, moon), 0.0);
|
||
let moon_disc = pow(cos_m, 700.0) * 0.9;
|
||
let moon_halo = pow(cos_m, 24.0) * 0.06;
|
||
sky = sky + vec3<f32>(0.86, 0.89, 0.96) * (moon_disc + moon_halo) * night;
|
||
|
||
return sky;
|
||
}
|
||
|
||
// ---------------- 4. Terrain lighting decomposition ----------------
|
||
//
|
||
// Each piece is a named function so `fs_main` reads as a pipeline:
|
||
//
|
||
// lit = base_color *
|
||
// ( ambient_term × ambient_strength + direct_sun_term × sun_color )
|
||
// × ao × leaf_jitter
|
||
// final = apply_fog( lit, dist, view_dir )
|
||
|
||
/// Physically-decoupled hemisphere ambient:
|
||
///
|
||
/// ambient = sky_radiance(N) × sky_vis + bounce × (1 − sky_vis)
|
||
///
|
||
/// Both visibility *and* bounce color are baked per vertex by
|
||
/// `sim::lighting::compute_ambience` (same 8 cosine-weighted rays
|
||
/// produce sky_vis and average bounce color in one pass). The
|
||
/// shader's job is just to combine the baked geometric terms with the
|
||
/// time-of-day radiometric terms.
|
||
///
|
||
/// - Open plain at noon: sky_vis≈1 → full bright sky-color ambient
|
||
/// - Deep cave at noon: sky_vis≈0 → bounce_color × sun strength
|
||
/// - Red brick wall corner: bounce_color carries the red tint from
|
||
/// the wall, giving a faint red glow on
|
||
/// adjacent surfaces (real GI hint, free at
|
||
/// runtime).
|
||
fn ambient_term(normal: vec3<f32>, sun: vec3<f32>, day: f32, sky_vis: f32, bounce_albedo: vec3<f32>) -> vec3<f32> {
|
||
let sky = sky_dome(normal, sun);
|
||
let bounce_strength = mix(0.04, 1.0, day);
|
||
let bounce = bounce_albedo * bounce_strength * sun_tint(sun);
|
||
return sky * sky_vis + bounce * (1.0 - sky_vis);
|
||
}
|
||
|
||
/// Material lookup. Keyed by `bounce_mat.a` (set at mesh-build time
|
||
/// from `Block::material_id`). Keep in sync with the Rust enum.
|
||
struct Material {
|
||
specular: f32,
|
||
emission: vec3<f32>,
|
||
};
|
||
|
||
fn material_for(id_f: f32) -> Material {
|
||
let id = u32(id_f + 0.5);
|
||
var m: Material;
|
||
m.specular = 0.0;
|
||
m.emission = vec3<f32>(0.0);
|
||
if (id == 1u) {
|
||
// Ice / snow — slight specular highlight.
|
||
m.specular = 0.45;
|
||
} else if (id == 2u) {
|
||
// Reserved: emissive (future glowing blocks).
|
||
m.emission = vec3<f32>(1.00, 0.70, 0.40);
|
||
}
|
||
return m;
|
||
}
|
||
|
||
/// Direct-sun Lambert term, gated by sun visibility above the horizon.
|
||
/// Returns a scalar 0..1 — caller multiplies by `sun_tint(sun)` for
|
||
/// color.
|
||
///
|
||
/// NOTE: This is an *unoccluded* Lambert. The mechanical sunlight
|
||
/// predicate (`crate::sim::lighting::is_in_direct_sun`) is more
|
||
/// accurate — it ray-traces the voxel grid. When real shadows land,
|
||
/// this function will multiply by an occlusion factor that approximates
|
||
/// that ray test (shadow map / voxel raymarch / etc).
|
||
fn direct_sun_term(normal: vec3<f32>, sun: vec3<f32>) -> f32 {
|
||
let ndl = max(dot(normal, sun), 0.0);
|
||
let sun_visible = smoothstep(-0.05, 0.10, sun.y);
|
||
return ndl * sun_visible;
|
||
}
|
||
|
||
/// Per-pixel value-noise jitter on leaf surfaces so the canopy doesn't
|
||
/// read as a flat green. 0.88 .. 1.06 range.
|
||
fn leaf_jitter(world_pos: vec3<f32>) -> f32 {
|
||
let n = fract(sin(dot(floor(world_pos * 1.3), vec3<f32>(12.9898, 78.233, 37.719))) * 43758.5453);
|
||
return 0.88 + n * 0.18;
|
||
}
|
||
|
||
/// Approximated leaf translucency. When the sun is behind the leaf
|
||
/// (relative to the surface normal), some light transmits through to
|
||
/// the viewer's side and the leaf glows softly with sun-tinted color.
|
||
/// Cheap stand-in for full subsurface scattering; reads as "sun
|
||
/// through canopy" without per-pixel sampling.
|
||
fn leaf_translucency(normal: vec3<f32>, sun: vec3<f32>, day: f32) -> f32 {
|
||
// peak transmittance when sun rakes across the leaf plane — i.e.
|
||
// when sun is roughly perpendicular to the normal (grazing).
|
||
let grazing = 1.0 - abs(dot(normal, sun));
|
||
// backlit bias: prefer light coming from BEHIND the leaf so we
|
||
// don't double-count the front-side Lambert from direct_sun_term.
|
||
let back = max(dot(-normal, sun), 0.0);
|
||
return grazing * back * 0.55 * day;
|
||
}
|
||
|
||
/// Distance fog. Returns 0 (no fog) → 1 (fully obscured).
|
||
fn fog_factor(dist: f32) -> f32 {
|
||
let fog_start = 90.0;
|
||
let fog_end = 320.0;
|
||
return clamp((dist - fog_start) / (fog_end - fog_start), 0.0, 1.0);
|
||
}
|
||
|
||
/// Blend `lit` toward the sky-dome gradient along the view ray. Uses
|
||
/// the cheap `sky_dome` (just horizon→zenith gradient + zenith warm
|
||
/// tint at twilight) rather than the full `sky_color` (4-octave cloud
|
||
/// fbm + star field + sun + moon disc) which was an ENORMOUS per-
|
||
/// pixel cost on every distant fragment. Visually the difference is
|
||
/// minor — distant terrain still fades into the right-direction sky
|
||
/// gradient at every time of day — but fragment cost drops dramatically.
|
||
/// The full `sky_color` still runs for the SKY BACKGROUND pass where
|
||
/// it's only paid for pixels with no terrain in front of them.
|
||
fn apply_fog(lit: vec3<f32>, dist: f32, view_dir: vec3<f32>) -> vec3<f32> {
|
||
let t = fog_factor(dist);
|
||
if (t <= 0.001) {
|
||
return lit;
|
||
}
|
||
let sun = sun_direction(scene_time());
|
||
let twi = twilight_amount(sun);
|
||
let sky = sky_dome(-view_dir, sun);
|
||
let fog_col = mix(sky, sky * sun_tint(sun), twi * 0.45);
|
||
return mix(lit, fog_col, t);
|
||
}
|
||
|
||
// ---------------- 5a. Terrain pipeline ----------------
|
||
|
||
struct VsIn {
|
||
@location(0) pos: vec3<f32>,
|
||
@location(1) color: vec3<f32>,
|
||
@location(2) normal: vec3<f32>,
|
||
@location(3) leaf: f32,
|
||
@location(4) ao: f32,
|
||
@location(5) sky_vis: f32,
|
||
@location(6) bounce_mat: vec4<f32>, // rgb = baked bounce color, a = material id
|
||
};
|
||
|
||
struct VsOut {
|
||
@builtin(position) clip: vec4<f32>,
|
||
@location(0) world_pos: vec3<f32>,
|
||
@location(1) color: vec3<f32>,
|
||
@location(2) normal: vec3<f32>,
|
||
@location(3) leaf: f32,
|
||
@location(4) ao: f32,
|
||
@location(5) sky_vis: f32,
|
||
@location(6) bounce_mat: vec4<f32>,
|
||
};
|
||
|
||
@vertex
|
||
fn vs_main(in: VsIn) -> VsOut {
|
||
var pos = in.pos;
|
||
if (in.leaf > 0.5) {
|
||
let t = scene_time();
|
||
let phase = pos.x * 0.35 + pos.z * 0.27 + pos.y * 0.11;
|
||
let sway = sin(t * 1.6 + phase) * 0.045;
|
||
let sway2 = cos(t * 1.1 + phase * 1.3) * 0.035;
|
||
pos.x = pos.x + sway;
|
||
pos.z = pos.z + sway2;
|
||
pos.y = pos.y + sway * 0.25;
|
||
}
|
||
var out: VsOut;
|
||
out.clip = camera.view_proj * vec4<f32>(pos, 1.0);
|
||
out.world_pos = pos;
|
||
out.color = in.color;
|
||
out.normal = in.normal;
|
||
out.leaf = in.leaf;
|
||
out.ao = in.ao;
|
||
out.sky_vis = in.sky_vis;
|
||
out.bounce_mat = in.bounce_mat;
|
||
return out;
|
||
}
|
||
|
||
@fragment
|
||
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
||
let t = scene_time();
|
||
let sun = sun_direction(t);
|
||
let day = day_strength(sun);
|
||
|
||
let n = normalize(in.normal);
|
||
|
||
// Lighting pipeline (all per-fragment scalars from baked vertex
|
||
// attributes + time-of-day functions):
|
||
// ambient (sky_vis-weighted + baked bounce_color)
|
||
// + direct_sun (Lambert × sun_visible)
|
||
// + specular (Phong, gated by material)
|
||
// + emission (gated by material)
|
||
// then base color × AO × leaf jitter, then fog.
|
||
let bounce_albedo = in.bounce_mat.rgb;
|
||
let mat = material_for(in.bounce_mat.a);
|
||
|
||
let ambient = ambient_term(n, sun, day, in.sky_vis, bounce_albedo);
|
||
let ambient_strength = mix(0.35, 1.00, day);
|
||
let sun_term = direct_sun_term(n, sun);
|
||
let sun_col = sun_tint(sun);
|
||
|
||
// Specular: Phong with half-vector. Gated by material + sun visibility.
|
||
let to_eye = normalize(eye_world() - in.world_pos);
|
||
let half_v = normalize(sun + to_eye);
|
||
let spec_dot = max(dot(n, half_v), 0.0);
|
||
let sun_visible = smoothstep(-0.05, 0.10, sun.y);
|
||
let specular = pow(spec_dot, 48.0) * mat.specular * sun_visible;
|
||
|
||
let lighting = ambient * ambient_strength + sun_col * sun_term;
|
||
var lit = in.color * lighting * in.ao + sun_col * specular + mat.emission;
|
||
|
||
if (in.leaf > 0.5) {
|
||
lit = lit * leaf_jitter(in.world_pos);
|
||
let trans = leaf_translucency(n, sun, day);
|
||
lit = lit + in.color * sun_col * trans;
|
||
}
|
||
|
||
// Reuse `to_eye` from the specular block (normalized eye-to-fragment).
|
||
// Fog needs the *raw* distance though, so recompute that here.
|
||
let to_eye_raw = eye_world() - in.world_pos;
|
||
let dist = length(to_eye_raw);
|
||
let view_dir = -to_eye_raw / max(dist, 0.0001);
|
||
|
||
let color = apply_fog(lit, dist, view_dir);
|
||
return vec4<f32>(color, 1.0);
|
||
}
|
||
|
||
// ---------------- 5b. Sky background (full-screen triangle) ----------------
|
||
|
||
struct SkyOut {
|
||
@builtin(position) clip: vec4<f32>,
|
||
@location(0) ndc: vec2<f32>,
|
||
};
|
||
|
||
@vertex
|
||
fn vs_sky(@builtin(vertex_index) idx: u32) -> SkyOut {
|
||
var corners = array<vec2<f32>, 3>(
|
||
vec2<f32>(-1.0, -1.0),
|
||
vec2<f32>( 3.0, -1.0),
|
||
vec2<f32>(-1.0, 3.0),
|
||
);
|
||
let p = corners[idx];
|
||
var out: SkyOut;
|
||
out.clip = vec4<f32>(p, 1.0, 1.0);
|
||
out.ndc = p;
|
||
return out;
|
||
}
|
||
|
||
@fragment
|
||
fn fs_sky(in: SkyOut) -> @location(0) vec4<f32> {
|
||
let far_h = camera.inv_view_proj * vec4<f32>(in.ndc.x, in.ndc.y, 1.0, 1.0);
|
||
let world_pos = far_h.xyz / far_h.w;
|
||
let dir = normalize(world_pos - eye_world());
|
||
// alpha = 0 flags this pixel as "sky" for the god-rays mask pass;
|
||
// terrain pipeline writes alpha = 1 where it overdraws.
|
||
return vec4<f32>(sky_color(dir), 0.0);
|
||
}
|
||
|
||
// ---------------- 5c. Outline ----------------
|
||
|
||
@vertex
|
||
fn vs_outline(@location(0) pos: vec3<f32>) -> @builtin(position) vec4<f32> {
|
||
return camera.view_proj * vec4<f32>(pos, 1.0);
|
||
}
|
||
|
||
@fragment
|
||
fn fs_outline() -> @location(0) vec4<f32> {
|
||
return vec4<f32>(0.05, 0.05, 0.07, 1.0);
|
||
}
|