diff --git a/builder/web/src/lib/trailview/README.md b/builder/web/src/lib/trailview/README.md index 21d2ab043c..6dacc3c428 100644 --- a/builder/web/src/lib/trailview/README.md +++ b/builder/web/src/lib/trailview/README.md @@ -36,7 +36,7 @@ is the palette of the `world` region. | `models/` | The house templates: the prototype's buildings as faces whose coordinates name slots that `houses.wgsl` fills per house; the prototype's trees and plants without three.js (`shape.ts`, `trees.ts`, `trees/`, `plant-shape.ts`, `plants/`); the models of flat faces of the kinds and the dressing (`flat.ts`, lifts, landmarks, rocks, animals, crosses) | | `instances/` | Instances on the drawn surface: producers emit them (`emit.wgsl`), one indirect draw per model and tier (`sort.wgsl`, `draw.wgsl`), a tree's phase models and crown colours, impostors, culling by the last frame's depth (`hiz.wgsl`), point pools of kinds (`points.ts`) | | `vegetation/` | Each tree's phase on the date (`phenology.ts`); the tree models of the phases, shared by the forest, the shrubs and the plantings (`tree-models.ts`); the canopy tiles about the eye (`canopy.ts`) and the forest placed from them each frame (`forest.wgsl`); the ground plants and shrubs about the eye (`plants.wgsl`); the clearing that moves with the eye (`clearing.wgsl`); the plants of the materials planted in rows, on the rows of the material fields (`rows.wgsl`, `ground/fields.wgsl`) | -| `dressing/` | The herds of the packs on the materials that they graze, and rocks on rough and rocky ground, placed each frame (`dressing.wgsl`) | +| `dressing/` | The herds of the packs on the materials that they graze, and rocks on rough and rocky ground, placed each frame (`dressing.wgsl`); the rocks' density and placement, which the ground also paints past their instances (`rocks.wgsl`) | | `light/` | The light keys and weather presets, the look at the eye, `shade.wgsl` (toon bands, haze, `finish()`), the sky with the moon, the stars and the Milky Way, and a solar eclipse where the moon covers the sun (`celestial.ts`, `eclipse.ts`, `night.ts`; `tools/trailview/sky.py` makes `milkyway.webp`, `stars.bin` and `moon.webp`), the object shadow maps near and far from the eye (`shadow.ts`) | | `resolve/` | The render targets, the jitter, TAA and `finish()` | | `rules/` | The regions of `rules.json`, resolved | diff --git a/builder/web/src/lib/trailview/dressing/dressing.ts b/builder/web/src/lib/trailview/dressing/dressing.ts index 911228e92b..f88ca6e8a1 100644 --- a/builder/web/src/lib/trailview/dressing/dressing.ts +++ b/builder/web/src/lib/trailview/dressing/dressing.ts @@ -9,9 +9,9 @@ import { geometry, type Model } from '../models/flat'; import { rock } from '../models/rocks'; import { bits, named, type Rules } from '../rules/regions'; -/** Herds stand within this many metres, rocks within this many; as HERDS and SLABS of dressing.wgsl. */ -const HERD_REACH_M = 1500, ROCK_REACH_M = 300, HERDS = 4, SLABS = 3; -/** Units along a herd cell and a rock point's cell, as HERD_SHIFT and ROCK_SHIFT of dressing.wgsl. */ +/** Herds stand within this many metres, rocks within this many; as HERDS and SLABS of dressing.wgsl and ROCK_REACH_M of rocks.wgsl. */ +const HERD_REACH_M = 1500, ROCK_REACH_M = 1000, HERDS = 4, SLABS = 3; +/** Units along a herd cell and a rock point's cell, as HERD_SHIFT of dressing.wgsl and ROCK_SHIFT of rocks.wgsl. */ const HERD_CELL = 2 ** 14, ROCK_CELL = 2 ** 7; /** Words of `Herd` and bytes of `Dressing` in dressing.wgsl. */ const HERD_WORDS = 8, DRESSING_BYTES = 48; @@ -32,7 +32,8 @@ export class Dressing implements Producer { constructor(rules: Rules, private readonly instances: Instances) { instances.produce(this); const spec = (model: Model, until: number) => ({ tiers: [{ mesh: geometry(model), until }] }); - this.rockModel = instances.add(...(['slab', 'boulder'] as const).flatMap(name => Array.from({ length: SLABS }, (_, k) => spec(rock(name, k + 1), ROCK_REACH_M)))); + // No fade of the rocks' tier: rocks_of ends each rock at its own distance before the reach. + this.rockModel = instances.add(...(['slab', 'boulder'] as const).flatMap(name => Array.from({ length: SLABS }, (_, k) => ({ ...spec(rock(name, k + 1), ROCK_REACH_M), fade: 0 })))); const breeds = new Map(), used = new Set(rules.regions.flatMap(region => region.herds.map(herd => herd.model)).filter(name => name in BREEDS)); for (const name of used) { const first = instances.add(...BREEDS[name].coats.map((_, coat) => spec(animal(name, coat), HERD_REACH_M))); @@ -80,7 +81,7 @@ export class Dressing implements Producer { words.set([this.herdSide, this.rockSide], 2); ints.set([Math.floor(ux / ROCK_CELL) - this.rockSide / 2, Math.floor(uy / ROCK_CELL) - this.rockSide / 2], 4); words[6] = this.rockModel ?? 0; - floats.set([HERD_REACH_M, ROCK_REACH_M], 8); + floats[8] = HERD_REACH_M; r.gpu.device.queue.writeBuffer(r.params, 0, params); } diff --git a/builder/web/src/lib/trailview/dressing/dressing.wgsl b/builder/web/src/lib/trailview/dressing/dressing.wgsl index 2d5b357191..3669725517 100644 --- a/builder/web/src/lib/trailview/dressing/dressing.wgsl +++ b/builder/web/src/lib/trailview/dressing/dressing.wgsl @@ -5,9 +5,9 @@ // Herds: one workgroup per herd cell of HERD_SHIFT. A cell holds the groups of each herd of the region // at its middle, as many per area as the pack's groups per eye's circle of 400 m; a group stands where // its middle's material is one that the herd grazes, its animals about it on such material too. -// Rocks: one thread per point of a grid of ROCK_SHIFT, jittered: a rock stands on rough or rocky -// ground as the region's outcrops, the slope and the hardness of the rock under it say. +// Rocks: one thread per point of the grid of rocks.wgsl. // #include ../terrain/drawn-mesh.wgsl +// #include rocks.wgsl // #include ../ground/look.wgsl // #include ../instances/emit.wgsl @@ -20,9 +20,10 @@ struct Dressing { // The first of the rock models: SLABS slabs, then as many boulders. rock_model: u32, unused: u32, - // Herds stand within x metres, rocks within y. - reach: vec2f, - unused2: vec2f, + // Herds stand within this many metres. + herd_reach: f32, + unused2: f32, + unused3: vec2f, } // A herd of a region, as dressing.ts writes it: its first model and coats, its groups per eye's circle, @@ -39,9 +40,8 @@ struct Herd { @group(0) @binding(8) var dressing: Dressing; @group(0) @binding(9) var herds: array; -// Units along a herd cell edge: about 420 m at 47° N; and along a rock point's cell: about 3.3 m. +// Units along a herd cell edge: about 420 m at 47° N. const HERD_SHIFT: u32 = 14u; -const ROCK_SHIFT: u32 = 7u; // Herds per region, groups of a herd per cell at most, and the area of an eye's circle of 400 m. const HERDS: u32 = 4u; const GROUPS: u32 = 3u; @@ -49,10 +49,8 @@ const CIRCLE_M2: f32 = 502655.0; // A group of 8 spreads over SPREAD_M; nothing stands within CLEAR_M of the eye. const SPREAD_M: f32 = 14.0; const CLEAR_M: f32 = 6.0; -// Rocks: their heights, their kinds, and the slope past which no boulder lies. -const ROCK_HEIGHT_M: vec2f = vec2f(0.25, 1.3); +// Rocks: their kinds. const SLABS: u32 = 3u; -const STEEPEST: f32 = 0.85; // Things keep this far off a road, a path or a track. const ROAD_CLEAR_M: f32 = 1.0; @@ -84,7 +82,7 @@ fn herds_of(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index } let middle = corner + vec2i(vec2f(unit_float(mix_bits(h + 1u)), unit_float(mix_bits(h + 2u))) * f32(size)); let centre = lod_space(vec2f(middle - surface.anchor)); - if (length(centre) > dressing.reach.x || !holds(herd.grazes, finest_cell(centre).r)) { + if (length(centre) > dressing.herd_reach || !holds(herd.grazes, finest_cell(centre).r)) { continue; } let count = u32(herd.size.x + unit_float(mix_bits(h + 3u)) * (herd.size.y - herd.size.x + 1.0)); @@ -115,11 +113,11 @@ fn rocks_of(@builtin(global_invocation_id) id: vec3u) { return; } let point = dressing.rock_origin + vec2i(id.xy); - let h = mix_bits3(point.x, point.y, 77u); - let at = (point << vec2u(ROCK_SHIFT)) + vec2i(vec2f(unit_float(mix_bits(h + 1u)), unit_float(mix_bits(h + 2u))) * f32(1 << ROCK_SHIFT)); + let h = rock_hash(point); + let at = rock_at(point, h); let s = lod_space(vec2f(at - surface.anchor)); let d = length(s); - if (d > dressing.reach.y || d < CLEAR_M) { + if (d > ROCK_REACH_M || d < CLEAR_M) { return; } // The cheap tests first: the view on the level's ground, then the ground's rules, then the slope. @@ -128,30 +126,27 @@ fn rocks_of(@builtin(global_invocation_id) id: vec3u) { return; } let cell = cell_at(s, u32(ceil(lod(s)))); - let kinds = rule(cell.b, slot_kinds() + i32(cell.r)); - let outcrops = rule(cell.b, slot_cliff() + 3).x; - // Rough ground or exposed rock: kinds.a rough, kinds.z rocky. - if ((kinds.a == 0.0 && kinds.z == 0.0) || outcrops <= 0.0) { - return; - } - let hardness = rule(cell.b, slot_hardness() + i32(cell.g)).x; - // As the prototype's near field: hard rock breaks through even on level ground; mapped rock shows as rocks. - let mapped = 0.9 * kinds.z * smoothstep(0.0, 0.6, outcrops) * smoothstep(0.2, 0.6, hardness); - let pick = unit_float(mix_bits(h + 3u)) / 0.55; - if (pick > max(outcrops * (0.3 + 1.4 * hardness), mapped)) { + let pick = rock_pick(h, rock_side(point.y)); + if (pick > rock_density(cell, -1.0)) { return; } let n = drawn_normal(s); let slope = sqrt(max(1.0 - n.z * n.z, 0.0)) / max(n.z, 0.1); - let rocky = max(outcrops * (0.3 + 1.4 * hardness) * clamp(1.4 * slope + 0.6 * (hardness - 0.5) - 0.05, 0.0, 1.0), mapped); - if (slope > STEEPEST || pick > rocky || kept_off(s, ROAD_CLEAR_M)) { + if (pick > rock_density(cell, slope) || kept_off(s, ROAD_CLEAR_M)) { return; } let ground = stand_height(s, at); if (!known(ground)) { return; } - let shape = select(SLABS + mix_bits(h + 4u) % SLABS, mix_bits(h + 5u) % SLABS, unit_float(mix_bits(h + 6u)) < 0.7); - let tall = ROCK_HEIGHT_M.x + pow(unit_float(mix_bits(h + 7u)), 2.0) * (ROCK_HEIGHT_M.y - ROCK_HEIGHT_M.x); - emit(dressing.rock_model + shape, s, ground, tall, 6.2831853 * unit_float(mix_bits(h + 8u)), 1.0, 0.85 + 0.3 * unit_float(mix_bits(h + 9u)), 1.0); + // The distance and the end of the instances' tier (tier_end of emit.wgsl), the same in the eye's + // view and the sun's. A rock under half a pixel tall shows nothing, yet takes a slot of the pool and + // shimmers. + let far = length(render_at(s, ground)); + let tall = rock_height(h); + if (far > rock_end(ROCK_REACH_M * params.detail, h) || tall < 0.5 * frame.pixel * far) { + return; + } + let shape = select(SLABS + mix_bits(h + 4u) % SLABS, mix_bits(h + 5u) % SLABS, rock_slab(h)); + emit(dressing.rock_model + shape, s, ground, tall, rock_yaw(h), 1.0, 0.85 + 0.3 * unit_float(mix_bits(h + 9u)), 1.0); } diff --git a/builder/web/src/lib/trailview/dressing/rocks.wgsl b/builder/web/src/lib/trailview/dressing/rocks.wgsl new file mode 100644 index 0000000000..2037316bc7 --- /dev/null +++ b/builder/web/src/lib/trailview/dressing/rocks.wgsl @@ -0,0 +1,142 @@ +// Strewn rocks, as their instances (rocks_of of dressing.wgsl) and the ground (look.wgsl) share them. +// A rock stands at a point of a jittered grid of ROCK_SHIFT, kept by the density of the ground there. +// Each instance ends at its own distance in the outer ROCK_HANDOFF of the reach, and past it the ground +// paints the rock's footprint: no edge shows where the instances end, and no rock shows twice. +// look.wgsl includes it, and it reads `rule()` of look.wgsl. +// #include ../ground/look.wgsl +// #include ../instances/hash.wgsl + +// Units along a rock point's cell: about 3.3 m at 47° N. +const ROCK_SHIFT: u32 = 7u; +// Rock instances stand within ROCK_REACH_M of the eye, as ROCK_REACH_M of dressing.ts, and end over its +// outer ROCK_HANDOFF. +const ROCK_REACH_M: f32 = 1000.0; +const ROCK_HANDOFF: f32 = 0.45; +// The ground in square metres that a rock point stands for at the rules' share 0.55, at every latitude. +const ROCK_POINT_M2: f32 = 17.0; +// The slope (rise per metre) past which no rock lies. +const STEEPEST: f32 = 0.85; +// Rocks are 0.25 to 1.3 m tall, the share u² between. A rock h tall covers an ellipse of half-axes 2.8 h +// and 2.1 h as a slab and a circle of radius 1.1 h as a boulder (models/rocks.ts), 0.7 of them slabs: +// 6.3 m² on average. A rock sinks into a slope and into the coarser far ground, so the ground paints +// PAINTED of a footprint's size: as much as shows of the instances there, by eye. +const ROCK_HEIGHT_M: vec2f = vec2f(0.25, 1.3); +const SLAB_AXES: vec2f = vec2f(2.8, 2.1); +const BOULDER_RADIUS: f32 = 1.1; +const ROCK_AREA_M2: f32 = 6.3; +const PAINTED: f32 = 0.6; +// The rocks' lit tops and their flanks against them (models/rocks.ts), linear; the sky's light on a face +// against the sun's. +const ROCK_ALBEDO: vec3f = vec3f(0.366, 0.371, 0.332); +const FLANK: f32 = 0.65; +const SKY: f32 = 0.35; + +// Rocks per square metre on ground of `cell` (r the material, g the lithology, b the region) at `slope`: +// on rough ground or exposed rock as the region's outcrops, the slope and the rock's hardness say. A +// negative slope gives the most that any slope gives. +fn rock_density(cell: vec4u, slope: f32) -> f32 { + // kinds.a rough, kinds.z rocky. + let kinds = rule(cell.b, slot_kinds() + i32(cell.r)); + let outcrops = rule(cell.b, slot_cliff() + 3).x; + if ((kinds.a == 0.0 && kinds.z == 0.0) || outcrops <= 0.0 || slope > STEEPEST) { + return 0.0; + } + let hardness = rule(cell.b, slot_hardness() + i32(cell.g)).x; + // As the prototype's near field: hard rock breaks through even on level ground; mapped rock shows as rocks. + let mapped = 0.9 * kinds.z * smoothstep(0.0, 0.6, outcrops) * smoothstep(0.2, 0.6, hardness); + let lie = select(clamp(1.4 * slope + 0.6 * (hardness - 0.5) - 0.05, 0.0, 1.0), 1.0, slope < 0.0); + return max(outcrops * (0.3 + 1.4 * hardness) * lie, mapped) * 0.55 / ROCK_POINT_M2; +} + +// A rock point's hash. +fn rock_hash(point: vec2i) -> u32 { + return mix_bits3(point.x, point.y, 77u); +} + +// Metres along a rock point's cell in row `y` of points, at the row's own latitude (as texel_metres of +// features.wgsl). +fn rock_side(y: i32) -> f32 { + return 2.0 * 3.14159265 * 6371000.0 / 8388608.0 / cosh(3.14159265 * (1.0 - f32(y) / 4194304.0)); +} + +// A rock point keeps a rock where this is below the density: a share of 1 over its cell's area, `side` +// metres a side. Where the density times the area passes 1, as a rules' share above ROCK_POINT_M2 / +// (12.5 m² × cos² of the latitude) does, every point keeps one and the ground holds fewer rocks than its +// rules ask. +fn rock_pick(h: u32, side: f32) -> f32 { + return unit_float(mix_bits(h + 3u)) / (side * side); +} + +// Where a rock point's rock stands, in world units. +fn rock_at(point: vec2i, h: u32) -> vec2i { + return (point << vec2u(ROCK_SHIFT)) + vec2i(vec2f(unit_float(mix_bits(h + 1u)), unit_float(mix_bits(h + 2u))) * f32(1 << ROCK_SHIFT)); +} + +// Whether a rock is a slab, else a boulder; its full height in metres; its yaw. +fn rock_slab(h: u32) -> bool { + return unit_float(mix_bits(h + 6u)) < 0.7; +} + +fn rock_height(h: u32) -> f32 { + return ROCK_HEIGHT_M.x + pow(unit_float(mix_bits(h + 7u)), 2.0) * (ROCK_HEIGHT_M.y - ROCK_HEIGHT_M.x); +} + +fn rock_yaw(h: u32) -> f32 { + return 6.2831853 * unit_float(mix_bits(h + 8u)); +} + +// Where a rock's instance ends, where the instances end at `end`. +fn rock_end(end: f32, h: u32) -> f32 { + return end * (1.0 - ROCK_HANDOFF * unit_float(mix_bits(h + 10u))); +} + +// The share of a pixel at `s`, `far` metres from the eye and `footprint` metres wide, that the ground +// paints as the footprints of the rocks whose instances have ended there, the instances ending at +// `end` (ROCK_REACH_M times the instances' detail). The ground's `cell` and `slope` at `s` stand for +// those of the rocks about it. A footprint is sampled at the pixel's middle with an edge of at most 0.3 +// of its size, as the instances are rasterised, so a far field of rocks stays a speckle as the instances +// make it. Pixels 8 to 16 m wide fade to the mean, which wider pixels take. +fn rocks_painted(s: vec2f, cell: vec4u, slope: f32, far: f32, end: f32, footprint: f32) -> f32 { + // The share of the instances that have ended, as rock_end spreads their ends. + let ended = clamp((far / end - (1.0 - ROCK_HANDOFF)) / ROCK_HANDOFF, 0.0, 1.0); + let density = rock_density(cell, slope); + if (density <= 0.0 || ended <= 0.0) { + return 0.0; + } + let mean = min(density * ROCK_AREA_M2 * PAINTED * PAINTED, 1.0) * ended; + if (footprint > 16.0) { + return mean; + } + // The pixel's point cell; a painted rock reaches at most one cell past its own. + let base = surface.anchor >> vec2u(ROCK_SHIFT); + let home = base + vec2i(floor((units_of(s) + vec2f(surface.anchor - (base << vec2u(ROCK_SHIFT)))) / f32(1 << ROCK_SHIFT))); + var painted = 0.0; + for (var row = -1; row <= 1; row++) { + let side = rock_side(home.y + row); + for (var column = -1; column <= 1; column++) { + let point = home + vec2i(column, row); + let h = rock_hash(point); + if (rock_pick(h, side) > density || far <= rock_end(end, h)) { + continue; + } + let d = s - lod_space(vec2f(rock_at(point, h) - surface.anchor)); + let yaw = rock_yaw(h); + let along = vec2f(dot(d, vec2f(cos(yaw), sin(yaw))), dot(d, vec2f(-sin(yaw), cos(yaw)))); + let axes = min(select(vec2f(BOULDER_RADIUS), SLAB_AXES, rock_slab(h)) * PAINTED * rock_height(h), vec2f(side)); + let edge = min(footprint / min(axes.x, axes.y), 0.3); + painted = max(painted, 1.0 - smoothstep(1.0 - edge, 1.0 + edge, length(along / axes))); + } + } + return mix(painted, mean, smoothstep(8.0, 16.0, footprint)); +} + +// The albedo that a painted rock shows in the light of ground of normal `n`, seen along `view` from the +// eye (render space): its lit top, and its flank toward the eye in its own light. A slab's flank shows +// against its top as its height against its width: 3.4 against 18.5 times the sines of the view. +fn rock_seen(view: vec3f, n: vec3f) -> vec3f { + let s = abs(dot(view, n)); + let c = sqrt(max(1.0 - s * s, 0.0)); + let flank = normalize(vec3f(-view.xy, 0.0) + vec3f(1e-6, 0.0, 0.0)); + let lit = (SKY + max(dot(flank, look.sun.xyz), 0.0)) / (SKY + max(dot(n, look.sun.xyz), 0.0)); + return ROCK_ALBEDO * mix(1.0, FLANK * lit, c / (c + 5.4 * s)); +} diff --git a/builder/web/src/lib/trailview/engine.ts b/builder/web/src/lib/trailview/engine.ts index 5cb2b9f20f..02755489b5 100644 --- a/builder/web/src/lib/trailview/engine.ts +++ b/builder/web/src/lib/trailview/engine.ts @@ -741,7 +741,7 @@ export class TrailView { camera: { right: right.map(v => v * tx), up: up.map(v => v * ty), forward }, codes: this.groundLook.codes, debug: DEBUG_VIEWS.indexOf(this.extras.debug), sample, - jitter, scale, + jitter, scale, detail: this.instances.detail, }); } diff --git a/builder/web/src/lib/trailview/ground/look.wgsl b/builder/web/src/lib/trailview/ground/look.wgsl index 86f879693c..8f5807a442 100644 --- a/builder/web/src/lib/trailview/ground/look.wgsl +++ b/builder/web/src/lib/trailview/ground/look.wgsl @@ -6,6 +6,7 @@ // #include ../light/shadow.wgsl // #include roads.wgsl // #include fields.wgsl +// #include ../dressing/rocks.wgsl // Per ground node MAP × MAP entries per level about the eye, in the windows of the LOD map: the page + 1, or 0. @group(2) @binding(1) var ground_map: array; @@ -628,7 +629,8 @@ fn ground_colour(s: vec2f, relative: vec3f, altitude: f32, level: u32) -> vec3f let surface_size = max(length(dpdx(relative)), length(dpdy(relative))) * scale; var facet = normalize(cross(dpdx(relative), dpdy(relative))); facet *= sign(facet.z); - var n = render_normal(drawn_normal(s)); + let drawn = drawn_normal(s); + var n = render_normal(drawn); // Where the mesh slopes far more steeply than the ground's normals, it is a slope the mesh // smoothed out of detail finer than its triangles: it is cliff, lit like the cliff beside it. let skirt = smoothstep(0.25, 0.5, n.z - facet.z); @@ -767,6 +769,13 @@ fn ground_colour(s: vec2f, relative: vec3f, altitude: f32, level: u32) -> vec3f let shapes = rule(region, slot_cliff() + 8); albedo = patched(albedo, rule(region, slot_cliff() + 1), shapes.xy, g.patches.x, world, n.y, r, surface_size); albedo = patched(albedo, rule(region, slot_cliff() + 2), shapes.zw, g.patches.y, world, n.y, r, surface_size); + // The strewn rocks that their instances no longer show (rocks.wgsl). + let far = length(relative); + let end = ROCK_REACH_M * look.screen.w; + if (far > (1.0 - ROCK_HANDOFF) * end) { + let rise = sqrt(max(1.0 - drawn.z * drawn.z, 0.0)) / max(drawn.z, 0.1); + albedo = mix(albedo, rock_seen(relative / far, n), rocks_painted(s, vec4u(g.material, g.lithology, region, 0u), rise, far, end, footprint)); + } albedo = mix(albedo, road.colour, road.cover * (1.0 - road.share)); if (DETAIL && masoned > 0.0) { let stone = rule(region, slot_lithologies() + i32(g.lithology)).rgb; diff --git a/builder/web/src/lib/trailview/instances/emit.wgsl b/builder/web/src/lib/trailview/instances/emit.wgsl index edbdb55160..0ba8deb5fd 100644 --- a/builder/web/src/lib/trailview/instances/emit.wgsl +++ b/builder/web/src/lib/trailview/instances/emit.wgsl @@ -1,5 +1,5 @@ // What a producer of instances includes: `emit` puts one instance on the drawn surface into the draw of -// its model's tier, and the hashes of a place. A producer is a compute pass that finds its instances and +// its model's tier, with the hashes of a place (hash.wgsl). A producer is a compute pass that finds its instances and // stands them on `stand_height` (drawn-mesh.wgsl); instances.ts sorts what the producers stage into one // list per draw. Group 0 holds bindings 0 to 6; a // producer's own bindings start at 8. Include it with `// #include ../instances/emit.wgsl`. @@ -7,6 +7,7 @@ // #include ../terrain/drawn-mesh.wgsl // #include ../terrain/view.wgsl // #include ../vegetation/phenology.wgsl +// #include hash.wgsl // A model, as instances.ts writes it. struct Model { @@ -111,20 +112,6 @@ const SINK_STEP: f32 = 0.25; // The terrain level whose height gives a tree its species and phase (105 m at 47° N), the same at every // distance. Near the eye the pools hold finer terrain pages only: there a tree takes the drawn ground. const BAND_LEVEL: u32 = 12u; -// The hashes of the producers: a place's values come from whole world cells, the same at every distance. -fn mix_bits(a: u32) -> u32 { - var x = a * 747796405u + 2891336453u; - x = ((x >> ((x >> 28u) + 4u)) ^ x) * 277803737u; - return (x >> 22u) ^ x; -} - -fn mix_bits3(x: i32, y: i32, z: u32) -> u32 { - return mix_bits(u32(x) ^ mix_bits(u32(y) ^ mix_bits(z))); -} - -fn unit_float(h: u32) -> f32 { - return f32(h >> 8u) / 16777216.0; -} // The NDC span of a sphere in front of the eye along the clip axis `row`, `w` the clip w row: the two // planes through the eye that touch it, a − k·b = ±r·|A − k·B| for the rows' xyz A, B and values a, b. diff --git a/builder/web/src/lib/trailview/instances/hash.wgsl b/builder/web/src/lib/trailview/instances/hash.wgsl new file mode 100644 index 0000000000..ec4ca45dfa --- /dev/null +++ b/builder/web/src/lib/trailview/instances/hash.wgsl @@ -0,0 +1,16 @@ +// The hashes of the producers and of what the ground paints for them: a place's values come from whole +// world cells, the same at every distance. Include it with `// #include ../instances/hash.wgsl`. + +fn mix_bits(a: u32) -> u32 { + var x = a * 747796405u + 2891336453u; + x = ((x >> ((x >> 28u) + 4u)) ^ x) * 277803737u; + return (x >> 22u) ^ x; +} + +fn mix_bits3(x: i32, y: i32, z: u32) -> u32 { + return mix_bits(u32(x) ^ mix_bits(u32(y) ^ mix_bits(z))); +} + +fn unit_float(h: u32) -> f32 { + return f32(h >> 8u) / 16777216.0; +} diff --git a/builder/web/src/lib/trailview/instances/instances.ts b/builder/web/src/lib/trailview/instances/instances.ts index e2f340635f..e1b9949dd3 100644 --- a/builder/web/src/lib/trailview/instances/instances.ts +++ b/builder/web/src/lib/trailview/instances/instances.ts @@ -189,8 +189,7 @@ export class Instances { private readonly sunTally: number[][] = []; private quad: [number, number, number] = [0, 0, 0]; private uploaded = false; - /** The tier distances' factor of the detail: below 1 while the staged instances fill the pool. */ - private detail = 1; + private detail_ = 1; /** The factor of the tier distances in the last frame. */ private lod = 1; private frame = 0; @@ -209,6 +208,11 @@ export class Instances { /** The day of the year (0 is 1 January), which sets the phase of each tree. */ day = 0; + /** The tier distances' factor of the detail: below 1 while the staged instances fill the pool. */ + get detail(): number { + return this.detail_; + } + get stats(): InstanceStats { return this.stats_; } @@ -224,7 +228,7 @@ export class Instances { } private lodFor(pixels: number): number { - return this.detail * pixels / REFERENCE_PIXELS; + return this.detail_ * pixels / REFERENCE_PIXELS; } /** Layout entries 0 to 7 of a producer's group 0, for its compute stage. */ @@ -359,7 +363,7 @@ export class Instances { r.gpu.device.queue.writeBuffer(r.coarse, 0, this.casterTables[sun.proxies ? 1 : 0]); } const params = new ArrayBuffer(PARAMS_BYTES), v = r.views[view]; - new Float32Array(params).set([this.lod, 0, this.detail, this.day, 0, sun.least]); + new Float32Array(params).set([this.lod, 0, this.detail_, this.day, 0, sun.least]); const words = new Uint32Array(params); words[1] = view === SUN ? SUN_CAPACITY : CAPACITY; words[4] = view; @@ -493,7 +497,7 @@ export class Instances { this.stats_ = { staged: tally[0], dropped: tally[1], lod: this.lod, perTier, impostors: this.atlas.size, casters }; // A full pool draws its tiers nearer, until it has room again. const used = tally[0] / CAPACITY; - this.detail = used > ROOM[1] ? this.detail * 0.9 : used < ROOM[0] ? Math.min(1, this.detail / 0.97) : this.detail; + this.detail_ = used > ROOM[1] ? this.detail_ * 0.9 : used < ROOM[0] ? Math.min(1, this.detail_ / 0.97) : this.detail_; this.specs.forEach((spec, m) => { if (spec.impostor && tally[4 + m * TIERS + spec.tiers.length] > 0) this.wantImpostor(r, m, frame); }); diff --git a/builder/web/src/lib/trailview/light/look.ts b/builder/web/src/lib/trailview/light/look.ts index 585545cafe..562c947127 100644 --- a/builder/web/src/lib/trailview/light/look.ts +++ b/builder/web/src/lib/trailview/light/look.ts @@ -24,6 +24,8 @@ export interface LookInput { /** This frame's jitter in clip space, and the render scale. */ jitter: [number, number]; scale: number; + /** The instances' detail this frame (Instances.detail), where the ground's rock paint starts (rocks.wgsl). */ + detail: number; } /** `Look` of shade.wgsl. */ @@ -43,7 +45,7 @@ export function lookUniform(input: LookInput): ArrayBuffer { floats.set([...input.camera.right, 0, ...input.camera.up, 0, ...input.camera.forward, 0], 48); words.set(input.codes, LOOK_CODES / 4); words.set([input.debug, input.sample, 0, 0], 64); - floats.set([...input.jitter, input.scale, 0], 68); + floats.set([...input.jitter, input.scale, input.detail], 68); floats.set([...input.skySun, light.glow], 72); floats.set([...eye.sea, 0], 76); return data; diff --git a/builder/web/src/lib/trailview/light/shade.wgsl b/builder/web/src/lib/trailview/light/shade.wgsl index a2073b59d0..88e12e083b 100644 --- a/builder/web/src/lib/trailview/light/shade.wgsl +++ b/builder/web/src/lib/trailview/light/shade.wgsl @@ -34,7 +34,8 @@ struct Look { codes: vec4u, // x: the debug view of terrain.wgsl. y: the jittered frame's sample, which turns the dithers. z, w: unused. modes: vec4u, - // xy: this frame's jitter in clip space. z: render pixels per output pixel, the render scale. w: unused. + // xy: this frame's jitter in clip space. z: render pixels per output pixel, the render scale. w: the + // instances' detail this frame (Params of emit.wgsl). screen: vec4f, // xyz: toward the sun, which the sky's glow follows also while the moon is the light. w: the warm // glow about a low sun.