@weft-js/plugin-surface-keel 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,439 @@
1
+ import './core-primitives.mjs';
2
+ import {
3
+ FX_ORDER,
4
+ GRADE_PRESETS,
5
+ SCREENS,
6
+ scalePx,
7
+ screenForTarget,
8
+ } from './core-primitives.mjs';
9
+
10
+ var FX = {
11
+ // A tube's bend: whole pixels moved outward from the middle, the corners gone to a border entry. (From 64 px.)
12
+ crt: { defaults: { curve: 0.08, border: { ramp: 0, index: 0 }, minSize: 64 }, resolve: (p, side) => side < p.minSize ? { off: `below ${p.minSize} px` } : { curve: p.curve, border: p.border } },
13
+ // Colour grading by ramps: each ramp swapped for its graded twin (map: { stone: "stoneNight" }), then shifted along it.
14
+ grade: {
15
+ defaults: { preset: "day", shift: null, map: {} },
16
+ resolve: (p) => ({ shift: p.shift ?? (Object.hasOwn(GRADE_PRESETS, p.preset) ? GRADE_PRESETS[p.preset] : GRADE_PRESETS.day).shift, map: { ...p.map } })
17
+ },
18
+ // Fog by distance: past `near`, pixels go over to the fog ramp -- dithered, by the screen -- all of them by `far`.
19
+ fog: { defaults: { ramp: "sky", near: 25, far: 90, amount: 1, light: 0.3 }, resolve: (p) => ({ ...p }) },
20
+ // Glow: emissive materials (glow >= threshold) climb `self` entries; round them a halo `radius` px (at 128) climbs `halo`.
21
+ glow: {
22
+ defaults: { radius: 3, halo: 2.5, self: 1, threshold: 0.2, tint: false },
23
+ resolve: (p, side) => ({ ...p, radius: scalePx(p.radius, side, 1, 16) })
24
+ },
25
+ // Rim light: an edge on the light's side (`dir`: "sun" or a screen direction [x, y]) climbs `steps`; `width` px at 128.
26
+ rim: { defaults: { width: 1, steps: 1.5, dir: "sun" }, resolve: (p, side) => ({ ...p, width: scalePx(p.width, side, 1, 4) }) },
27
+ // Hit flash: the struck thing -- by material (`mats`) or id range (`ids: [from, to]`) -- goes up by `amount` (0..1), or onto `ramp`.
28
+ flash: { defaults: { amount: 0, mats: [], ids: null, ramp: null }, resolve: (p) => ({ ...p, amount: Math.max(0, Math.min(1, p.amount)), mats: p.mats.slice(0, 8) }) },
29
+ // Vignette: from `inner` to `outer` (fractions of the half-diagonal), `steps` down the ramps, dithered.
30
+ vignette: { defaults: { inner: 0.55, outer: 1.1, steps: 2 }, resolve: (p) => ({ ...p }) },
31
+ // Scanlines: `dark` rows of every `period` (px at 128) a step down. Only from `minSize` px: below it they are the picture.
32
+ scanlines: {
33
+ defaults: { period: 2, steps: 1, minSize: 96 },
34
+ resolve: (p, side) => {
35
+ if (side < p.minSize) return { off: `below ${p.minSize} px` };
36
+ const period = Math.max(2, scalePx(p.period, side, 2, 16));
37
+ return { period, dark: Math.max(1, Math.floor(period / 2)), steps: p.steps };
38
+ }
39
+ },
40
+ // The dither screen: a core screen id or family ("ordered", "dot", "line", "noise", "pattern"), or "auto"; `amount` 0..1.
41
+ dither: {
42
+ defaults: { screen: "auto", amount: 0.9 },
43
+ resolve: (p, _side, W2, H) => {
44
+ if (p.screen === "none" || p.amount <= 0) return { screen: "none", amount: 0 };
45
+ const pref = p.screen === "auto" ? "ordered" : p.screen;
46
+ return { screen: screenForTarget(W2, H, pref).id, amount: p.amount };
47
+ }
48
+ },
49
+ // The outline: "all" (a thing against anything behind it -- the classic), "outer" (only across `gap` metres:
50
+ // silhouettes, not the parts inside them), "none"; `steps` darker, or in one `color` { ramp, index }.
51
+ outline: {
52
+ defaults: { mode: "all", steps: 3, gap: 1.5, color: null },
53
+ resolve: (p) => p.mode === "none" ? { off: "mode none" } : { mode: p.mode, steps: p.steps, gap: p.mode === "outer" ? p.gap : 0.56, color: p.color }
54
+ },
55
+ // Palette cycling: named ramps' upper entries (from `from`, a fraction of the ramp) turn over at `speed` entries a second.
56
+ cycle: {
57
+ defaults: { ramps: {}, speed: 3, from: 0.5 },
58
+ resolve: (p) => {
59
+ const list = isList(p.ramps) ? Object.fromEntries(p.ramps.map((r) => [r, {}])) : p.ramps;
60
+ return { ramps: Object.fromEntries(Object.entries(list).map(([r, o]) => [r, { speed: o.speed ?? p.speed, from: o.from ?? p.from }])) };
61
+ }
62
+ }
63
+ };
64
+ var isList = (v) => Array.isArray(v);
65
+ var FX_NAMES = Object.keys(FX);
66
+ var known = (name) => typeof name === "string" && Object.hasOwn(FX, name);
67
+ var dims = (target) => {
68
+ if (typeof target === "number") return [target, target];
69
+ if ("width" in target) return [target.width, target.height];
70
+ return [target[0], target[1]];
71
+ };
72
+ function resolveFx(list = [], target = 128) {
73
+ const [W2, H] = dims(target);
74
+ const side = Math.min(W2, H);
75
+ const byName = /* @__PURE__ */ new Map();
76
+ for (const e of list) {
77
+ if (!e || !known(e.name)) throw new RangeError(`Unknown fx pass: ${String(e?.name)}`);
78
+ byName.set(e.name, { ...byName.get(e.name) ?? {}, ...e });
79
+ }
80
+ const out = [];
81
+ for (const name of FX_ORDER) {
82
+ const entry = byName.get(name);
83
+ if (!entry) continue;
84
+ const { name: _, on = true, ...given } = entry;
85
+ out.push(resolveOne(name, Boolean(on), given, side, W2, H));
86
+ }
87
+ return out;
88
+ }
89
+ function resolveOne(name, on, given, side, W2, H) {
90
+ const def = FX[name];
91
+ const params = { ...def.defaults, ...given };
92
+ if (!on) return { name, on: false, params, note: "turned off" };
93
+ const r = def.resolve(params, side, W2, H);
94
+ if ("off" in r && r.off) return { name, on: false, params, note: r.off };
95
+ return { name, on: true, params: r };
96
+ }
97
+ function toggleFxList(list, name, on) {
98
+ if (!known(name)) throw new RangeError(`Unknown fx pass: ${String(name)}`);
99
+ const has = list.some((e) => e.name === name);
100
+ return has ? list.map((e) => e.name === name ? { ...e, on } : e) : [...list, { name, on }];
101
+ }
102
+ var ALL_FX = () => [
103
+ { name: "grade", preset: "dusk" },
104
+ { name: "fog" },
105
+ { name: "glow" },
106
+ { name: "rim" },
107
+ { name: "vignette" },
108
+ { name: "scanlines" },
109
+ { name: "dither", screen: "auto" },
110
+ { name: "outline", mode: "all" },
111
+ { name: "cycle", ramps: ["water"] }
112
+ ];
113
+ function colourEntry(c, ramp2, look) {
114
+ if (c == null) return -1;
115
+ if (typeof c === "number") return c;
116
+ const r = ramp2(c.ramp);
117
+ if (r < 0) return -1;
118
+ const [base, len2] = look.rampOf(r);
119
+ return base + Math.max(0, Math.min(len2 - 1, c.index < 0 ? len2 + c.index : c.index));
120
+ }
121
+
122
+ function fxUniforms(resolved, look) {
123
+ const on = {};
124
+ for (const p of resolved) if (p.on) on[p.name] = p.params;
125
+ const ramp2 = (r) => typeof r === "number" ? r : look.ramp(r);
126
+ const entry = (c) => colourEntry(c, ramp2, look);
127
+ const style = look.style ?? { screen: 4, dither: 0.9, outline: 1 };
128
+ let screen = style.screen;
129
+ let dither = style.dither;
130
+ if (on.dither) {
131
+ screen = on.dither.screen;
132
+ dither = on.dither.amount;
133
+ } else if (resolved.some((p) => p.name === "dither")) {
134
+ screen = 0;
135
+ dither = 0;
136
+ }
137
+ const bayer = { bayer2: 2, bayer4: 4, bayer8: 8, none: 0 };
138
+ let uScreen;
139
+ let tile = null;
140
+ if (typeof screen === "string") {
141
+ const b = Object.hasOwn(bayer, screen) ? bayer[screen] : void 0;
142
+ if (b !== void 0) uScreen = b;
143
+ else if (Object.hasOwn(SCREENS, screen)) {
144
+ uScreen = -1;
145
+ tile = screen;
146
+ } else throw new RangeError(`Unknown screen: ${screen}`);
147
+ } else uScreen = screen | 0;
148
+ const ol = resolved.find((p) => p.name === "outline");
149
+ let uOutline;
150
+ let uOutlineInk;
151
+ if (ol) {
152
+ uOutline = ol.on ? [1, ol.params.steps, ol.params.gap / look.far] : [0, 3, 4e-3];
153
+ uOutlineInk = ol.on ? entry(ol.params.color) : -1;
154
+ } else {
155
+ uOutline = [style.outline ? 1 : 0, 3, 4e-3];
156
+ uOutlineInk = -1;
157
+ }
158
+ const { crt, fog, glow, rim, flash: fl, vignette: vig, scanlines: sc, cycle: cy } = on;
159
+ const mats = fl ? [...fl.mats, -1, -1, -1, -1, -1, -1, -1, -1].slice(0, 8) : Array(8).fill(-1);
160
+ const cycle = [];
161
+ if (cy) {
162
+ for (const [name, o] of Object.entries(cy.ramps)) {
163
+ const r = ramp2(name);
164
+ if (r < 0) continue;
165
+ const len2 = look.rampOf(r)[1];
166
+ cycle.push([r, Math.min(len2 - 1, Math.max(0, Math.round(o.from * len2))), o.speed]);
167
+ }
168
+ }
169
+ const grade = [];
170
+ if (on.grade) for (const [from, to] of Object.entries(on.grade.map)) {
171
+ const a = ramp2(from);
172
+ const b = ramp2(to);
173
+ if (a >= 0 && b >= 0) grade.push([a, b]);
174
+ }
175
+ const u = {
176
+ uScreen,
177
+ uDither: dither,
178
+ uOutline,
179
+ uOutlineInk,
180
+ uCrt: crt ? [1, crt.curve, Math.max(0, entry(crt.border)), 0] : [0, 0, 0, 0],
181
+ uGrade: on.grade ? [1, on.grade.shift] : [0, 0],
182
+ uFog: fog ? [ramp2(fog.ramp) >= 0 ? 1 : 0, fog.near, fog.far, fog.amount] : [0, 0, 1, 0],
183
+ uFogLook: fog ? [Math.max(0, ramp2(fog.ramp)), fog.light] : [0, 0],
184
+ uGlow: glow ? [1, glow.radius, glow.halo, glow.self] : [0, 1, 0, 0],
185
+ uGlowK: glow ? [glow.threshold, glow.tint ? 1 : 0] : [1, 0],
186
+ uRim: rim ? [1, rim.width, rim.steps, 0] : [0, 1, 0, 0],
187
+ uRimDir: rim && isPair(rim.dir) ? rim.dir : null,
188
+ // (null: the renderer aims it at the sun)
189
+ uFlash: fl ? [fl.amount, fl.ramp == null ? -1 : ramp2(fl.ramp), fl.ids ? fl.ids[0] : 1, fl.ids ? fl.ids[1] : 0] : [0, -1, 1, 0],
190
+ uFlashMats: mats,
191
+ uVig: vig ? [1, vig.inner, vig.outer, vig.steps] : [0, 0, 1, 0],
192
+ uScan: sc ? [1, sc.period, sc.dark, sc.steps] : [0, 2, 0, 0],
193
+ uCycle: cycle.length ? 1 : 0
194
+ };
195
+ return { u, ramps: { cycle, grade }, screen: tile };
196
+ }
197
+ var isPair = (v) => Array.isArray(v);
198
+ function screenTile(id, tile = 192) {
199
+ const at = SCREENS[id].at;
200
+ const out = new Uint8Array(tile * tile);
201
+ for (let y = 0; y < tile; y += 1) for (let x = 0; x < tile; x += 1) out[y * tile + x] = Math.max(0, Math.min(255, Math.round(at(x, y) * 255)));
202
+ return out;
203
+ }
204
+
205
+ // packages/render/src/shaders.ts
206
+ var MAX_BOXES = 256;
207
+ var MAX_WEDGES = 128;
208
+ var MAX_CAPS = 256;
209
+ var MAX_RAMPS = 256;
210
+ var MAX_MATERIALS = 255;
211
+ var PALETTE_WIDTH = 1024;
212
+ var MAX_COLOURS = PALETTE_WIDTH * 64;
213
+ var SCREEN_TILE = 192;
214
+ var FAR = 140;
215
+ var WATER_MAT = 4;
216
+ var SKY_MAT = 5;
217
+ var PARTICLE_MAT = 255;
218
+ var FULLSCREEN_VS = `#version 300 es
219
+ in vec2 aPos;
220
+ void main() { gl_Position = vec4(aPos, 0.0, 1.0); }`;
221
+ var WORLD_FS = `#version 300 es
222
+ precision highp float;
223
+ precision highp sampler2D;
224
+ #define MAXB ${MAX_BOXES}
225
+ #define MAXW ${MAX_WEDGES}
226
+ #define MAXC ${MAX_CAPS}
227
+ #define FAR ${FAR.toFixed(1)}
228
+ uniform vec2 uRes;
229
+ uniform vec3 uEye, uFwd, uRight, uUp;
230
+ uniform float uTan, uTime;
231
+ uniform int uBoxes, uWedges, uCaps;
232
+ // The solids, in uniform blocks (fast to read in the march's inner loop; each block well under the 16 KB baseline).
233
+ layout(std140) uniform Boxes { vec4 uBox[2 * MAXB]; }; // [centre, mat][half, yaw]
234
+ layout(std140) uniform Wedges { vec4 uWedge[3 * MAXW]; }; // [centre, mat][half, yaw][lo, 0, 0, 0]
235
+ layout(std140) uniform Capsules { vec4 uCap[2 * MAXC]; }; // [a, r][b, mat]
236
+ uniform sampler2D uMats; // row 0 per material: ramp, lightness scale, pattern (0 none, 1 checker), emissive
237
+ uniform vec3 uSun;
238
+ uniform float uWaterY, uFogNear, uFogFar;
239
+ layout(location = 0) out vec4 outData; // lightness, ramp / 255, material / 255, id / 255
240
+ layout(location = 1) out vec4 outData2; // glow, facing, 0, 0
241
+
242
+ float sdBox(vec3 p, vec3 b) { vec3 q = abs(p) - b; return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0); }
243
+ float sdCap(vec3 p, vec3 a, vec3 b, float r) { vec3 pa = p - a, ba = b - a; float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-6), 0.0, 1.0); return length(pa - ba * h) - r; }
244
+ // A wedge's cross-section in (z, y): the foot at +z (lo x its height), rising to full height at -z.
245
+ // Exact for a convex polygon: outside, the nearest edge; inside, the nearest edge's plane.
246
+ // (physics/character.js wedgeDistance is the same solid.)
247
+ float sdSection(vec2 p, vec2 h, float lo) {
248
+ vec2 v[4] = vec2[4](vec2(-h.x, -h.y), vec2(h.x, -h.y), vec2(h.x, -h.y + 2.0 * h.y * lo), vec2(-h.x, h.y));
249
+ float out2 = 1e18;
250
+ float far = -1e9;
251
+ bool inside = true;
252
+ for (int i = 0; i < 4; i++) {
253
+ vec2 a = v[i];
254
+ vec2 e = v[(i + 1) & 3] - a;
255
+ float L2 = dot(e, e);
256
+ if (L2 < 1e-12) continue;
257
+ vec2 w = p - a;
258
+ vec2 q = w - e * clamp(dot(w, e) / L2, 0.0, 1.0);
259
+ out2 = min(out2, dot(q, q));
260
+ float s = (w.x * e.y - w.y * e.x) * inversesqrt(L2);
261
+ if (s > 0.0) inside = false;
262
+ far = max(far, s);
263
+ }
264
+ return inside ? far : sqrt(out2);
265
+ }
266
+ float sdWedge(vec3 q, vec3 h, float lo) {
267
+ float a = abs(q.x) - h.x;
268
+ float b = sdSection(q.zy, h.zy, lo);
269
+ return length(max(vec2(a, b), 0.0)) + min(max(a, b), 0.0);
270
+ }
271
+
272
+ // The world: nearest distance and which thing (material in .y, index in .z).
273
+ vec3 map(vec3 p) {
274
+ vec3 best = vec3(1e9, -1.0, -1.0);
275
+ for (int i = 0; i < MAXB; i++) {
276
+ if (i >= uBoxes) break;
277
+ vec4 A = uBox[2 * i]; vec4 B = uBox[2 * i + 1];
278
+ vec3 q = p - A.xyz;
279
+ float c = cos(B.w), s = sin(B.w);
280
+ q.xz = mat2(c, s, -s, c) * q.xz; // (world -> the box's frame, core/frame.js: x' = c x - s z, z' = s x + c z)
281
+ float d = sdBox(q, B.xyz);
282
+ if (d < best.x) best = vec3(d, A.w, float(i));
283
+ }
284
+ for (int i = 0; i < MAXW; i++) {
285
+ if (i >= uWedges) break;
286
+ vec4 A = uWedge[3 * i]; vec4 B = uWedge[3 * i + 1];
287
+ float lo = uWedge[3 * i + 2].x;
288
+ vec3 q = p - A.xyz;
289
+ float c = cos(B.w), s = sin(B.w);
290
+ q.xz = mat2(c, s, -s, c) * q.xz; // (the same turn as a box)
291
+ float d = sdWedge(q, B.xyz, lo);
292
+ // (A tapered wedge: its right and left edges run from its span at the back, -z, in to the foot's full width.)
293
+ vec2 span = uWedge[3 * i + 2].zw * B.x;
294
+ float mr = (B.x - span.y) / max(0.0001, 2.0 * B.z), ml = (B.x + span.x) / max(0.0001, 2.0 * B.z);
295
+ d = max(d, (q.x - mr * q.z - (B.x + span.y) * 0.5) / sqrt(1.0 + mr * mr));
296
+ d = max(d, (-q.x - ml * q.z - (B.x - span.x) * 0.5) / sqrt(1.0 + ml * ml));
297
+ float skin = uWedge[3 * i + 2].y;
298
+ if (skin > 0.0) {
299
+ float slope = -B.y * (1.0 - lo) / max(0.0001, B.z);
300
+ float top = B.y + slope * (q.z + B.z);
301
+ d = max(d, (top - skin - q.y) / sqrt(1.0 + slope * slope));
302
+ }
303
+ if (d < best.x) best = vec3(d, A.w, float(200 + i));
304
+ }
305
+ for (int i = 0; i < MAXC; i++) {
306
+ if (i >= uCaps) break;
307
+ vec4 A = uCap[2 * i]; vec4 B = uCap[2 * i + 1];
308
+ float d = sdCap(p, A.xyz, B.xyz, A.w);
309
+ if (d < best.x) best = vec3(d, B.w, float(100 + i));
310
+ }
311
+ return best;
312
+ }
313
+ vec3 normalAt(vec3 p) {
314
+ const vec2 k = vec2(1.0, -1.0);
315
+ const float e = 0.002;
316
+ return normalize(k.xyy * map(p + k.xyy * e).x + k.yyx * map(p + k.yyx * e).x + k.yxy * map(p + k.yxy * e).x + k.xxx * map(p + k.xxx * e).x);
317
+ }
318
+ float shadowAt(vec3 p, vec3 l) {
319
+ float res = 1.0, t = 0.05;
320
+ for (int i = 0; i < 24; i++) {
321
+ float h = map(p + l * t).x;
322
+ res = min(res, 10.0 * h / t);
323
+ t += clamp(h, 0.04, 0.6);
324
+ if (res < 0.02 || t > 18.0) break;
325
+ }
326
+ return clamp(res, 0.0, 1.0);
327
+ }
328
+ float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
329
+ float vnoise(vec2 p) { vec2 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f); return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y); }
330
+ float fbm(vec2 p) { float v = 0.0, a = 0.5; for (int i = 0; i < 5; i++) { v += a * vnoise(p); p *= 2.03; a *= 0.5; } return v; }
331
+
332
+ // Checker tiles on whichever face a point is on, with a dark joint.
333
+ float tiles(vec3 p, vec3 n) {
334
+ vec2 uv = abs(n.y) > 0.6 ? p.xz : abs(n.x) > abs(n.z) ? p.zy : p.xy;
335
+ vec2 g = uv * 1.0;
336
+ vec2 f = fract(g);
337
+ float joint = step(0.95, max(f.x, f.y)) * 0.06;
338
+ float chk = mod(floor(g.x) + floor(g.y), 2.0);
339
+ return 0.16 * chk - 0.08 - joint;
340
+ }
341
+ vec4 matOf(int i) { return texelFetch(uMats, ivec2(i, 0), 0); }
342
+
343
+ void main() {
344
+ vec2 uv = (gl_FragCoord.xy / uRes) * 2.0 - 1.0;
345
+ float aspect = uRes.x / uRes.y;
346
+ vec3 rd = normalize(uFwd + uv.x * uTan * aspect * uRight + uv.y * uTan * uUp);
347
+ vec3 ro = uEye;
348
+ float t = 0.05;
349
+ vec3 hit = vec3(1e9, -1.0, -1.0);
350
+ for (int i = 0; i < 110; i++) {
351
+ vec3 h = map(ro + rd * t);
352
+ if (h.x < 0.0015 * t) { hit = vec3(t, h.y, h.z); break; }
353
+ t += h.x;
354
+ if (t > FAR) break;
355
+ }
356
+ float tw = rd.y < -1e-4 ? (uWaterY - ro.y) / rd.y : 1e9;
357
+ float L; float ramp; float id; float depth; float mat; float glow = 0.0; float facing = 1.0;
358
+ if (tw < hit.x && tw < FAR) {
359
+ // The water: dark and deep, bright where it meets what stands in it, rippled, catching the sky.
360
+ vec3 p = ro + rd * tw;
361
+ float edge = clamp(1.0 - map(p).x / 0.9, 0.0, 1.0);
362
+ float ripple = fbm(p.xz * 1.7 + vec2(uTime * 0.35, uTime * 0.2)) ;
363
+ float glint = step(0.72, fbm(p.xz * 5.0 + uTime * 0.8)) * 0.35 * clamp(1.0 - tw / 40.0, 0.0, 1.0);
364
+ L = 0.1 + 0.16 * ripple * ripple + 0.7 * pow(edge, 3.0) + glint;
365
+ L *= mix(1.0, 0.45, clamp((tw - uFogNear) / (uFogFar - uFogNear), 0.0, 1.0));
366
+ ramp = matOf(${WATER_MAT}).x; id = 254.0; depth = tw / FAR; mat = ${WATER_MAT}.0; facing = -rd.y;
367
+ } else if (hit.y >= 0.0) {
368
+ vec3 p = ro + rd * hit.x;
369
+ vec3 n = normalAt(p);
370
+ int mi = int(hit.y + 0.5);
371
+ vec4 mr = matOf(mi);
372
+ float sh = shadowAt(p + n * 0.01, uSun);
373
+ float diff = max(dot(n, uSun), 0.0) * sh;
374
+ // (Water light: the glow off the water reaches the bottom of walls.)
375
+ float wglow = clamp(1.0 - (p.y - uWaterY) / 1.6, 0.0, 1.0) * max(0.0, -n.y * 0.2 + 0.8) * 0.35;
376
+ L = (0.16 + 0.1 * n.y + 0.5 * diff) * mr.y + wglow + mr.w; // (headroom: the brightest lit face stays under the top of its ramp)
377
+ // (Patterns know the resolution: a tile a few pixels across fades rather than aliasing, and small targets
378
+ // quieten every pattern so the subject still reads.)
379
+ float tilePx = uRes.y / max(hit.x * 2.0 * uTan, 1e-3);
380
+ float patternK = smoothstep(2.5, 7.0, tilePx) * (0.35 + 0.65 * smoothstep(28.0, 96.0, uRes.y));
381
+ if (mr.z > 0.5 && mr.z < 1.5) L += tiles(p, n) * patternK;
382
+ L = mix(L, 0.12, clamp((hit.x - uFogNear) / (uFogFar - uFogNear), 0.0, 1.0));
383
+ ramp = mr.x; id = mod(hit.z, 250.0); depth = hit.x / FAR; mat = float(mi);
384
+ glow = clamp(mr.w, 0.0, 1.0); facing = clamp(dot(n, -rd), 0.0, 1.0);
385
+ } else {
386
+ // The sky: a lid of low cloud, lit warm from somewhere, dark at the horizon.
387
+ float up = max(rd.y, 0.0);
388
+ vec2 sp = rd.xz / (rd.y + 0.12) * 1.4 + vec2(uTime * 0.02, 0.0);
389
+ float cl = fbm(sp) * smoothstep(0.02, 0.35, up);
390
+ L = 0.08 + cl * 0.75;
391
+ ramp = matOf(${SKY_MAT}).x; id = 255.0; depth = 1.0; mat = ${SKY_MAT}.0;
392
+ }
393
+ outData = vec4(clamp(L, 0.0, 1.0), ramp / 255.0, mat / 255.0, id / 255.0);
394
+ outData2 = vec4(glow, facing, 0.0, 0.0);
395
+ gl_FragDepth = clamp(depth, 0.0, 1.0);
396
+ }`;
397
+ var POINTS_VS = `#version 300 es
398
+ in vec4 aPos; // xyz, size
399
+ in vec3 aLook; // lightness, ramp, glow
400
+ uniform vec3 uEye, uFwd, uRight, uUp;
401
+ uniform float uTan, uAspect, uH;
402
+ out vec3 vLook;
403
+ void main() {
404
+ vec3 v = aPos.xyz - uEye;
405
+ float z = dot(v, uFwd);
406
+ float depth = length(v) / ${FAR.toFixed(1)};
407
+ gl_Position = z > 0.05 ? vec4(dot(v, uRight) / (z * uTan * uAspect), dot(v, uUp) / (z * uTan), depth * 2.0 - 1.0, 1.0) : vec4(2.0, 2.0, 2.0, 1.0);
408
+ gl_PointSize = max(1.0, aPos.w * 6.0 * (uH / 128.0) / max(z, 0.5)); // (a speck is the same size in the world at any target: fewer pixels, smaller dots)
409
+ vLook = aLook;
410
+ }`;
411
+ var POINTS_FS = `#version 300 es
412
+ precision highp float;
413
+ in vec3 vLook;
414
+ layout(location = 0) out vec4 outData;
415
+ layout(location = 1) out vec4 outData2;
416
+ void main() { outData = vec4(vLook.x, vLook.y / 255.0, ${PARTICLE_MAT}.0 / 255.0, 253.0 / 255.0); outData2 = vec4(vLook.z, 1.0, 0.0, 0.0); }`;
417
+
418
+ export {
419
+ ALL_FX,
420
+ FAR,
421
+ FULLSCREEN_VS,
422
+ MAX_BOXES,
423
+ MAX_CAPS,
424
+ MAX_COLOURS,
425
+ MAX_MATERIALS,
426
+ MAX_RAMPS,
427
+ MAX_WEDGES,
428
+ PALETTE_WIDTH,
429
+ POINTS_FS,
430
+ POINTS_VS,
431
+ SCREEN_TILE,
432
+ SKY_MAT,
433
+ WATER_MAT,
434
+ WORLD_FS,
435
+ fxUniforms,
436
+ resolveFx,
437
+ screenTile,
438
+ toggleFxList,
439
+ };