Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions assets/materials/grass_instanced.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,13 @@
// UBO (EN-013); cascade sun shadows from sample_sun_shadow
// (EN-016).
//
// Canonical mesh layout (model space):
// v0 (-w, 0, 0) normal=+Z color.r=0 // root left, plane 1
// v1 (+w, 0, 0) normal=+Z color.r=0 // root right, plane 1
// v2 ( 0, h, 0) normal=+Z color.r=1 // tip, plane 1
// v3 ( 0, 0,-w) normal=+X color.r=0 // root left, plane 2
// v4 ( 0, 0,+w) normal=+X color.r=0 // root right, plane 2
// v5 ( 0, h, 0) normal=+X color.r=1 // tip, plane 2
// 12 indices = 4 triangles (front + back of each plane). The cross
// shape covers any horizontal viewing angle even with backface
// culling on (the Opaque bucket rule for instanced pipelines).
// Canonical mesh layout (model space) — Round-4: two-segment tapered
// blades with a bow (root pair → narrower mid pair → tip point, per
// crossed plane; 10 verts, 36 indices). See GRASS_BLADE_VERTS in
// main.ts for the authoritative table. color.r is the tip weight
// (0 root → 0.55 mid → 1 tip). The cross shape covers any horizontal
// viewing angle even with backface culling on (the Opaque bucket rule
// for instanced pipelines).
//
// IMPORTANT: this file is the source of truth, but the engine's
// `compileMaterialInstanced` takes a WGSL string (not a path), so
Expand Down Expand Up @@ -151,9 +148,14 @@ fn fs_main(in: VsOut) -> OpaqueOut {
let shadow = sample_sun_shadow(in.world_pos);
let direct = view.sun_color.rgb * direct_w * cloud * shadow;

// Tip blades catch more sun (real grass: tips bleached, roots
// shadowed).
let albedo = in.blade_tint * (0.7 + 0.3 * in.tip_weight);
// Round-4 — strong root→tip gradient (real grass: roots sit in
// shadowed olive-dark, tips are sun-bleached toward straw). The
// squared tip weight keeps the lower half dark and lets the top
// quarter bloom.
let tip2 = in.tip_weight * in.tip_weight;
let root_col = in.blade_tint * vec3<f32>(0.42, 0.50, 0.38);
let tip_col = in.blade_tint * vec3<f32>(1.12, 1.08, 0.72);
let albedo = mix(root_col, tip_col, tip2);

// Transmission tint — slightly warmer than albedo for the
// luminous-leaf look. Strength is grass.base.w.
Expand Down
29 changes: 28 additions & 1 deletion assets/materials/terrain.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ struct TerrainParams {
// x = noise_freq (wraps per metre), y = slope_threshold (cos),
// z = ridge_height (m), w = pale_strength
knobs: vec4<f32>,
// Round-4 — x = river centre z (m), y = river half-width incl. bank
// fade start, z = bank fade width, w = waterline y (bed blend fades
// out above it).
river: vec4<f32>,
};
@group(2) @binding(11) var<uniform> tp: TerrainParams;

Expand Down Expand Up @@ -81,7 +85,30 @@ fn fs_main(in: VsOut) -> OpaqueOut {
// and dirt dominates.
let up_dot = clamp(n.y, 0.0, 1.0);
let slope_t = smoothstep(tp.knobs.y, tp.knobs.y + 0.15, up_dot);
let surface = mix(tp.dirt.rgb, grass, slope_t);
var surface = mix(tp.dirt.rgb, grass, slope_t);

// Round-4 — macro moisture patches (~45 m wavelength): dry
// straw-olive sweeps + slightly darker lush pockets, so the field
// stops reading as one uniform green lawn. Loosely matches the
// grass blades' moisture tinting (they use the same idea at
// scatter time).
let patch_n = fbm2(in.world_pos.xz * 0.022);
let dry_t = smoothstep(0.52, 0.78, patch_n);
let wet_t = smoothstep(0.42, 0.18, patch_n);
surface = mix(surface, surface * vec3<f32>(1.30, 1.12, 0.62), dry_t * 0.55);
surface = mix(surface, surface * vec3<f32>(0.80, 0.92, 0.82), wet_t * 0.35);

// Round-4 — riverbed: blend to wet mud inside the river channel,
// gated on world height so only the carved bed (below the
// waterline) and a thin damp bank strip take it. Pebble-scale
// noise keeps the mud from being one flat brown.
let dzr = abs(in.world_pos.z - tp.river.x);
let bed_x = 1.0 - smoothstep(tp.river.y, tp.river.y + tp.river.z, dzr);
let bed_y = 1.0 - smoothstep(tp.river.w, tp.river.w + 0.35, in.world_pos.y);
let bed_t = bed_x * bed_y;
let pebble = value_noise(in.world_pos.xz * 3.1);
let mud = vec3<f32>(0.23, 0.17, 0.11) * (0.75 + pebble * 0.5);
surface = mix(surface, mud, bed_t);

// Height tint — pale near ridges. Use world.y / ridge_height as
// the lerp control, capped.
Expand Down
97 changes: 80 additions & 17 deletions assets/materials/water.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,39 @@ fn micro_normal(world_xz: vec2<f32>, t: f32) -> vec3<f32> {
return normalize(vec3<f32>(nx, 4.0, nz)); // bias toward +Y
}

// Round-3 — bilinear sample of the impulse field. The field is a
// non-filterable R32Float at 0.5 m/texel; the nearest-neighbour
// textureLoad the shader used before rendered every splat as hard
// half-metre squares (the "pixelated fading" while wading). wgpu
// won't filter R32Float, so the 2×2 lerp is done by hand.
fn sample_impulse(world_xz: vec2<f32>) -> f32 {
let dims = vec2<f32>(textureDimensions(impulse_tex));
let uv = clamp(world_xz / 128.0 + vec2<f32>(0.5), vec2<f32>(0.0), vec2<f32>(1.0));
let p = uv * dims - vec2<f32>(0.5);
let base = vec2<i32>(floor(p));
let f = p - floor(p);
let hi = vec2<i32>(dims) - vec2<i32>(1);
let lo = vec2<i32>(0);
let v00 = textureLoad(impulse_tex, clamp(base, lo, hi), 0).r;
let v10 = textureLoad(impulse_tex, clamp(base + vec2<i32>(1, 0), lo, hi), 0).r;
let v01 = textureLoad(impulse_tex, clamp(base + vec2<i32>(0, 1), lo, hi), 0).r;
let v11 = textureLoad(impulse_tex, clamp(base + vec2<i32>(1, 1), lo, hi), 0).r;
return mix(mix(v00, v10, f.x), mix(v01, v11, f.x), f.y);
}

@fragment
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
// Round-3 — impulse splashes agitate the surface: they boost the
// micro-normal weight locally (chop where something disturbed the
// water) instead of painting flat white (the old 85% white mix).
let imp = clamp(sample_impulse(in.world_pos.xz), 0.0, 1.0);

// Tier 4 — perturb the per-vertex normal with a fragment-level
// micro-normal so close-range water reads as crinkled.
let micro = micro_normal(in.world_pos.xz, frame.time);
let n_base = normalize(in.world_normal);
let n = normalize(mix(n_base, micro, water_params.knobs.w));
let micro_w = clamp(water_params.knobs.w + imp * 0.45, 0.0, 0.8);
let n = normalize(mix(n_base, micro, micro_w));
let v = normalize(view.camera_pos.xyz - in.world_pos);

// Round-2 fix: derive screen UV from the fragment position instead of
Expand All @@ -106,13 +132,27 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {

// Phase 4c — shoreline fade. Compute depth + water column FIRST
// so Tier 4's Beer-Lambert absorption can use it.
//
// Round-3 fix: do NOT unproject through view.inv_proj — the engine's
// perspective is GL-convention and mat4_invert of it produces garbage
// unprojections (the same failure that collapsed the shadow cascades;
// see docs/shadow-cascade-and-ssao-fixes.md). The column this block
// used to compute was ~0 everywhere, which silently zeroed the
// absorption, caustics, shore fade AND the old rim — a root cause of
// the uniform "milk film" look. Instead linearize the two depths
// straight from the projection constants:
// z_clip = A·z_view + B·w, w_clip = -z_view
// → z_view = -B / (d + A), A = proj[2][2], B = proj[3][2]
// (The ABI's linearize_depth helper remaps d*2-1 first, which is
// wrong for this engine's depth range — don't use it here.)
let depth_ix = vec2<i32>(in.clip_pos.xy);
let scene_d = textureLoad(scene_depth_tex, depth_ix, 0);
let ndc_xy = vec2<f32>(screen_uv.x * 2.0 - 1.0, 1.0 - screen_uv.y * 2.0);
let floor_v = view.inv_proj * vec4<f32>(ndc_xy, scene_d, 1.0);
let surf_v = view.inv_proj * vec4<f32>(ndc_xy, in.clip_pos.z, 1.0);
let floor_z = floor_v.z / floor_v.w;
let surf_z = surf_v.z / surf_v.w;
let proj_a = view.proj[2][2];
let proj_b = view.proj[3][2];
let floor_z = -proj_b / (scene_d + proj_a);
let surf_z = -proj_b / (in.clip_pos.z + proj_a);
// Both z are negative (camera looks down -Z); bed is farther → more
// negative → column = surf - floor > 0 metres of water along the ray.
let column = max(surf_z - floor_z, 0.0);

// Refraction — perturb screen UV by wave normal xz; the offset
Expand All @@ -133,7 +173,9 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
let s3 = sin( cp.x * 4.3 + frame.time * 1.5);
let caustic = max(s1, max(s2, s3)) * 0.5 + 0.5; // 0..1
let caustic_t = smoothstep(0.0, 0.05, column) * (1.0 - smoothstep(0.6, 1.5, column));
refracted = refracted * mix(1.0, 1.0 + caustic * 1.4, caustic_t);
// Round-3: gain 1.4 → 0.5 — at 1.4 the caustics brightened the bed up
// to 2.4× and were a big part of the milky wash.
refracted = refracted * mix(1.0, 1.0 + caustic * 0.5, caustic_t);

// Tier 4 — Beer-Lambert absorption. Refracted scene colour fades
// exponentially through the water column, replaced by the tint
Expand All @@ -155,10 +197,19 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
let r = normalize(vec3<f32>(r_raw.x, max(r_raw.y, 0.08), r_raw.z));
let sky = sample_env(r, water_params.knobs.z);

// Round-3 — planar reflections (EN-011). The probe mirror-renders the
// cached-model world (trees / house / banks) across the water plane
// each frame; alpha 0 where no geometry was written, so the sky env
// shows through the gaps. Without this the water only ever reflected
// the featureless prefiltered sky — nothing anchored it to the scene.
let refl_uv = clamp(screen_uv + n.xz * 0.08, vec2<f32>(0.001), vec2<f32>(0.999));
let planar = textureSampleLevel(planar_reflection_tex, planar_reflection_samp, refl_uv, 0.0);
let refl = mix(sky, planar.rgb, planar.a);

// Schlick Fresnel — F0 ≈ 0.02 for water, capped (see above).
let cos_theta = max(dot(n, v), 0.0);
let fresnel = min(0.02 + (1.0 - 0.02) * pow(1.0 - cos_theta, 5.0), 0.60);
var water = mix(absorbed, sky, fresnel);
var water = mix(absorbed, refl, fresnel);

// Foam on wave crests (slope proxy: low n.y), faded toward grazing
// views — without the fade the whole far half of the river picks up
Expand All @@ -174,14 +225,26 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
let rim = (1.0 - shore_t) * water_params.knobs.y;
water = mix(water, vec3<f32>(0.96, 0.99, 1.0), rim);

// Phase 7 — impulse ripples.
let imp_uv = clamp(in.world_pos.xz / 128.0 + vec2<f32>(0.5), vec2<f32>(0.0), vec2<f32>(0.999));
let imp_dims = textureDimensions(impulse_tex);
let imp_ix = vec2<i32>(imp_uv * vec2<f32>(imp_dims));
let imp = textureLoad(impulse_tex, imp_ix, 0).r;
let imp_mix = clamp(imp * 1.2, 0.0, 1.0);
water = mix(water, vec3<f32>(0.96, 0.99, 1.0), imp_mix * 0.85);

let alpha = mix(0.45, 0.92, shore_t);
// Phase 7 / Round-3 — impulse wake foam. The normal agitation above
// carries most of the effect; this is just a soft churned-water
// brightening, capped well below white so it never reads as paint.
water = mix(water, vec3<f32>(0.88, 0.93, 0.95), imp * 0.22);

// Round-3 — sun glint: tight Blinn specular from the sun direction.
// sample_env at any useful LOD blurs the sun disc into a wash, so a
// direct term is what actually sells the surface as water. Additive
// HDR, applied after all the mixes so foam / wake can't dim it.
let l = normalize(view.sun_dir.xyz);
let h = normalize(l + v);
let n_dot_h = max(dot(n, h), 0.0);
let glint = pow(n_dot_h, 380.0) * view.sun_dir.w;
water = water + view.sun_color.rgb * glint * 1.5;

// Round-3 — the shader already composites the refracted scene colour
// itself, so hardware alpha-blending the same background in AGAIN
// just washes the surface out (the milky-film look). Own the pixel
// (alpha ≈ 1) and only fade alpha over the first 15 cm of water
// column so the shoreline still dissolves smoothly into the bank.
let alpha = 0.97 * shore_t;
return vec4<f32>(water, alpha);
}
Binary file modified assets/models/house.glb
Binary file not shown.
Binary file modified assets/models/prop_tree.glb
Binary file not shown.
Binary file added assets/models/prop_tree2.glb
Binary file not shown.
Binary file added assets/models/prop_tree3.glb
Binary file not shown.
42 changes: 42 additions & 0 deletions assets/sounds/SOURCES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Sound sources

All files converted to 16-bit mono 44.1 kHz WAV (the engine's `parse_wav`
decodes 8/16-bit PCM only). Conversion: ffmpeg, no processing beyond
downmix/bit-depth (`tools` has no audio step — regenerate by hand if
sources change).

## Unvanquished legacy assets (GPL-compatible, same line as our models)

From github.com/UnvanquishedAssets/res-legacy_src.dpkdir (FLAC sources):

| File(s) | Source path |
|---|---|
| `alien{0-4}_die{1-3}.wav` | `sound/player/level{0-4}/death{1-3}.flac` |
| `alien{0-4}_pain.wav` | `sound/player/level{0-4}/pain50_1.flac` |
| `alien0_attack.wav` | `models/weapons/level0/flash0.flac` |
| `alien1_attack.wav` | `sound/player/level1/grab.flac` |
| `alien{2-4}_attack.wav` | `models/weapons/level{2-4}/flash0.flac` |
| `impact_flesh.wav` | `models/weapons/level2/impactflesh0.flac` |
| `ricochet{1,2}.wav` | `models/weapons/rifle/ricochet{0,1}.flac` |
| `player_pain{1,2}.wav` | `sound/player/human_bsuit/pain{50,100}_1.flac` |
| `player_die{1,2}.wav` | `sound/player/human_bsuit/death{1,2}.flac` |

Kind mapping: 0=dretch(level0) 1=mantis(level1) 2=marauder(level2)
3=dragoon(level3) 4=tyrant(level4).

## Sonniss GDC 2024 Game Audio Bundle (royalty-free game-use license)

License: royalty-free incl. commercial use, no attribution; NOT
redistributable as a sound library — only these processed/renamed
game-ready files are committed, never pack folders.

| File | Source |
|---|---|
| `rifle_fire2.wav` | Dramatic Cat — SVD Dragunov, DESIGNED Single Shot Core Long |
| `blaster_fire.wav` | BluezoneCorp — Sci Fi Weapon, gun_shot_008 |
| `splash1.wav` | BluezoneCorp — Designed Water, impact_006 (first 1.2 s) |

## Pre-existing (provenance predates this file)

`rifle_fire.wav` (superseded by rifle_fire2 but kept), `dretch_attack.wav`,
`pickup.wav`, `menu.wav`, `game.wav`, `ambient.ogg`.
Binary file added assets/sounds/alien0_attack.wav
Binary file not shown.
Binary file added assets/sounds/alien0_die1.wav
Binary file not shown.
Binary file added assets/sounds/alien0_die2.wav
Binary file not shown.
Binary file added assets/sounds/alien0_die3.wav
Binary file not shown.
Binary file added assets/sounds/alien0_pain.wav
Binary file not shown.
Binary file added assets/sounds/alien1_attack.wav
Binary file not shown.
Binary file added assets/sounds/alien1_die1.wav
Binary file not shown.
Binary file added assets/sounds/alien1_die2.wav
Binary file not shown.
Binary file added assets/sounds/alien1_die3.wav
Binary file not shown.
Binary file added assets/sounds/alien1_pain.wav
Binary file not shown.
Binary file added assets/sounds/alien2_attack.wav
Binary file not shown.
Binary file added assets/sounds/alien2_die1.wav
Binary file not shown.
Binary file added assets/sounds/alien2_die2.wav
Binary file not shown.
Binary file added assets/sounds/alien2_die3.wav
Binary file not shown.
Binary file added assets/sounds/alien2_pain.wav
Binary file not shown.
Binary file added assets/sounds/alien3_attack.wav
Binary file not shown.
Binary file added assets/sounds/alien3_die1.wav
Binary file not shown.
Binary file added assets/sounds/alien3_die2.wav
Binary file not shown.
Binary file added assets/sounds/alien3_die3.wav
Binary file not shown.
Binary file added assets/sounds/alien3_pain.wav
Binary file not shown.
Binary file added assets/sounds/alien4_attack.wav
Binary file not shown.
Binary file added assets/sounds/alien4_die1.wav
Binary file not shown.
Binary file added assets/sounds/alien4_die2.wav
Binary file not shown.
Binary file added assets/sounds/alien4_die3.wav
Binary file not shown.
Binary file added assets/sounds/alien4_pain.wav
Binary file not shown.
Binary file added assets/sounds/blaster_fire.wav
Binary file not shown.
Binary file added assets/sounds/impact_flesh.wav
Binary file not shown.
Binary file added assets/sounds/player_die1.wav
Binary file not shown.
Binary file added assets/sounds/player_die2.wav
Binary file not shown.
Binary file added assets/sounds/player_pain1.wav
Binary file not shown.
Binary file added assets/sounds/player_pain2.wav
Binary file not shown.
Binary file added assets/sounds/ricochet1.wav
Binary file not shown.
Binary file added assets/sounds/ricochet2.wav
Binary file not shown.
Binary file added assets/sounds/rifle_fire2.wav
Binary file not shown.
Binary file added assets/sounds/splash1.wav
Binary file not shown.
23 changes: 23 additions & 0 deletions assets/textures/external/SOURCES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# External texture sources

All assets in this folder are **CC0 (public domain)** — no attribution
required, compatible with this repo's GPLv3 distribution. Provenance
recorded for hygiene:

| Folder | Asset | Source | License |
|---|---|---|---|
| `pine_bark/` | Pine Bark (diff / nor_gl / rough, 1K + 2K JPG) | https://polyhaven.com/a/pine_bark | CC0 |
| `forrest_ground_01/` | Forest Ground 01 (diff / nor_gl / rough, 2K JPG) | https://polyhaven.com/a/forrest_ground_01 | CC0 |
| `leafset/LeafSet004/` | Leaf Set 004 (Color + Opacity, 2K PNG) | https://ambientcg.com/view?id=LeafSet004 | CC0 |

Consumed by `tools/build-props.ts`:
- `pine_bark` 1K maps embed directly (as `image/jpeg`) into the tree GLB
trunk material.
- `LeafSet004` Color+Opacity are composited into the leaf-card cutout
texture (`externalLeafCard`) — real scanned leaves stamped into
branch-cluster cards.
- `forrest_ground_01` is downloaded for the upcoming terrain splat pass
(EN-014 texture arrays); not consumed yet.

If this folder is missing, `build-props.ts` silently falls back to the
procedural textures — a fresh checkout still builds.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading