carry: the runtime and render3d work Maroon Lake builds against
Input.text, App.monitor_count / window_to_monitor / window_fixed, gl_sleep_us, and the grass and collide changes, uncommitted on main and depended on by the game; carried here so the language work starts from what the game actually uses. main's working tree is untouched.
This commit is contained in:
parent
86492064aa
commit
e6f565a6c4
17 changed files with 622 additions and 34 deletions
|
|
@ -12,6 +12,12 @@ const COL_CAP: int = 120000
|
|||
var col_x: words = null
|
||||
var col_z: words = null
|
||||
var col_r: words = null
|
||||
# What a collider occupies VERTICALLY: y0 its base, y1 its top, metres, float bits. A circle used
|
||||
# to be an infinite pillar - you could not climb a boulder, and a knee-high rock stopped you dead,
|
||||
# because there was no height to compare against. col_add keeps that shape (a span from far below
|
||||
# to far above) so every existing caller behaves exactly as it did; col_add_h gives a real one.
|
||||
var col_y0: words = null
|
||||
var col_y1: words = null
|
||||
var col_n: int = 0
|
||||
var col_side: int = 0 # cells per side
|
||||
var col_start: words = null # per cell: first index into col_sorted (side*side + 1)
|
||||
|
|
@ -19,10 +25,19 @@ var col_sorted: words = null
|
|||
var col_built: bool = false
|
||||
var col_out: words = null # the resolved position (x, z)
|
||||
|
||||
function col_add(x: int, z: int, r: int) -> void {
|
||||
if col_x == null { col_x = words(COL_CAP); col_z = words(COL_CAP); col_r = words(COL_CAP); col_out = words(2) }
|
||||
const COL_LOW: int = 0xCB800000 # -16777216.0: below any ground
|
||||
const COL_HIGH: int = 0x4B800000 # 16777216.0: above any sky
|
||||
function col_add(x: int, z: int, r: int) -> void { col_add_h(x, z, r, COL_LOW, COL_HIGH) }
|
||||
# a collider that occupies only y0 .. y1: a body above its top walks over it, a body below its base
|
||||
# passes under, and col_top_at reports it as something to stand on
|
||||
function col_add_h(x: int, z: int, r: int, y0: int, y1: int) -> void {
|
||||
if col_x == null {
|
||||
col_x = words(COL_CAP); col_z = words(COL_CAP); col_r = words(COL_CAP)
|
||||
col_y0 = words(COL_CAP); col_y1 = words(COL_CAP); col_out = words(2)
|
||||
}
|
||||
if col_n >= COL_CAP { return }
|
||||
col_x[col_n] = x; col_z[col_n] = z; col_r[col_n] = r
|
||||
col_y0[col_n] = y0; col_y1[col_n] = y1
|
||||
col_n += 1
|
||||
col_built = false
|
||||
}
|
||||
|
|
@ -52,7 +67,10 @@ function col_build() -> void {
|
|||
}
|
||||
|
||||
# push (px, pz) with radius pr out of every circle it overlaps; the result is in col_out
|
||||
function col_resolve(px: int, pz: int, pr: int) -> bool {
|
||||
function col_resolve(px: int, pz: int, pr: int) -> bool { return col_resolve_at(px, pz, pr, COL_LOW, COL_HIGH) }
|
||||
# the same, for a body that occupies feet .. head: a collider whose span misses that is not in the
|
||||
# way at all. This is what lets a hiker stand on top of a boulder rather than inside it.
|
||||
function col_resolve_at(px: int, pz: int, pr: int, feet: int, head: int) -> bool {
|
||||
col_out[0] = px; col_out[1] = pz
|
||||
if not col_built or col_n == 0 { return false }
|
||||
var x = px; var z = pz
|
||||
|
|
@ -71,6 +89,9 @@ function col_resolve(px: int, pz: int, pr: int) -> bool {
|
|||
let ex = f_sub(x, col_x[i]); let ez = f_sub(z, col_z[i])
|
||||
let d2 = f_add(f_mul(ex, ex), f_mul(ez, ez))
|
||||
let rr = f_add(col_r[i], pr)
|
||||
# nothing to push out of if the body is wholly above its top or below its base
|
||||
if not f_ls(feet, col_y1[i]) { continue }
|
||||
if not f_gt(head, col_y0[i]) { continue }
|
||||
if f_ls(d2, f_mul(rr, rr)) {
|
||||
let d = f_sqrt(d2)
|
||||
# The UNIT normal out of this circle. (ex, ez) / d is always unit for d > 0,
|
||||
|
|
@ -98,6 +119,35 @@ function col_resolve(px: int, pz: int, pr: int) -> bool {
|
|||
col_out[0] = x; col_out[1] = z
|
||||
return moved
|
||||
}
|
||||
# The highest collider top under (px, pz) that a body at `feet` could be standing on or step up to:
|
||||
# tops above `reach` are a wall, not a step. F_ZERO-safe: returns `floor` when there is nothing, so
|
||||
# a caller can pass the terrain height and use the answer directly as the ground.
|
||||
function col_top_at(px: int, pz: int, pr: int, feet: int, reach: int, floor: int) -> int {
|
||||
var top = floor
|
||||
if not col_built or col_n == 0 { return top }
|
||||
let cx = col_cell_of(px, ter_ox); let cz = col_cell_of(pz, ter_oz)
|
||||
let limit = f_add(feet, reach)
|
||||
for dz in 0 .. 3 {
|
||||
let zc = cz + dz - 1
|
||||
if zc < 0 or zc >= col_side { continue }
|
||||
for dx in 0 .. 3 {
|
||||
let xc = cx + dx - 1
|
||||
if xc < 0 or xc >= col_side { continue }
|
||||
let c = zc * col_side + xc
|
||||
for k in col_start[c] .. col_start[c + 1] {
|
||||
let i = col_sorted[k]
|
||||
let ex = f_sub(px, col_x[i]); let ez = f_sub(pz, col_z[i])
|
||||
let d2 = f_add(f_mul(ex, ex), f_mul(ez, ez))
|
||||
let rr = f_add(col_r[i], pr)
|
||||
if not f_ls(d2, f_mul(rr, rr)) { continue }
|
||||
let t = col_y1[i]
|
||||
if f_gt(t, limit) { continue } # too tall to step onto: it is a wall
|
||||
if f_gt(t, top) { top = t }
|
||||
}
|
||||
}
|
||||
}
|
||||
return top
|
||||
}
|
||||
# is the segment from (x0,z0) to (x1,z1) clear of every circle (a camera line of sight)?
|
||||
function col_clear(x0: int, z0: int, x1: int, z1: int, r: int) -> bool {
|
||||
let steps = 6
|
||||
|
|
|
|||
|
|
@ -470,21 +470,45 @@ function gvk_compare_op(f: int) -> int {
|
|||
if f == GL_ALWAYS { return VK_COMPARE_OP_ALWAYS }
|
||||
return VK_COMPARE_OP_LESS_OR_EQUAL
|
||||
}
|
||||
# The texture mip bias DLSS needs. DLSS draws the scene at a fraction of the output resolution, so
|
||||
# every texture picks its mip for THAT resolution - and then the upscaler has no detail left to
|
||||
# reconstruct, which is what "blurry and muddy" is. NVIDIA's requirement is to bias the mip
|
||||
# selection back toward the output resolution:
|
||||
#
|
||||
# bias = log2(renderWidth / displayWidth) - 1
|
||||
#
|
||||
# which is -2.0 at Performance and about -1.6 at Quality. It is applied ONLY while DLSS is live: a
|
||||
# plain spatial upscale has no temporal accumulation to hide the aliasing a negative bias brings,
|
||||
# so biasing there would trade blur for shimmer.
|
||||
function gvk_mip_bias() -> int {
|
||||
# R3D_NO_MIPBIAS=1 puts it back the way it was, so one build can be compared against itself
|
||||
if r3d_env_has("R3D_NO_MIPBIAS") { return F_ZERO }
|
||||
if not r3d_dlss_live() { return F_ZERO }
|
||||
let rw = r3d_dlss_render_w()
|
||||
if rw <= 0 or gl_w <= 0 or rw >= gl_w { return F_ZERO }
|
||||
# log2 from the natural log the runtime has: log2(x) = ln(x) * 1/ln(2)
|
||||
return f_sub(f_mul(f_log(f_div(fi(rw), fi(gl_w))), fl(1.4426950408889634)), F_ONE)
|
||||
}
|
||||
# the sampler for texture tex's parameters, from the texture's own one-entry cache when they have
|
||||
# not changed since it last asked - a draw asks for every texture it binds
|
||||
function gvk_tex_sampler(tex: int, min_f: int, mag_f: int, wrap_s: int, wrap_t: int, compare: int, aniso: int) -> long {
|
||||
let bias = gvk_mip_bias()
|
||||
var sig = min_f * 31 + mag_f
|
||||
sig = sig * 31 + wrap_s
|
||||
sig = sig * 31 + wrap_t
|
||||
sig = sig * 31 + compare
|
||||
sig = (sig * 31 + aniso) | 1
|
||||
sig = sig * 31 + aniso
|
||||
# the bias is part of what the sampler IS, so it has to invalidate this cache too - otherwise
|
||||
# turning DLSS on mid-session keeps every sampler already made at the old bias
|
||||
sig = (sig * 31 + bias) | 1
|
||||
if tex > 0 and tex < len(gvk_tex_smp_sig) and gvk_tex_smp_sig[tex] == sig { return gvk_tex_smp[tex] }
|
||||
let s = gvk_sampler(min_f, mag_f, wrap_s, wrap_t, compare, aniso)
|
||||
if tex > 0 and tex < len(gvk_tex_smp_sig) { gvk_tex_smp_sig[tex] = sig; gvk_tex_smp[tex] = s }
|
||||
return s
|
||||
}
|
||||
function gvk_sampler(min_f: int, mag_f: int, wrap_s: int, wrap_t: int, compare: int, aniso: int) -> long {
|
||||
let key = `{min_f}/{mag_f}/{wrap_s}/{wrap_t}/{compare}/{aniso}`
|
||||
let bias = gvk_mip_bias()
|
||||
let key = `{min_f}/{mag_f}/{wrap_s}/{wrap_t}/{compare}/{aniso}/{bias}`
|
||||
if gvk_smp_keys == null { gvk_smp_keys = new []string; gvk_smp = new []long }
|
||||
for i in 0 .. len(gvk_smp_keys) { if gvk_smp_keys[i] == key { return gvk_smp[i] } }
|
||||
var mn = min_f
|
||||
|
|
@ -504,6 +528,7 @@ function gvk_sampler(min_f: int, mag_f: int, wrap_s: int, wrap_t: int, compare:
|
|||
Vk.put_i32(sci, VkSamplerCreateInfo_addressModeW, gvk_address(wrap_t))
|
||||
# without mipmaps, a max LOD of 0.25 samples level 0 only (the spec's own recipe for GL_LINEAR)
|
||||
if mipmapped { Vk.put_i32(sci, VkSamplerCreateInfo_maxLod, 0x447A0000) } else { Vk.put_i32(sci, VkSamplerCreateInfo_maxLod, 0x3E800000) }
|
||||
if mipmapped { Vk.put_i32(sci, VkSamplerCreateInfo_mipLodBias, bias) }
|
||||
if aniso > 0x3F800000 and mipmapped {
|
||||
var a = aniso
|
||||
if a > gvk_max_aniso { a = gvk_max_aniso }
|
||||
|
|
|
|||
|
|
@ -15,6 +15,14 @@ var grass_prog: int = 0
|
|||
var grass_mesh: Mesh = null
|
||||
var grass_on: bool = true
|
||||
var grass_wind: int = 0
|
||||
# A photographed blade, as an atlas of straightened blades side by side (the game sets
|
||||
# this; the renderer does not name a game asset). 0 = the procedural gradient, which is
|
||||
# what this was for a year: a two-tone ramp with a hard edge, and every blade in the
|
||||
# valley the same blade. A real blade has a midrib, a colour that runs olive to straw,
|
||||
# browning where it has dried and a tip that is its own shape - none of which can be
|
||||
# written down, only photographed.
|
||||
var grass_blade_tex: int = 0
|
||||
var grass_blade_cols: int = 8
|
||||
# Where a body is standing, and how wide it pushes. The grass has never known the player was
|
||||
# in it: you walked through a meadow and every blade ignored you, which is the single most
|
||||
# noticeable thing missing from every step the game asks you to take. The game sets this each
|
||||
|
|
@ -59,10 +67,25 @@ function grass_blade_mesh(rows: int) -> Mesh {
|
|||
let m = gpu_mesh_new()
|
||||
let v = gl_floats(rows * 2 * 5)
|
||||
var k = 0
|
||||
# The blade's PROFILE, and it is the whole difference between grass and a green spike.
|
||||
# It used to be `1 - t^2.5` floored at 0.12 with a bend of 0.28t^2: widest at the very
|
||||
# bottom, narrowing to a needle, and standing almost straight. That is the silhouette of
|
||||
# a pine needle, and eighty of them to the square metre read as a bed of nails.
|
||||
#
|
||||
# A real blade is narrow where it leaves the sheath, WIDEST about a fifth of the way up,
|
||||
# and then tapers the rest of the way to a fine point - and it arches over under its own
|
||||
# weight. Both terms below say that. The tip is floored just off zero rather than at 0.12
|
||||
# so the point is a point and not a cut-off stub, but not so low that the last quad is
|
||||
# degenerate.
|
||||
#
|
||||
# grass.mesh carries A COPY of these two lines for the mesh-shader path; change both or
|
||||
# the Windows blades stop matching the ones everywhere else.
|
||||
for r in 0 .. rows {
|
||||
let t = fr(r, rows - 1)
|
||||
let taper = f_max(f_sub(F_ONE, f_mul(t, f_mul(t, f_sqrt(t)))), fl(0.12))
|
||||
let bend = f_mul(f_mul(t, t), fl(0.28))
|
||||
let grow = f_min(f_div(t, fl(0.22)), F_ONE)
|
||||
let wide = f_mul(f_add(fl(0.50), f_mul(fl(0.33), grow)), f_sub(F_ONE, f_mul(t, f_mul(t, t))))
|
||||
let taper = f_max(wide, fl(0.05))
|
||||
let bend = f_mul(f_mul(t, t), fl(0.52))
|
||||
for sd in 0 .. 2 {
|
||||
var x = f_neg(F_HALF)
|
||||
if sd == 1 { x = F_HALF }
|
||||
|
|
@ -100,9 +123,17 @@ function grass_init() -> void {
|
|||
}
|
||||
grass_prog = r3d_program("grass.vert", "model.frag", defs)
|
||||
if grass_merge and gpu_has_mesh() { grass_mesh_prog = r3d_program("grass.mesh", "model.frag", "#define FOLIAGE\n#define BLADE\n#define MESH\n") }
|
||||
grass_mesh = grass_blade_mesh(4)
|
||||
# five rows, four quads: the arch above needs somewhere to bend, and at four rows a
|
||||
# blade that leans over is three straight segments and shows every join
|
||||
grass_mesh = grass_blade_mesh(5)
|
||||
grass_wind = fl(2.4)
|
||||
grass_s0 = fl(0.11)
|
||||
# Matched to the blade's width: a 1 cm blade at 0.11 m spacing covers a third of what a
|
||||
# 2.8 cm blade did, and the meadow goes bare. The game's graphics settings override this
|
||||
# (gfx_grass_spacing), but only once game_init has run - a plain headless render never
|
||||
# gets there, so the two have to agree or a shot shows something no player will see.
|
||||
# That is exactly how the last change measured as "no effect": the render was identical
|
||||
# because this line, not the settings, was deciding.
|
||||
grass_s0 = fl(0.066)
|
||||
grass_d0 = fi(45)
|
||||
grass_radius = fi(1600)
|
||||
if r3d_env_has("R3D_NOBLADES") { grass_on = false }
|
||||
|
|
@ -181,6 +212,14 @@ function grass_draw() -> void {
|
|||
u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
|
||||
u_mat4(gpu_uniform(p, "u_vp"), cam_vp_clean)
|
||||
u_f(gpu_uniform(p, "u_wind"), grass_wind)
|
||||
# copied into a local first: a global reaching a uniform call is the codegen fault
|
||||
# CLAUDE.md records against u_wade and u_flutter, and it costs a day every time
|
||||
let btex = grass_blade_tex
|
||||
let bcols = grass_blade_cols
|
||||
u_f(gpu_uniform(p, "u_blade_cols"), fi(bcols))
|
||||
var bon = F_ZERO
|
||||
if btex != 0 { bon = F_ONE; r3d_bind_2d(p, "u_blade_tex", 12, btex) }
|
||||
u_f(gpu_uniform(p, "u_blade_tex_on"), bon)
|
||||
u_f3(gpu_uniform(p, "u_push"), grass_push_x, grass_push_z, grass_push_r)
|
||||
u_f(gpu_uniform(p, "u_rough_scale"), F_ONE)
|
||||
u_v3(gpu_uniform(p, "u_tint"), sc_blade_tint)
|
||||
|
|
@ -208,7 +247,12 @@ function grass_draw() -> void {
|
|||
sky_bind_lighting(p)
|
||||
shadow_bind(p)
|
||||
fog_bind(p)
|
||||
u_f(gpu_uniform(p, "u_spec_scale"), fl(0.15))
|
||||
# 0.15 was enough to put a hard white highlight down the length of a blade whenever it
|
||||
# caught the sun, and a white blade of grass is the one thing grass is never. Measured:
|
||||
# at 0.15, 0.70% of a near-ground frame was over 210 of 255; at 0.0 it is 0.05%. A blade
|
||||
# does have a faint sheen, so this is small rather than nothing - the foliage layers have
|
||||
# used 0.05 all along and never showed the fault.
|
||||
u_f(gpu_uniform(p, "u_spec_scale"), fl(0.008))
|
||||
gpu_cull(false)
|
||||
grass_draws = 0
|
||||
grass_n = 0
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
// count is skipped.
|
||||
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
const int BLADES = 16; // blades an invocation may emit
|
||||
const int ROWS = 4; // grass_blade_mesh(4): two vertices a row, three quads
|
||||
const int ROWS = 5; // grass_blade_mesh(5): two vertices a row, four quads
|
||||
layout(triangles, max_vertices = 128, max_primitives = 96) out;
|
||||
|
||||
uniform mat4 u_view;
|
||||
|
|
@ -129,11 +129,13 @@ void main() {
|
|||
if (tilt) n = normalize(n * ck + cross(k, n) * sk + k * dot(k, n) * (1.0 - ck));
|
||||
n = normalize(mix(n, gn, smoothstep(2.0, 12.0, dist)));
|
||||
float hull = (dist > 2.0 || far || (u_dbg & 2) != 0) ? -1.0 : 1.0;
|
||||
// the blade: grass_blade_mesh(4)'s vertices, placed as grass.vert places them
|
||||
// the blade: grass_blade_mesh(5)'s vertices, placed as grass.vert places them
|
||||
for (int r = 0; r < ROWS; r++) {
|
||||
float t = float(r) / float(ROWS - 1);
|
||||
float taper = max(1.0 - t * t * sqrt(t), 0.12);
|
||||
float bend = t * t * 0.28;
|
||||
// A COPY of grass_blade_mesh's profile in grass.ludic - narrow at the sheath,
|
||||
// widest a fifth of the way up, a fine point, and arched. Change both together.
|
||||
float taper = max((0.50 + 0.33 * min(t / 0.22, 1.0)) * (1.0 - t * t * t), 0.05);
|
||||
float bend = t * t * 0.52;
|
||||
for (int sd = 0; sd < 2; sd++) {
|
||||
vec3 a_pos = vec3((float(sd) - 0.5) * taper, t, bend);
|
||||
vec2 a_uv = vec2(float(sd), t);
|
||||
|
|
|
|||
|
|
@ -100,8 +100,16 @@ void main() {
|
|||
// of the count shrinks to nothing so a blade never pops.
|
||||
float spacing = u_s0 * (1.0 + dist / u_d0);
|
||||
float count = CELL * CELL / (spacing * spacing) * (1.0 - smoothstep(u_radius * 0.7, u_radius, dist));
|
||||
if (fj >= count) { cull(); return; }
|
||||
float life = 1.0 - smoothstep(0.8, 1.0, fj / max(count, 1.0));
|
||||
// WHICH blades thin out has to be random, not the last indices. `fj >= count` keeps
|
||||
// blades 0..N-1, and as N falls by one with distance, the SAME index dies in every
|
||||
// cell at the same radius - one blade in a fixed place per cell, over a whole ring.
|
||||
// Seen from above that is a set of arcs centred on the camera, and at eye level it is
|
||||
// the banding that reads as a ploughed field. Giving each blade its own fixed number
|
||||
// and comparing that against the density makes the thinning scatter instead.
|
||||
float keep = count / float(max(per_cell, 1));
|
||||
float r = bladeHash(ci, j, 5);
|
||||
if (r > keep) { cull(); return; }
|
||||
float life = 1.0 - smoothstep(keep * 0.75, keep, r);
|
||||
// the ground under it
|
||||
vec2 huv = (xz - u_ts_origin) / (2.0 * u_ts_half) + 0.5;
|
||||
if (huv.x < 0.0 || huv.x > 1.0 || huv.y < 0.0 || huv.y > 1.0) { cull(); return; }
|
||||
|
|
@ -136,7 +144,11 @@ void main() {
|
|||
if ((u_dbg & 1) != 0) h += 0.3;
|
||||
// the blade: sized so that coverage stays level as the spacing grows
|
||||
float seed = hv.x * 0.7 + hv.y * 0.3;
|
||||
float ang = hv.y * 6.2831853;
|
||||
// A BLADE'S YAW MUST NOT BE ITS POSITION. This was `hv.y * 2pi`, and hv.y is the same
|
||||
// number that places the blade along the cell's z - so every blade at the same depth
|
||||
// in a cell faced the same way, in rows, sixteen metres wide. That is the single thing
|
||||
// that made the meadow look ploughed. Its own hash costs nothing.
|
||||
float ang = bladeHash(ci, j, 6) * 6.2831853;
|
||||
float s = sin(ang), c_ = cos(ang);
|
||||
float grow = spacing / u_s0; // 1 at the camera, growing with distance
|
||||
// Height is biased SHORT rather than spread evenly. A meadow is not one length of grass: it
|
||||
|
|
@ -146,7 +158,12 @@ void main() {
|
|||
// that had been cut, which is the one thing an alpine meadow is not.
|
||||
float hh = h3 * h3;
|
||||
float tall = mix(0.09, 0.60, hh) * mix(0.8, 1.2, hash1(cid * 0.1)) * (1.0 + 0.35 * smoothstep(1.0, 12.0, grow)) * life;
|
||||
float bw = 0.028 * mix(1.0, 0.45 * grow, smoothstep(1.0, 4.0, grow));
|
||||
// 2.8 cm was FIVE TIMES a blade of meadow grass, which is 3-6 mm. At 2 m from the
|
||||
// camera that is a broad dark scimitar lying along the ground rather than a blade
|
||||
// standing in a sward, and no amount of profile or colour work fixes a blade that is
|
||||
// the wrong size. Measured, not guessed: it is the one number behind every "too wide
|
||||
// and too flat" note in this stage.
|
||||
float bw = 0.010 * mix(1.0, 0.45 * grow, smoothstep(1.0, 4.0, grow));
|
||||
if (far) { bw = max(bw, spacing * 0.35); tall = min(tall, spacing * 0.3); }
|
||||
vec3 p = vec3(a_pos.x * bw, a_pos.y * tall, a_pos.z * tall * (0.6 + 0.8 * h4));
|
||||
vec3 n = vec3(0.0, 0.3, 1.0);
|
||||
|
|
|
|||
|
|
@ -45,16 +45,27 @@ void main() {
|
|||
vec3 toCam = normalize(u_cam_pos - v_wpos); toCam.y = 0.0; toCam = normalize(toCam);
|
||||
vec3 right = vec3(-toCam.z, 0.0, toCam.x);
|
||||
vec3 hull = normalize(right * q.x * 0.8 + vec3(0.0, 1.0, 0.0) * (q.y * 0.6 + 0.35) + toCam * 0.7);
|
||||
n = normalize(mix(hull, n, mix(0.65, 0.35, far)));
|
||||
// Keep the BAKED normal. This was mix(0.65, 0.35, far), so past a few hundred metres
|
||||
// only a third of the foliage's own normal survived and two thirds was a smooth
|
||||
// rounded shell - which shades evenly and turns a canopy into a flat coloured blob.
|
||||
// Impostors draw nearly all the forest a player ever sees (turn them off and the
|
||||
// valley is bare), so that shell was most of the tree line.
|
||||
n = normalize(mix(hull, n, mix(0.88, 0.70, far)));
|
||||
float dist = length(v_wpos - u_cam_pos);
|
||||
float viewDepth = -(u_view * vec4(v_wpos, 1.0)).z;
|
||||
vec3 alb = a.rgb * u_tint * (0.85 + 0.3 * fract(v_seed * 7.13)) * regionTint(v_wpos, 0.4);
|
||||
// a distant stand reads as a dark mass, not as bright separate sprites
|
||||
alb = mix(alb, alb * vec3(0.72, 0.78, 0.72), far);
|
||||
// A distant stand reads as a dark mass, not as bright separate sprites - but this was
|
||||
// also pulling the colour out of it. Darken without desaturating: the same factor on
|
||||
// all three channels, and a touch of saturation put back, because the thing that makes
|
||||
// a far tree line read as forest rather than as a wash is that it is still GREEN.
|
||||
alb = mix(alb, alb * 0.72, far);
|
||||
float lum = dot(alb, vec3(0.2126, 0.7152, 0.0722));
|
||||
alb = mix(vec3(lum), alb, 1.45); // conifer foliage is SATURATED, not grey-green
|
||||
alb *= 0.78; // and it is dark: a spruce canopy is not a lawn
|
||||
// the card itself is the caster: look up the shadow a little toward the sun so it does not self-shadow
|
||||
float shadow = sunShadow(v_wpos + u_sun_dir * u_radius * 0.7, vec3(0, 1, 0), viewDepth);
|
||||
// crowns are dense: darken toward the centre of the card as a cheap interior occlusion
|
||||
float interior = 1.0 - 0.45 * smoothstep(0.9, 0.3, abs(q.x)) * smoothstep(1.0, 0.2, v_uv.y);
|
||||
float interior = 1.0 - 0.22 * smoothstep(0.9, 0.3, abs(q.x)) * smoothstep(1.0, 0.2, v_uv.y);
|
||||
// ground contact: the lowest part of anything sitting on the ground is occluded by it
|
||||
// (a boulder's underside, a trunk's base); without it a far boulder is a sticker on the grass
|
||||
interior *= mix(0.55, 1.0, smoothstep(0.0, 0.3, v_uv.y));
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ uniform float u_emissive; // self-lit (a flame): albedo added back after sha
|
|||
#ifdef BLADE
|
||||
uniform vec3 u_blade_base;
|
||||
uniform vec3 u_blade_tip;
|
||||
uniform sampler2D u_blade_tex; // straightened photographic blades, side by side
|
||||
uniform float u_blade_cols; // how many are in it
|
||||
uniform float u_blade_tex_on;
|
||||
#endif
|
||||
#ifdef CARD
|
||||
uniform float u_cull; // the layer's cull distance (m); 0 = none
|
||||
|
|
@ -80,6 +83,33 @@ void main() {
|
|||
alb = mix(u_blade_base, u_blade_tip, t * t) * regionTint(v_wpos, 0.35);
|
||||
float dry = smoothstep(0.7, 0.8, fract(v_seed * 3.17));
|
||||
alb = mix(alb, vec3(0.40, 0.36, 0.13) * (0.45 + 0.55 * t), dry * 0.75);
|
||||
// A REAL BLADE, if the game gave us one. Each blade picks a column of the atlas from
|
||||
// its own seed and runs v from sheath to tip, so a meadow is eight different plants
|
||||
// rather than one plant ten thousand times. The photograph carries the midrib, the
|
||||
// olive-to-straw run and the dry browning; the gradient above stays as the tint that
|
||||
// the season and the lushness drive, so nothing that used to control the colour stops
|
||||
// working - the picture multiplies it rather than replacing it.
|
||||
if (u_blade_tex_on > 0.5) {
|
||||
float col = floor(fract(v_seed * 7.31) * u_blade_cols);
|
||||
vec2 buv = vec2((col + clamp(v_uv.x, 0.0, 1.0)) / u_blade_cols, 1.0 - t);
|
||||
vec3 photo = texture(u_blade_tex, buv).rgb;
|
||||
// Normalised by the atlas's OWN MEAN LUMINANCE - a measured constant, 0.3736 over the
|
||||
// opaque pixels - and not by each pixel's mean. Dividing by the per-pixel mean was the
|
||||
// bug: it cancels exactly the thing the photograph was fetched for. What survives is
|
||||
// the hue ratio, so every blade comes back out at the same brightness and the midrib,
|
||||
// the dry browning and the sheath-to-tip run all vanish. It looked like a faint tint
|
||||
// over the old procedural blade, which is precisely what it was.
|
||||
// Blended, not applied whole. At full strength a photo pixel brighter than the
|
||||
// atlas's mean is multiplied by up to 1.9, and the bright blades came out white -
|
||||
// straws in a green sward. Three quarters of the photograph keeps the midrib, the
|
||||
// browning and the sheath-to-tip run and leaves the extremes alone.
|
||||
// CLAMPED. The atlas's mean luminance is 0.3736, so a blade pixel brighter than the
|
||||
// mean is multiplied by up to 2.7 and comes out white - and a white blade is the one
|
||||
// thing grass never is. Holding the factor to 1.15 keeps the midrib and the browning,
|
||||
// which are the parts of the photograph worth having, and refuses the bleach.
|
||||
vec3 g = photo * (1.0 / 0.3736);
|
||||
alb *= mix(vec3(1.0), min(g, vec3(1.15)), 0.85);
|
||||
}
|
||||
float patchy = fbm(v_wpos.xz * 0.045, 3) * 0.5 + 0.5;
|
||||
alb *= mix(vec3(0.7, 0.8, 0.55), vec3(1.1, 1.05, 0.85), patchy);
|
||||
// a far tuft is a patch of the meadow, darker than a lit blade tip and never straw
|
||||
|
|
@ -87,7 +117,15 @@ void main() {
|
|||
// a rounded cross-section reads softer than a flat card
|
||||
vec3 side = normalize(cross(N, vec3(0.0, 1.0, 0.0)) + vec3(1e-4));
|
||||
n = (v_hull < 0.0) ? N : normalize(N + side * (v_uv.x * 2.0 - 1.0) * 0.6);
|
||||
arm = vec3(mix(0.2, 1.0, t * t), 0.85, 0.0);
|
||||
// A blade's own ambient occlusion. This was mix(0.2, 1.0, t*t): 80% occluded at the
|
||||
// sheath and still 60% at half height, because t*t holds the curve down. Measured in a
|
||||
// walking-distance shot the blades came out at 24-31 of 255 against a ground of 143 -
|
||||
// near-black on light earth - and at that contrast the eye reads every blade as a hard
|
||||
// EDGE rather than as a mass of vegetation, whatever shape it is. No albedo can answer
|
||||
// an occlusion term; grass albedo is capped near 0.5 and this was dividing it by five.
|
||||
// A blade is a thin thing standing in open air: shaded at the root by its neighbours,
|
||||
// not buried.
|
||||
arm = vec3(mix(0.55, 1.0, t), 0.85, 0.0);
|
||||
#elif defined(CARD)
|
||||
// thin grass is lit from either side: face the card toward the sun before shading
|
||||
if (dot(N, u_sun_dir) < 0.0) N = -N;
|
||||
|
|
@ -156,8 +194,20 @@ void main() {
|
|||
// The quake made visible. A turning aspen leaf shows its pale, almost white underside, so
|
||||
// the crown does not merely move - it GLITTERS, leaf by leaf, and that is what reads at a
|
||||
// distance and in a still frame. Toward the viewer is the pale side; away is the face.
|
||||
if (v_quake > 0.0) alb = mix(alb, alb * 0.55 + vec3(0.42, 0.45, 0.33), min(v_quake * 2.6, 0.75));
|
||||
else if (v_quake < 0.0) alb *= 1.0 + v_quake * 0.28; // the dark half stays gentle: a crown should shimmer, not flicker
|
||||
// The quake, as a LIGHTENING rather than a repaint. This used to mix up to 75% toward
|
||||
// a fixed near-white (0.42,0.45,0.33), which was written for a hand-painted leaf atlas
|
||||
// that had no pale underside of its own. On a photographed leaf it does not read as a
|
||||
// turning leaf at all - it reads as parts of the tree going WHITE, because that is
|
||||
// exactly what it does: it replaces three quarters of the leaf's colour with a constant.
|
||||
// Nothing in a wood turns white in the sun. A real turning leaf shows a paler, greyer
|
||||
// version of ITSELF, so lift and desaturate the leaf's own colour instead.
|
||||
if (v_quake > 0.0) {
|
||||
float q = min(v_quake * 2.6, 0.75);
|
||||
float l = dot(alb, vec3(0.2126, 0.7152, 0.0722));
|
||||
alb = mix(alb, mix(alb, vec3(l), 0.45) * 1.30, q);
|
||||
} else if (v_quake < 0.0) {
|
||||
alb *= 1.0 + v_quake * 0.28; // the dark half stays gentle: a crown should shimmer, not flicker
|
||||
}
|
||||
#if defined(FOLIAGE) && !defined(BLADE)
|
||||
// Per-plant HUE, not only per-plant brightness. The line above varies value by +-15% and
|
||||
// nothing else, so a stand of one species was one colour at fifteen different exposures -
|
||||
|
|
|
|||
|
|
@ -502,6 +502,17 @@ function terrain_bind_prog(p: int) -> void {
|
|||
sky_bind_lighting(p)
|
||||
shadow_bind(p)
|
||||
fog_bind(p)
|
||||
# GROUND IS NOT A MIRROR, and this has to come AFTER fog_bind, which hands every
|
||||
# program u_spec_scale = 1. That is right for water and for a varnished prop and wrong
|
||||
# for a hillside: the image-based specular lays a broad reflection of a bright sky over
|
||||
# every square metre of rock and meadow and washes them toward the sky's own colour.
|
||||
# It is why the range named for the colour of its rock rendered pale lilac rather than
|
||||
# maroon - the maroon was under a sheet of reflected sky. Foliage already gets 0.05.
|
||||
#
|
||||
# Set before fog_bind it measured as EXACTLY zero pixels changed, which is the same
|
||||
# shape of mistake as setting r3d_fog_scale before gfx_apply: the value was right and
|
||||
# something downstream put it back.
|
||||
u_f(gpu_uniform(p, "u_spec_scale"), fl(0.22))
|
||||
u_v3(gpu_uniform(p, "u_cam_pos"), cam_pos)
|
||||
u_f(gpu_uniform(p, "u_grid"), fi(CD_G))
|
||||
# The ground reads its sun visibility out of the buffer tersun.frag filled, and has no
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue