@moood/render 0.1.3 → 0.1.5
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.
- package/README.md +82 -9
- package/cli.js +294 -28
- package/compose.js +71 -0
- package/engine/directions.js +3 -3
- package/engine/engine.js +1600 -150
- package/engine/engine.json +1 -1
- package/engine/gl.js +1092 -216
- package/engine/kit.js +50 -6
- package/engine/motion.html +47 -0
- package/engine/motion.js +258 -0
- package/engine/render.html +11 -3
- package/engine/scenestate.js +127 -15
- package/engine/select.js +4 -3
- package/engine/soma.bin +0 -0
- package/engine/soma.md +4 -0
- package/engine/speech.js +23 -19
- package/index.js +135 -26
- package/package.json +4 -2
- package/scene.js +28 -12
- package/video.js +66 -0
package/engine/gl.js
CHANGED
|
@@ -7,32 +7,120 @@
|
|
|
7
7
|
// (like a map), a few per frame, ahead of the camera.
|
|
8
8
|
const MooodGL = (() => {
|
|
9
9
|
const I = Moood.internals;
|
|
10
|
-
const QUADS = 8192, STRIDE =
|
|
10
|
+
const QUADS = 8192, STRIDE = 12; // per vertex: x y u v r g b a, haze k b, lit, depth
|
|
11
11
|
|
|
12
12
|
// Everything is drawn in the frame's pixels (engine.js: the frame is a landscape rectangle inside
|
|
13
13
|
// the canvas, which shows the scene on around it): u_off is where the frame starts in the
|
|
14
14
|
// canvas, u_canvas the canvas's size.
|
|
15
15
|
const VS = `#version 300 es
|
|
16
|
-
in vec2 a_pos; in vec2 a_uv; in vec4 a_col; in vec2 a_haze;
|
|
16
|
+
in vec2 a_pos; in vec2 a_uv; in vec4 a_col; in vec2 a_haze; in vec2 a_lit;
|
|
17
17
|
uniform vec2 u_canvas, u_off;
|
|
18
|
-
out vec2 v_uv; out vec4 v_col; out vec2 v_haze;
|
|
18
|
+
out vec2 v_uv; out vec4 v_col; out vec2 v_haze; out vec2 v_lit;
|
|
19
19
|
void main() {
|
|
20
|
-
v_uv = a_uv; v_col = a_col; v_haze = a_haze;
|
|
20
|
+
v_uv = a_uv; v_col = a_col; v_haze = a_haze; v_lit = a_lit;
|
|
21
21
|
vec2 p = a_pos + u_off;
|
|
22
22
|
gl_Position = vec4(p.x / u_canvas.x * 2.0 - 1.0, 1.0 - p.y / u_canvas.y * 2.0, 0.0, 1.0);
|
|
23
23
|
}`;
|
|
24
|
+
// A lit scene (engine.js: lookOf) draws into three targets: the colour; what is lit and how far
|
|
25
|
+
// it is (lit·a, lit·depth·a, 0, a — composed like a colour, so the nearest painting wins); and the
|
|
26
|
+
// glows (additive quads: light in the air, covered by what is drawn in front of them later). The
|
|
27
|
+
// final pass lays the light on the colour by the depth, then adds the glows.
|
|
24
28
|
const FS = `#version 300 es
|
|
25
29
|
precision mediump float;
|
|
26
|
-
in vec2 v_uv; in vec4 v_col; in vec2 v_haze;
|
|
30
|
+
in vec2 v_uv; in vec4 v_col; in vec2 v_haze; in vec2 v_lit;
|
|
27
31
|
uniform sampler2D u_tex;
|
|
28
32
|
uniform vec3 u_fog;
|
|
29
|
-
out vec4 o;
|
|
33
|
+
layout(location = 0) out vec4 o;
|
|
34
|
+
#ifdef LIT
|
|
35
|
+
layout(location = 1) out vec4 o_lit;
|
|
36
|
+
layout(location = 2) out vec4 o_glow;
|
|
37
|
+
#endif
|
|
30
38
|
// aerial perspective (see haze()): colour·k + fog·b, premultiplied
|
|
31
39
|
void main() {
|
|
32
|
-
|
|
40
|
+
vec4 c = texture(u_tex, v_uv) * v_col;
|
|
33
41
|
// painted colours don't always hold the haze we assume: never darken more than 40%; an
|
|
34
42
|
// additive quad (light, alpha 0) is never brightened by it
|
|
35
|
-
|
|
43
|
+
c.rgb = clamp(c.rgb * v_haze.x + u_fog * v_haze.y * c.a, c.rgb * 0.6, c.a > 0.0 ? vec3(c.a) : c.rgb);
|
|
44
|
+
#ifdef LIT
|
|
45
|
+
if (v_col.a > 0.0) { o = c; o_lit = vec4(v_lit.x * c.a, v_lit.x * v_lit.y * c.a, 0.0, c.a); o_glow = vec4(0.0, 0.0, 0.0, c.a); }
|
|
46
|
+
else { o = vec4(0.0); o_lit = vec4(0.0); o_glow = vec4(c.rgb, 0.0); }
|
|
47
|
+
#else
|
|
48
|
+
o = c;
|
|
49
|
+
#endif
|
|
50
|
+
}`;
|
|
51
|
+
// the same, cut by a mask (an element's `clip`): what other elements painted this frame, in a
|
|
52
|
+
// target as big as the canvas — seen inside it (u_inside 1) or outside it (0)
|
|
53
|
+
const CFS = FS.replace('uniform vec3 u_fog;', 'uniform vec3 u_fog;\n uniform sampler2D u_mask;\n uniform highp vec2 u_canvas;\n uniform float u_inside;')
|
|
54
|
+
.replace(/}\s*$/, ' float m = texture(u_mask, gl_FragCoord.xy / u_canvas).a, k = mix(1.0 - m, m, u_inside);\n o *= k;\n #ifdef LIT\n o_lit *= k; o_glow *= k;\n #endif\n }');
|
|
55
|
+
// A reflecting plane's mirror pass (mirrors): what is above the plane drawn where its mirror image
|
|
56
|
+
// is seen, into three targets like a lit scene's — the colour; [height above the plane (eye heights),
|
|
57
|
+
// px from where it meets it] (v_lit in this pass, premultiplied); the glows. Below the plane: nothing.
|
|
58
|
+
// (Its lit variant is drawn: litOf.)
|
|
59
|
+
const MFS = FS.replace('o_lit = vec4(v_lit.x * c.a, v_lit.x * v_lit.y * c.a, 0.0, c.a);', 'o_lit = vec4(v_lit * c.a, 0.0, c.a);')
|
|
60
|
+
.replace('void main() {', 'void main() {\n if (v_lit.y < 0.0) discard;');
|
|
61
|
+
// ... laid on the plane (drawn as its cells): only where it is painted (u_tex), u_strength of it,
|
|
62
|
+
// fading out over u_fade above the plane, blurred the more the farther above it (u_blur per px from
|
|
63
|
+
// where it meets the plane: the mirror's mip levels). The height of what is mirrored where nothing
|
|
64
|
+
// is: the average around.
|
|
65
|
+
const RFS = `#version 300 es
|
|
66
|
+
precision highp float;
|
|
67
|
+
in vec2 v_uv;
|
|
68
|
+
uniform sampler2D u_tex, u_mc, u_mh, u_mg;
|
|
69
|
+
uniform vec2 u_canvas;
|
|
70
|
+
uniform float u_strength, u_fade, u_blur;
|
|
71
|
+
layout(location = 0) out vec4 o;
|
|
72
|
+
#ifdef LIT
|
|
73
|
+
layout(location = 1) out vec4 o_lit;
|
|
74
|
+
layout(location = 2) out vec4 o_glow;
|
|
75
|
+
#endif
|
|
76
|
+
void main() {
|
|
77
|
+
vec2 q = gl_FragCoord.xy / u_canvas;
|
|
78
|
+
vec4 hc = textureLod(u_mh, q, 0.0), hb = textureLod(u_mh, q, 4.0);
|
|
79
|
+
vec2 hd = hc.a > 0.5 ? hc.rg / hc.a : hb.rg / max(hb.a, 1e-4);
|
|
80
|
+
float lod = min(5.0, log2(max(1.0, u_blur * hd.y * 0.5)));
|
|
81
|
+
vec2 d = exp2(lod) / u_canvas;
|
|
82
|
+
vec4 c = vec4(0.0); vec3 gw = vec3(0.0);
|
|
83
|
+
for (int i = 0; i < 4; i++) {
|
|
84
|
+
vec2 p = q + d * vec2(i == 0 || i == 3 ? -1.0 : 1.0, i < 2 ? -1.0 : 1.0);
|
|
85
|
+
c += textureLod(u_mc, p, lod); gw += textureLod(u_mg, p, lod).rgb;
|
|
86
|
+
}
|
|
87
|
+
float f = u_fade > 0.0 ? 1.0 - smoothstep(0.0, u_fade, hd.x) : 1.0, k = 0.25 * u_strength * f * texture(u_tex, v_uv).a;
|
|
88
|
+
#ifdef LIT
|
|
89
|
+
o = c * k; o_lit = vec4(0.0); o_glow = vec4(gw * k, 0.0);
|
|
90
|
+
#else
|
|
91
|
+
o = vec4((c.rgb + gw) * k, c.a * k);
|
|
92
|
+
#endif
|
|
93
|
+
}`;
|
|
94
|
+
// A plane's pool of light (`lit`: the scene's point lights where the scene is not lit), drawn as its
|
|
95
|
+
// cells, added: its painting (u_tex) lit that much more by each light by its distance — a point of it
|
|
96
|
+
// in the world from its uv (the plane: u_pc its centre, u_pa its size and turn)
|
|
97
|
+
const LFS = `#version 300 es
|
|
98
|
+
precision highp float;
|
|
99
|
+
in vec2 v_uv; in vec2 v_haze;
|
|
100
|
+
uniform sampler2D u_tex;
|
|
101
|
+
uniform vec3 u_pc;
|
|
102
|
+
uniform vec4 u_pa;
|
|
103
|
+
uniform int u_n;
|
|
104
|
+
uniform vec4 u_lp[8];
|
|
105
|
+
uniform vec3 u_lc[8];
|
|
106
|
+
layout(location = 0) out vec4 o;
|
|
107
|
+
#ifdef LIT
|
|
108
|
+
layout(location = 1) out vec4 o_lit;
|
|
109
|
+
layout(location = 2) out vec4 o_glow;
|
|
110
|
+
#endif
|
|
111
|
+
void main() {
|
|
112
|
+
vec4 c = texture(u_tex, v_uv);
|
|
113
|
+
float u = (v_uv.x - 0.5) * u_pa.x, v = (v_uv.y - 0.5) * u_pa.y;
|
|
114
|
+
vec3 P = vec3(u_pc.x + u * u_pa.z + v * u_pa.w, u_pc.y, u_pc.z - v * u_pa.z + u * u_pa.w), L = vec3(0.0);
|
|
115
|
+
for (int i = 0; i < 8; i++) {
|
|
116
|
+
if (i >= u_n) break;
|
|
117
|
+
float x = length(u_lp[i].xyz - P) / u_lp[i].w;
|
|
118
|
+
if (x < 1.0) L += u_lc[i] * (1.0 - x * x) * (1.0 - x * x);
|
|
119
|
+
}
|
|
120
|
+
o = vec4((c.rgb + 0.12 * c.a) * L * v_haze.x, 0.0);
|
|
121
|
+
#ifdef LIT
|
|
122
|
+
o_lit = vec4(0.0); o_glow = vec4(0.0);
|
|
123
|
+
#endif
|
|
36
124
|
}`;
|
|
37
125
|
// full-screen passes: a triangle pair in pixel coordinates
|
|
38
126
|
const QVS = `#version 300 es
|
|
@@ -45,7 +133,11 @@ const MooodGL = (() => {
|
|
|
45
133
|
uniform vec2 u_res, u_canvas, u_off; // the frame, the canvas, where the frame starts in it
|
|
46
134
|
uniform float u_wy, u_k, u_t, u_speed, u_u;
|
|
47
135
|
uniform vec3 u_col;
|
|
48
|
-
out vec4 o;
|
|
136
|
+
layout(location = 0) out vec4 o;
|
|
137
|
+
#ifdef LIT
|
|
138
|
+
layout(location = 1) out vec4 o_lit;
|
|
139
|
+
layout(location = 2) out vec4 o_glow;
|
|
140
|
+
#endif
|
|
49
141
|
void main() {
|
|
50
142
|
float px = gl_FragCoord.x - u_off.x, py = u_canvas.y - gl_FragCoord.y - u_off.y;
|
|
51
143
|
float depth = u_res.y - u_wy, d = clamp((py - u_wy) / depth, 0.0, 1.0);
|
|
@@ -55,29 +147,73 @@ const MooodGL = (() => {
|
|
|
55
147
|
vec3 c = mix(refl, u_col, mix(0.35, 0.8, d));
|
|
56
148
|
c += vec3(0.06) * (1.0 - step(u_u, py - u_wy)); // thin bright line on the horizon
|
|
57
149
|
o = vec4(c, 1.0);
|
|
150
|
+
#ifdef LIT
|
|
151
|
+
// a row h below the horizon is about 1/h eye heights away
|
|
152
|
+
o_lit = vec4(1.0, clamp(u_res.y / max(1.0, py - u_wy), 0.3, 150.0), 0.0, 1.0);
|
|
153
|
+
o_glow = vec4(0.0);
|
|
154
|
+
#endif
|
|
58
155
|
}`;
|
|
59
|
-
// Final pass: soft-light tint, vignette, per-pixel film grain (overlay), fresh every frame
|
|
156
|
+
// Final pass: soft-light tint, vignette, per-pixel film grain (overlay), fresh every frame; the
|
|
157
|
+
// scene's brightness and saturation (its look, engine.js: lookOf). A lit scene: first the light
|
|
158
|
+
// laid on the colour — the ambient, and each point source by how far the painted thing under the
|
|
159
|
+
// pixel is from it (a source behind it, farther along the gaze, lights it little: a figure before
|
|
160
|
+
// a lamp is a silhouette, a wall behind the lamp is lit) — then the glows.
|
|
161
|
+
const LIGHTS = 8;
|
|
60
162
|
const PFS = `#version 300 es
|
|
61
163
|
precision highp float;
|
|
62
164
|
uniform sampler2D u_scene;
|
|
63
165
|
uniform vec2 u_res, u_at, u_canvas; // the frame's size, where it starts in the canvas (window px), the canvas
|
|
64
166
|
uniform vec3 u_tint;
|
|
65
|
-
uniform float u_tintA, u_vig, u_grain, u_seed, u_fade;
|
|
167
|
+
uniform float u_tintA, u_vig, u_grain, u_seed, u_fade, u_bright, u_sat;
|
|
66
168
|
uniform vec3 u_fadeCol;
|
|
169
|
+
#ifdef LIT
|
|
170
|
+
uniform sampler2D u_lit, u_glow;
|
|
171
|
+
uniform vec3 u_amb;
|
|
172
|
+
uniform int u_n;
|
|
173
|
+
uniform vec4 u_lp[${LIGHTS}]; // a point source: where (eye heights), and its reach
|
|
174
|
+
uniform vec3 u_lc[${LIGHTS}]; // its colour × strength
|
|
175
|
+
uniform vec4 u_cam; // the camera: x, z, height, yaw
|
|
176
|
+
uniform vec2 u_view; // the gaze lowered (screen heights), the horizon
|
|
177
|
+
#endif
|
|
67
178
|
out vec4 o;
|
|
68
179
|
float hash(vec2 p) { vec3 q = fract(vec3(p.xyx) * 0.1031); q += dot(q, q.yzx + 33.33); return fract((q.x + q.y) * q.z); }
|
|
69
180
|
vec3 softLight(vec3 b, vec3 s) {
|
|
70
181
|
vec3 d = mix(((16.0 * b - 12.0) * b + 4.0) * b, sqrt(b), step(0.25, b));
|
|
71
182
|
return mix(b - (1.0 - 2.0 * s) * b * (1.0 - b), b + (2.0 * s - 1.0) * (d - b), step(0.5, s));
|
|
72
183
|
}
|
|
184
|
+
#ifdef LIT
|
|
185
|
+
// the light on the painted colour c at frame pixel px (y up), its depth D along the gaze
|
|
186
|
+
vec3 lit(vec3 c, vec2 px, float D) {
|
|
187
|
+
float xe = (px.x - u_res.x * 0.5) / u_res.y * D, ye = (u_res.y - px.y) / u_res.y;
|
|
188
|
+
float cy = cos(u_cam.w), sy = sin(u_cam.w);
|
|
189
|
+
vec3 P = vec3(u_cam.x + xe * cy + D * sy, u_cam.z - (ye - u_view.y + u_view.x) * D, u_cam.y - xe * sy + D * cy);
|
|
190
|
+
vec3 L = u_amb;
|
|
191
|
+
for (int i = 0; i < ${LIGHTS}; i++) {
|
|
192
|
+
if (i >= u_n) break;
|
|
193
|
+
vec3 d = u_lp[i].xyz - P;
|
|
194
|
+
float x = length(d) / u_lp[i].w;
|
|
195
|
+
if (x >= 1.0) continue;
|
|
196
|
+
float f = (1.0 - x * x) * (1.0 - x * x);
|
|
197
|
+
float Dl = (u_lp[i].x - u_cam.x) * sy + (u_lp[i].z - u_cam.y) * cy;
|
|
198
|
+
L += u_lc[i] * f * mix(1.0, 0.3, smoothstep(0.02, 0.3, Dl - D));
|
|
199
|
+
}
|
|
200
|
+
return c * L;
|
|
201
|
+
}
|
|
202
|
+
#endif
|
|
73
203
|
// Around the frame the scene goes on as painted; where nothing is painted (alpha 0), the colour at
|
|
74
204
|
// the frame's nearest edge carries on. Out of the frame it dims gently toward the canvas's edges.
|
|
75
205
|
void main() {
|
|
76
206
|
vec2 q = gl_FragCoord.xy, px = q - u_at;
|
|
77
207
|
vec4 s = texture(u_scene, q / u_canvas);
|
|
78
208
|
vec3 edge = texture(u_scene, clamp(q, u_at + 0.5, u_at + u_res - 0.5) / u_canvas).rgb;
|
|
209
|
+
#ifdef LIT
|
|
210
|
+
vec4 l = texture(u_lit, q / u_canvas);
|
|
211
|
+
if (l.r > 0.002) s.rgb = mix(s.rgb, lit(s.rgb, px, l.g / l.r), min(1.0, l.r / max(s.a, 0.002)));
|
|
212
|
+
s.rgb += texture(u_glow, q / u_canvas).rgb;
|
|
213
|
+
#endif
|
|
79
214
|
vec3 c = s.rgb + (1.0 - s.a) * edge;
|
|
80
215
|
c = mix(c, softLight(c, u_tint), u_tintA);
|
|
216
|
+
c = mix(vec3(dot(c, vec3(0.299, 0.587, 0.114))), c, u_sat) * u_bright;
|
|
81
217
|
float r0 = min(u_res.x, u_res.y) * 0.35, r1 = length(u_res) * 0.6;
|
|
82
218
|
c *= 1.0 - u_vig * clamp((length(px - u_res * 0.5) - r0) / (r1 - r0), 0.0, 1.0);
|
|
83
219
|
vec2 out_ = max(max(-px, px - u_res), 0.0);
|
|
@@ -86,6 +222,32 @@ const MooodGL = (() => {
|
|
|
86
222
|
vec3 ov = mix(2.0 * c * n, 1.0 - 2.0 * (1.0 - c) * (1.0 - n), step(0.5, c));
|
|
87
223
|
o = vec4(mix(mix(c, ov, u_grain), u_fadeCol, u_fade), 1.0);
|
|
88
224
|
}`;
|
|
225
|
+
// A shadow: the caster's painting, black, laid where it falls (on a floor, on a wall) — through
|
|
226
|
+
// the alpha of what it falls on (u_mask; outside it, nothing), as dark as v_a.
|
|
227
|
+
const SVS = `#version 300 es
|
|
228
|
+
in vec2 a_pos; in vec2 a_uv; in vec2 a_uv2; in float a_a; in vec2 a_blur;
|
|
229
|
+
uniform vec2 u_canvas, u_off;
|
|
230
|
+
out vec2 v_uv; out vec2 v_uv2; out float v_a; out vec2 v_blur;
|
|
231
|
+
void main() {
|
|
232
|
+
v_uv = a_uv; v_uv2 = a_uv2; v_a = a_a; v_blur = a_blur;
|
|
233
|
+
vec2 p = a_pos + u_off;
|
|
234
|
+
gl_Position = vec4(p.x / u_canvas.x * 2.0 - 1.0, 1.0 - p.y / u_canvas.y * 2.0, 0.0, 1.0);
|
|
235
|
+
}`;
|
|
236
|
+
const SFS = `#version 300 es
|
|
237
|
+
precision mediump float;
|
|
238
|
+
in vec2 v_uv; in vec2 v_uv2; in float v_a; in vec2 v_blur;
|
|
239
|
+
uniform sampler2D u_tex, u_mask;
|
|
240
|
+
out vec4 o;
|
|
241
|
+
void main() {
|
|
242
|
+
float inside = step(0.0, v_uv2.x) * step(v_uv2.x, 1.0) * step(0.0, v_uv2.y) * step(v_uv2.y, 1.0);
|
|
243
|
+
// the caster's alpha over a disc as wide as the penumbra (a spiral of taps)
|
|
244
|
+
float a = 0.0;
|
|
245
|
+
for (int i = 0; i < 16; i++) {
|
|
246
|
+
float f = (float(i) + 0.5) / 16.0, t = float(i) * 2.39996;
|
|
247
|
+
a += texture(u_tex, v_uv + vec2(cos(t), sin(t)) * sqrt(f) * v_blur).a;
|
|
248
|
+
}
|
|
249
|
+
o = vec4(0.0, 0.0, 0.0, a / 16.0 * texture(u_mask, v_uv2).a * inside * v_a);
|
|
250
|
+
}`;
|
|
89
251
|
|
|
90
252
|
function program(gl, vs, fs) {
|
|
91
253
|
const p = gl.createProgram();
|
|
@@ -100,11 +262,17 @@ const MooodGL = (() => {
|
|
|
100
262
|
gl.bindAttribLocation(p, 1, 'a_uv');
|
|
101
263
|
gl.bindAttribLocation(p, 2, 'a_col');
|
|
102
264
|
gl.bindAttribLocation(p, 3, 'a_haze');
|
|
265
|
+
gl.bindAttribLocation(p, 4, 'a_lit');
|
|
266
|
+
gl.bindAttribLocation(p, 2, 'a_uv2');
|
|
267
|
+
gl.bindAttribLocation(p, 3, 'a_a');
|
|
268
|
+
gl.bindAttribLocation(p, 4, 'a_blur');
|
|
103
269
|
gl.linkProgram(p);
|
|
104
270
|
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
|
|
105
271
|
return p;
|
|
106
272
|
}
|
|
107
273
|
const uniforms = (gl, p, names) => Object.fromEntries(names.map(n => [n, gl.getUniformLocation(p, 'u_' + n)]));
|
|
274
|
+
// a shader's lit variant (LIT defined)
|
|
275
|
+
const litOf = src => src.replace('#version 300 es', '#version 300 es\n #define LIT');
|
|
108
276
|
|
|
109
277
|
// small 2D helpers for sprite textures
|
|
110
278
|
function paint(w, h, fn) { const c = I.layer(w, h); fn(c.getContext('2d'), c.width, c.height); return c; }
|
|
@@ -134,10 +302,30 @@ const MooodGL = (() => {
|
|
|
134
302
|
function create(canvas) {
|
|
135
303
|
const gl = canvas.getContext('webgl2', { alpha: false, antialias: false, depth: false, stencil: false, premultipliedAlpha: true, preserveDrawingBuffer: false });
|
|
136
304
|
if (!gl) return null;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
305
|
+
// the programs, plain and lit (made when a scene is first lit): { prog, cprog (cut by a mask:
|
|
306
|
+
// clip), wprog, pprog } and their uniforms; P: the current frame's
|
|
307
|
+
const programs = lit => {
|
|
308
|
+
const v = lit ? litOf : s => s, prog = program(gl, VS, v(FS)), cprog = program(gl, VS, v(CFS)), wprog = program(gl, QVS, v(WFS)), pprog = program(gl, QVS, v(PFS));
|
|
309
|
+
const rprog = program(gl, VS, v(RFS)), lprog = program(gl, VS, v(LFS));
|
|
310
|
+
return {
|
|
311
|
+
prog, cprog, wprog, pprog, rprog, lprog,
|
|
312
|
+
ru: uniforms(gl, rprog, ['canvas', 'off', 'tex', 'fog', 'mc', 'mh', 'mg', 'strength', 'fade', 'blur']),
|
|
313
|
+
lu: uniforms(gl, lprog, ['canvas', 'off', 'tex', 'fog', 'pc', 'pa', 'n', 'lp', 'lc']),
|
|
314
|
+
su: uniforms(gl, prog, ['canvas', 'off', 'tex', 'fog']),
|
|
315
|
+
cu: uniforms(gl, cprog, ['canvas', 'off', 'tex', 'fog', 'mask', 'inside']),
|
|
316
|
+
wu: uniforms(gl, wprog, ['res', 'canvas', 'off', 'tex', 'wy', 'k', 't', 'speed', 'u', 'col']),
|
|
317
|
+
pu: uniforms(gl, pprog, ['res', 'at', 'canvas', 'off', 'scene', 'tint', 'tintA', 'vig', 'grain', 'seed', 'fade', 'fadeCol', 'bright', 'sat', 'lit', 'glow', 'amb', 'n', 'lp', 'lc', 'cam', 'view']),
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
const PLAIN = programs(false);
|
|
321
|
+
let LITP = null, P = PLAIN;
|
|
322
|
+
// a lit scene needs a target it can write depths to (16-bit floats: either extension lets it)
|
|
323
|
+
const canLight = !!(gl.getExtension('EXT_color_buffer_float') || gl.getExtension('EXT_color_buffer_half_float'));
|
|
324
|
+
const sprog = program(gl, SVS, SFS), shu = uniforms(gl, sprog, ['canvas', 'off', 'tex', 'mask']);
|
|
325
|
+
// the mirror pass's program (made when a scene first has a reflecting plane: it needs the depths'
|
|
326
|
+
// targets too)
|
|
327
|
+
let MIRP = null;
|
|
328
|
+
const mirrorProgram = () => { const prog = program(gl, VS, litOf(MFS)); return { prog, su: uniforms(gl, prog, ['canvas', 'off', 'tex', 'fog']) }; };
|
|
141
329
|
|
|
142
330
|
const data = new Float32Array(QUADS * 4 * STRIDE);
|
|
143
331
|
const vao = gl.createVertexArray();
|
|
@@ -153,6 +341,19 @@ const MooodGL = (() => {
|
|
|
153
341
|
gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 2, gl.FLOAT, false, STRIDE * 4, 8);
|
|
154
342
|
gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 4, gl.FLOAT, false, STRIDE * 4, 16);
|
|
155
343
|
gl.enableVertexAttribArray(3); gl.vertexAttribPointer(3, 2, gl.FLOAT, false, STRIDE * 4, 32);
|
|
344
|
+
gl.enableVertexAttribArray(4); gl.vertexAttribPointer(4, 2, gl.FLOAT, false, STRIDE * 4, 40);
|
|
345
|
+
// shadows: x y, u v (the caster), u v (what it falls on), alpha, blur (u v) — as triangles
|
|
346
|
+
const SH_STRIDE = 9, SH_MAX = 6 * 4096;
|
|
347
|
+
const svao = gl.createVertexArray();
|
|
348
|
+
gl.bindVertexArray(svao);
|
|
349
|
+
const svbo = gl.createBuffer();
|
|
350
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, svbo);
|
|
351
|
+
gl.bufferData(gl.ARRAY_BUFFER, SH_MAX * SH_STRIDE * 4, gl.DYNAMIC_DRAW);
|
|
352
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, SH_STRIDE * 4, 0);
|
|
353
|
+
gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 2, gl.FLOAT, false, SH_STRIDE * 4, 8);
|
|
354
|
+
gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 2, gl.FLOAT, false, SH_STRIDE * 4, 16);
|
|
355
|
+
gl.enableVertexAttribArray(3); gl.vertexAttribPointer(3, 1, gl.FLOAT, false, SH_STRIDE * 4, 24);
|
|
356
|
+
gl.enableVertexAttribArray(4); gl.vertexAttribPointer(4, 2, gl.FLOAT, false, SH_STRIDE * 4, 28);
|
|
156
357
|
const qvao = gl.createVertexArray();
|
|
157
358
|
gl.bindVertexArray(qvao);
|
|
158
359
|
const qvbo = gl.createBuffer();
|
|
@@ -188,7 +389,49 @@ const MooodGL = (() => {
|
|
|
188
389
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
189
390
|
return { tex, fbo };
|
|
190
391
|
}
|
|
191
|
-
|
|
392
|
+
// a lit scene's target: the colour, what is lit and how far (16-bit floats), the glows — one
|
|
393
|
+
// framebuffer drawing to all three
|
|
394
|
+
function litTarget(w, h) {
|
|
395
|
+
const fbo = gl.createFramebuffer(), texs = [];
|
|
396
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
397
|
+
[gl.RGBA8, gl.RGBA16F, gl.RGBA8].forEach((format, i) => {
|
|
398
|
+
const tex = gl.createTexture();
|
|
399
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
400
|
+
gl.texStorage2D(gl.TEXTURE_2D, 1, format, w, h);
|
|
401
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, i === 1 ? gl.NEAREST : gl.LINEAR);
|
|
402
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, i === 1 ? gl.NEAREST : gl.LINEAR);
|
|
403
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
404
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
405
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, tex, 0);
|
|
406
|
+
texs.push(tex);
|
|
407
|
+
});
|
|
408
|
+
gl.drawBuffers(ALL_BUFFERS);
|
|
409
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
410
|
+
return { tex: texs[0], lit: texs[1], glow: texs[2], fbo };
|
|
411
|
+
}
|
|
412
|
+
// a reflecting plane's mirror (MFS): the colour, [height, px] (16-bit floats), the glows — at half
|
|
413
|
+
// the canvas's size, with MIRROR_LEVELS mip levels its blur is read from
|
|
414
|
+
const MIRROR_LEVELS = 6;
|
|
415
|
+
function mirrorTarget(w, h) {
|
|
416
|
+
const fbo = gl.createFramebuffer(), texs = [];
|
|
417
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
418
|
+
[gl.RGBA8, gl.RGBA16F, gl.RGBA8].forEach((format, i) => {
|
|
419
|
+
const tex = gl.createTexture();
|
|
420
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
421
|
+
gl.texStorage2D(gl.TEXTURE_2D, MIRROR_LEVELS, format, w, h);
|
|
422
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
|
423
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
424
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
425
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
426
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, tex, 0);
|
|
427
|
+
texs.push(tex);
|
|
428
|
+
});
|
|
429
|
+
gl.drawBuffers(ALL_BUFFERS);
|
|
430
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
431
|
+
return { tex: texs[0], lit: texs[1], glow: texs[2], fbo, w, h };
|
|
432
|
+
}
|
|
433
|
+
const ALL_BUFFERS = [gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2], COLOUR_ONLY = [gl.COLOR_ATTACHMENT0, gl.NONE, gl.NONE];
|
|
434
|
+
const dropTarget = r => { for (const t of [r.tex, r.lit, r.glow]) if (t) gl.deleteTexture(t); gl.deleteFramebuffer(r.fbo); };
|
|
192
435
|
|
|
193
436
|
// textures shared by every scene
|
|
194
437
|
const T = {
|
|
@@ -206,32 +449,61 @@ const MooodGL = (() => {
|
|
|
206
449
|
// OX, OY: where the frame starts in the canvas, CW × CH the canvas (px); what is seen is the
|
|
207
450
|
// canvas — in the frame's pixels from (VX0, VY0) to (VX1, VY1)
|
|
208
451
|
let n = 0, curTex = null, curAdd = false, W = 1, H = 1, SW = 1, ROUND = false;
|
|
452
|
+
// what the quads drawn now are cut by (an element's `clip`): { tex: the mask, inside }, or null
|
|
453
|
+
let CLIP = null;
|
|
209
454
|
let OX = 0, OY = 0, CW = 1, CH = 1, VX0 = 0, VY0 = 0, VX1 = 1, VY1 = 1;
|
|
210
455
|
// Aerial perspective: air between you and a thing tints it toward the horizon's sky
|
|
211
456
|
// colour, h(D) = 1 − e^(−D/HAZE). The scene is painted as seen from the start, so its
|
|
212
457
|
// colours already hold the haze of distance Z; walking to distance D we take that out and
|
|
213
458
|
// put the new amount in: c·k + fog·b, k = (1−h(D))/(1−h(Z)), b = h(D) − h(Z)·k.
|
|
214
|
-
// HAZE in eye heights: 220 = clear air (~40% haze at 200 m); spec.light.haze 0…1 → 600…25
|
|
459
|
+
// HAZE in eye heights: 220 = clear air (~40% haze at 200 m); spec.light.haze 0…1 → 600…25. The
|
|
460
|
+
// painting holds the scene's own haze (HAZE0, spec.light.haze); its look now (engine.js:
|
|
461
|
+
// lookOf) may thicken or thin the air (HAZE): what the painting holds is taken out, what the air
|
|
462
|
+
// holds now put in — in the starting view too.
|
|
215
463
|
const NOHAZE = [1, 0];
|
|
216
|
-
let HAZE = 220;
|
|
464
|
+
let HAZE = 220, HAZE0 = 220;
|
|
217
465
|
let HZ = NOHAZE, FOG = [0.5, 0.5, 0.5];
|
|
218
466
|
const hz = D => 1 - Math.exp(-Math.max(0, D) / HAZE);
|
|
219
467
|
// painted for distance Z, seen from D away
|
|
220
468
|
const haze = (Z, D) => {
|
|
221
|
-
const h0 = Math.min(0.85,
|
|
469
|
+
const h0 = Math.min(0.85, 1 - Math.exp(-Math.max(0, Z) / HAZE0)), h1 = hz(D), k = Math.min(3, (1 - h1) / (1 - h0));
|
|
222
470
|
return [k, h1 - h0 * k];
|
|
223
471
|
};
|
|
472
|
+
// A lit scene: is what is drawn now lit by the scene's light, and how far along the gaze it is
|
|
473
|
+
// (eye heights) — [lit 0|1, depth]; the sky, the glows and inserts are not
|
|
474
|
+
const UNLIT = [0, 0];
|
|
475
|
+
let LT = UNLIT;
|
|
476
|
+
// The mirror pass (mirrors): MIR — the plane it mirrors, its level seen A + B/D down the frame D away
|
|
477
|
+
// along the gaze; MD — how far what is drawn now is (a vertex given none). A vertex at y, D away, goes
|
|
478
|
+
// where its mirror image is seen; MH, MP: its height above the plane (eye heights) and px from
|
|
479
|
+
// where it meets it — what the mirror fades and blurs by (MFS, RFS)
|
|
480
|
+
let MIR = null, MD = Infinity, MH = 0, MP = 0;
|
|
481
|
+
const mirrorY = (y, D) => {
|
|
482
|
+
const g = MIR.A + MIR.B / D;
|
|
483
|
+
MP = g - y; MH = isFinite(D) ? Math.max(-1e4, Math.min(1e4, MP * D / H)) : Math.sign(MP) * 1e4;
|
|
484
|
+
return g + MP;
|
|
485
|
+
};
|
|
486
|
+
// a pass over a plane's cells with a program of its own (RFS, LFS): { prog, u, set(u), add }
|
|
487
|
+
let PASS = null;
|
|
224
488
|
function flush() {
|
|
225
489
|
if (!n) return;
|
|
226
|
-
|
|
490
|
+
const u = PASS ? PASS.u : CLIP ? P.cu : P.su;
|
|
491
|
+
gl.useProgram(PASS ? PASS.prog : CLIP ? P.cprog : P.prog);
|
|
227
492
|
gl.bindVertexArray(vao);
|
|
228
|
-
gl.uniform2f(
|
|
229
|
-
gl.uniform2f(
|
|
230
|
-
gl.uniform3f(
|
|
493
|
+
gl.uniform2f(u.canvas, CW, CH);
|
|
494
|
+
gl.uniform2f(u.off, OX, OY);
|
|
495
|
+
gl.uniform3f(u.fog, ...FOG);
|
|
496
|
+
if (CLIP) {
|
|
497
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
498
|
+
gl.bindTexture(gl.TEXTURE_2D, CLIP.tex);
|
|
499
|
+
gl.uniform1i(u.mask, 1);
|
|
500
|
+
gl.uniform1f(u.inside, CLIP.inside ? 1 : 0);
|
|
501
|
+
}
|
|
502
|
+
if (PASS) PASS.set(u);
|
|
231
503
|
gl.activeTexture(gl.TEXTURE0);
|
|
232
504
|
gl.bindTexture(gl.TEXTURE_2D, curTex);
|
|
233
|
-
gl.uniform1i(
|
|
234
|
-
if (curAdd) gl.blendFunc(gl.ONE, gl.ONE); else gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
505
|
+
gl.uniform1i(u.tex, 0);
|
|
506
|
+
if (curAdd || PASS?.add) gl.blendFunc(gl.ONE, gl.ONE); else gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
235
507
|
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
|
236
508
|
gl.bufferSubData(gl.ARRAY_BUFFER, 0, data, 0, n * 4 * STRIDE);
|
|
237
509
|
gl.drawElements(gl.TRIANGLES, n * 6, gl.UNSIGNED_SHORT, 0);
|
|
@@ -240,40 +512,91 @@ const MooodGL = (() => {
|
|
|
240
512
|
// one textured quad: the rect (x, y, w, h) transformed by m, uv rect, tint, alpha.
|
|
241
513
|
// Colours are premultiplied; additive quads get alpha 0 so they only add light.
|
|
242
514
|
const UV = [0, 0, 1, 1];
|
|
243
|
-
function rect(tex, m, x, y, w, h, c = WHITE, a = 1, add = false, uv = UV) {
|
|
244
|
-
if (a <= 0) return;
|
|
515
|
+
function rect(tex, m, x, y, w, h, c = WHITE, a = 1, add = false, uv = UV, a1 = a) {
|
|
516
|
+
if (a <= 0 && a1 <= 0) return;
|
|
245
517
|
if (tex !== curTex || add !== curAdd || n >= QUADS) { flush(); curTex = tex; curAdd = add; }
|
|
246
|
-
const r = c[0] * a, g = c[1] * a, b = c[2] * a, al = add ? 0 : a;
|
|
247
518
|
const ax = m[0] * w, ay = m[1] * w, bx = m[2] * h, by = m[3] * h;
|
|
248
519
|
const ox = m[0] * x + m[2] * y + m[4], oy = m[1] * x + m[3] * y + m[5];
|
|
249
520
|
let k = n * 4 * STRIDE;
|
|
250
|
-
|
|
251
|
-
v(
|
|
252
|
-
v(ox
|
|
253
|
-
v(ox + ax
|
|
254
|
-
v(ox + bx, oy + by, uv[
|
|
521
|
+
// (a1: the alpha along its bottom edge, y + h — a reflection fading away)
|
|
522
|
+
const v = (px, py, u, vv, f) => { if (MIR) py = mirrorY(py, MD); data[k] = px; data[k + 1] = py; data[k + 2] = u; data[k + 3] = vv; data[k + 4] = c[0] * f; data[k + 5] = c[1] * f; data[k + 6] = c[2] * f; data[k + 7] = add ? 0 : f; data[k + 8] = HZ[0]; data[k + 9] = HZ[1]; data[k + 10] = MIR ? MH : LT[0]; data[k + 11] = MIR ? MP : LT[1]; k += STRIDE; };
|
|
523
|
+
v(ox, oy, uv[0], uv[1], a);
|
|
524
|
+
v(ox + ax, oy + ay, uv[2], uv[1], a);
|
|
525
|
+
v(ox + ax + bx, oy + ay + by, uv[2], uv[3], a1);
|
|
526
|
+
v(ox + bx, oy + by, uv[0], uv[3], a1);
|
|
255
527
|
n++;
|
|
256
528
|
}
|
|
257
|
-
// a textured quad with four free corners p = [x0,y0, x1,y1, x2,y2, x3,y3] (clockwise from top-left)
|
|
258
|
-
|
|
529
|
+
// a textured quad with four free corners p = [x0,y0, x1,y1, x2,y2, x3,y3] (clockwise from top-left);
|
|
530
|
+
// d: their depths (a lit scene: a surface going away), else the quad's (LT)
|
|
531
|
+
function quad(tex, p, uv, d = null) {
|
|
259
532
|
if (tex !== curTex || curAdd || n >= QUADS) { flush(); curTex = tex; curAdd = false; }
|
|
260
|
-
const r = c[0] * a, g = c[1] * a, b = c[2] * a;
|
|
261
533
|
let k = n * 4 * STRIDE;
|
|
262
|
-
const v = (i, u, vv) => {
|
|
534
|
+
const v = (i, u, vv) => { const y = MIR ? mirrorY(p[i + 1], d ? d[i >> 1] : MD) : p[i + 1]; data[k] = p[i]; data[k + 1] = y; data[k + 2] = u; data[k + 3] = vv; data[k + 4] = 1; data[k + 5] = 1; data[k + 6] = 1; data[k + 7] = 1; data[k + 8] = HZ[0]; data[k + 9] = HZ[1]; data[k + 10] = MIR ? MH : LT[0]; data[k + 11] = MIR ? MP : d ? d[i >> 1] : LT[1]; k += STRIDE; };
|
|
263
535
|
v(0, uv[0], uv[1]); v(2, uv[2], uv[1]); v(4, uv[2], uv[3]); v(6, uv[0], uv[3]);
|
|
264
536
|
n++;
|
|
265
537
|
}
|
|
266
|
-
// a textured quad with four free corners, each [x, y, u, v]
|
|
538
|
+
// a textured quad with four free corners, each [x, y, u, v, depth?]
|
|
267
539
|
function quadAt(tex, A, B, C_, D) {
|
|
268
540
|
if (tex !== curTex || curAdd || n >= QUADS) { flush(); curTex = tex; curAdd = false; }
|
|
269
541
|
let k = n * 4 * STRIDE;
|
|
270
542
|
for (const q of [A, B, C_, D]) {
|
|
271
|
-
data[k] = q[0]; data[k + 1] = q[1]; data[k + 2] = q[2]; data[k + 3] = q[3];
|
|
272
|
-
data[k + 4] = 1; data[k + 5] = 1; data[k + 6] = 1; data[k + 7] = 1; data[k + 8] = HZ[0]; data[k + 9] = HZ[1];
|
|
543
|
+
data[k] = q[0]; data[k + 1] = MIR ? mirrorY(q[1], q[4] ?? MD) : q[1]; data[k + 2] = q[2]; data[k + 3] = q[3];
|
|
544
|
+
data[k + 4] = 1; data[k + 5] = 1; data[k + 6] = 1; data[k + 7] = 1; data[k + 8] = HZ[0]; data[k + 9] = HZ[1]; data[k + 10] = MIR ? MH : LT[0]; data[k + 11] = MIR ? MP : q[4] ?? LT[1];
|
|
273
545
|
k += STRIDE;
|
|
274
546
|
}
|
|
275
547
|
n++;
|
|
276
548
|
}
|
|
549
|
+
// a convex polygon of points [x, y, u, v] (frame px, its texture's uv) as a fan, alpha a
|
|
550
|
+
function fan(tex, pts, a) {
|
|
551
|
+
if (tex !== curTex || curAdd || n + pts.length >= QUADS) { flush(); curTex = tex; curAdd = false; }
|
|
552
|
+
for (let i = 1; i + 1 < pts.length; i++) {
|
|
553
|
+
let k = n * 4 * STRIDE;
|
|
554
|
+
for (const q of [pts[0], pts[i], pts[i + 1], pts[i + 1]]) {
|
|
555
|
+
data[k] = q[0]; data[k + 1] = MIR ? mirrorY(q[1], MD) : q[1]; data[k + 2] = q[2]; data[k + 3] = q[3];
|
|
556
|
+
data[k + 4] = a; data[k + 5] = a; data[k + 6] = a; data[k + 7] = a; data[k + 8] = HZ[0]; data[k + 9] = HZ[1]; data[k + 10] = MIR ? MH : LT[0]; data[k + 11] = MIR ? MP : LT[1];
|
|
557
|
+
k += STRIDE;
|
|
558
|
+
}
|
|
559
|
+
n++;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// the part of a convex polygon where f (linear in x, y) is ≥ 0, its points' uv carried along
|
|
563
|
+
function keep(pts, f) {
|
|
564
|
+
const out = [];
|
|
565
|
+
for (let i = 0; i < pts.length; i++) {
|
|
566
|
+
const a = pts[i], b = pts[(i + 1) % pts.length], fa = f(a), fb = f(b);
|
|
567
|
+
if (fa >= 0) out.push(a);
|
|
568
|
+
if ((fa >= 0) !== (fb >= 0)) { const k = fa / (fa - fb); out.push(a.map((v, j) => v + (b[j] - v) * k)); }
|
|
569
|
+
}
|
|
570
|
+
return out;
|
|
571
|
+
}
|
|
572
|
+
// a textured rect (x, y, w, h: transformed by m, its uv) seen only above the line [[x, y], …]
|
|
573
|
+
// (frame px, x growing): what the land in front of it leaves seen
|
|
574
|
+
function rectAbove(tex, m, x, y, w, h, uv, a, line) {
|
|
575
|
+
const corners = [[x, y], [x + w, y], [x + w, y + h], [x, y + h]].map(([px, py]) => [...apply(m, px, py), uv[0] + (px - x) / w * (uv[2] - uv[0]), uv[1] + (py - y) / h * (uv[3] - uv[1])]);
|
|
576
|
+
for (let j = 0; j + 1 < line.length; j++) {
|
|
577
|
+
const [xa, ya] = line[j], [xb, yb] = line[j + 1], k = (yb - ya) / ((xb - xa) || 1);
|
|
578
|
+
const poly = keep(keep(keep(corners, p => p[0] - xa), p => xb - p[0]), p => ya + k * (p[0] - xa) - p[1]);
|
|
579
|
+
if (poly.length >= 3) fan(tex, poly, a);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
// the land in front (LAND, this frame's — engine.js landLine): for something drawn as the rect r
|
|
583
|
+
// through m, D away along the gaze, the line it is seen above — null: all of it seen, false: none
|
|
584
|
+
let LAND = null;
|
|
585
|
+
function landOver(m, r, D) {
|
|
586
|
+
if (!LAND) return null;
|
|
587
|
+
const xs = [], ys = [];
|
|
588
|
+
for (const [px, py] of [[r.x, r.y], [r.x + r.w, r.y], [r.x + r.w, r.y + r.h], [r.x, r.y + r.h]]) { const q = apply(m, px, py); xs.push(q[0]); ys.push(q[1]); }
|
|
589
|
+
const line = LAND.at(D, Math.min(...xs), Math.max(...xs), Math.max(...ys));
|
|
590
|
+
if (!line) return null;
|
|
591
|
+
return line.every(([, y]) => y <= Math.min(...ys)) ? false : line;
|
|
592
|
+
}
|
|
593
|
+
// is the point (x, y) (frame px) under the line?
|
|
594
|
+
const under = (line, x, y) => {
|
|
595
|
+
const j = line.findIndex(([lx]) => lx >= x);
|
|
596
|
+
if (j <= 0) return j === 0 ? y > line[0][1] : y > line.at(-1)[1];
|
|
597
|
+
const [xa, ya] = line[j - 1], [xb, yb] = line[j];
|
|
598
|
+
return y > ya + (yb - ya) * (x - xa) / ((xb - xa) || 1);
|
|
599
|
+
};
|
|
277
600
|
function sprite(tex, m, cx, cy, w, h, rot, c, a, add) {
|
|
278
601
|
rect(tex, rot ? mul(m, around(cx, cy, rot, 1, 1)) : m, cx - w / 2, cy - h / 2, w, h, c, a, add);
|
|
279
602
|
}
|
|
@@ -398,7 +721,10 @@ const MooodGL = (() => {
|
|
|
398
721
|
function build(spec, w, h, prev, out) {
|
|
399
722
|
if (prev) {
|
|
400
723
|
for (const t of prev.textures) gl.deleteTexture(t);
|
|
401
|
-
dropTarget(prev.
|
|
724
|
+
dropTarget(prev.plain); dropTarget(prev.copy);
|
|
725
|
+
if (prev.litScene) dropTarget(prev.litScene);
|
|
726
|
+
if (prev.mask) dropTarget(prev.mask);
|
|
727
|
+
for (const ob of prev.objects) if (ob.mirrorT) dropTarget(ob.mirrorT);
|
|
402
728
|
for (const l of prev.layers) if (l.tiles) for (const t of l.tiles.values()) freeTile(t);
|
|
403
729
|
}
|
|
404
730
|
W = w; H = h; SW = I.STAGE * h; ROUND = I.camSetup(spec).round;
|
|
@@ -407,8 +733,7 @@ const MooodGL = (() => {
|
|
|
407
733
|
const u = H / 900, seed = I.hash(JSON.stringify(spec.layers || []) + (spec.title || '')), rnd = I.seeded(seed);
|
|
408
734
|
const x0 = -I.EXT * SW, span = (1 + 2 * I.EXT) * SW, dz = I.depths(spec);
|
|
409
735
|
YH = dz.yh;
|
|
410
|
-
|
|
411
|
-
HAZE = typeof spec.light?.haze === 'number' ? 600 * Math.pow(25 / 600, I.clamp(spec.light.haze, 0, 1)) : 220;
|
|
736
|
+
HAZE0 = typeof spec.light?.haze === 'number' ? I.hazeReach(spec.light.haze) : 220;
|
|
412
737
|
|
|
413
738
|
const sky = spec.sky ? tex(paint(4, 256, (g) => { const r = g.createLinearGradient(0, 0, 0, 256); r.addColorStop(0, spec.sky.top); r.addColorStop(0.55, spec.sky.middle); r.addColorStop(1, spec.sky.bottom); g.fillStyle = r; g.fillRect(0, 0, 4, 256); })) : null;
|
|
414
739
|
const starCount = Math.round((spec.stars?.density ?? 0) * span * H / 2500 / (u * u));
|
|
@@ -450,7 +775,8 @@ const MooodGL = (() => {
|
|
|
450
775
|
const lseed = seed + i * 101, st = I.staticLayer(L, SW, H, lseed);
|
|
451
776
|
return { L, Z, seed: lseed, tex: tex(st.canvas), x0: st.x0, region: { x: st.x0, y: 0, w: span, h: I.DEEP * H }, tops: st.tops, solids: st.solids, stripW: st.stripW, windows: st.windows, tiles: new Map() };
|
|
452
777
|
});
|
|
453
|
-
const
|
|
778
|
+
const parted = []; // [an element painting parts, the parts it painted]
|
|
779
|
+
const objects = (spec.objects || []).filter(o0 => !o0.insert).map(o0 => {
|
|
454
780
|
if (o0.plane) return planeOf(o0, dz, tex);
|
|
455
781
|
// looking around, the sky is the full circle, 2π stage heights wide (engine.js)
|
|
456
782
|
const n = dz.now(o0), o = { ...n.o, _sw: ROUND && o0.sky ? I.TAU * H : SW }, Z = n.Z;
|
|
@@ -468,30 +794,163 @@ const MooodGL = (() => {
|
|
|
468
794
|
// a custom element whose code reads the time is repainted live every frame (one reading the
|
|
469
795
|
// scene's state, when the state changes)
|
|
470
796
|
const { bx, by, bw, bh } = ROUND && o.sky ? { bx: 0, by: -0.35 * H, bw: I.TAU * H, bh: 1.6 * H } : I.objectBox(o, SW, H);
|
|
471
|
-
const
|
|
797
|
+
const whole = { x: bx, y: by, w: bw, h: bh };
|
|
798
|
+
const r0 = paintObject(o, bx, by, bw, bh, Math.min(1, MAX_SIDE / bw, MAX_SIDE / bh), 0, null, whole), frames = [tex(r0.canvas)], lights = r0.lights;
|
|
472
799
|
const live = (o.kind === 'custom' || !o.kind) ? liveOf(o, o.frames, tex) : null;
|
|
473
|
-
|
|
474
|
-
o, Z, x, y, s, bx, by, bw, bh, tex: frames[0], frames, period: o.period || 2, detail: null,
|
|
475
|
-
p: ROUND && !o.sky ? n.at : null,
|
|
476
|
-
live, ground: !!o.ground, from: o0.place ? o0 : null,
|
|
800
|
+
const ob = {
|
|
801
|
+
o, Z, x, y, s, bx, by, bw, bh, tex: frames[0], frames, pad: [(r0.region.w - bw) / 2 / bw, (r0.region.h - bh) / 2 / bh], period: o.period || 2, detail: null, cut: r0.cut,
|
|
802
|
+
p: ROUND && !o.sky ? n.at : null, at: n.at,
|
|
803
|
+
live, ground: !!o.ground, from: o0.place ? o0 : null, spec: o0, unseen: !!o0.unseen, parts: null,
|
|
477
804
|
reflect: o.reflect ?? (o.kind === 'boat' || o.kind === 'bridge'),
|
|
478
|
-
lights: lights.map(l => ({ x: l.x, y: l.y, r: l.r, c: col(l.col), a: l.a, dot: l.dot, fire: o.kind === 'campfire', flicker: l.flicker })),
|
|
805
|
+
lights: lights.map(l => ({ x: l.x, y: l.y, r: l.r, c: col(l.col), a: l.a, dot: l.dot, fire: o.kind === 'campfire', flicker: l.flicker, id: l.id })),
|
|
479
806
|
};
|
|
807
|
+
if (r0.parts.length) { ob.parts = []; parted.push([ob, r0.parts]); }
|
|
808
|
+
return ob;
|
|
809
|
+
});
|
|
810
|
+
// inserts (engine.js): over all of it, fixed to the frame — repainted while open as the state
|
|
811
|
+
// changes (the iris their key opens), and every frame if their painting reads the time
|
|
812
|
+
const inserts = (spec.objects || []).filter(o0 => o0.insert).map(o0 => {
|
|
813
|
+
const o = { ...o0, _yh: dz.yh };
|
|
814
|
+
return { o, ...I.objectBox(o, SW, H), x: 0, y: 0, frames: [], period: o.period || 2, live: { canvas: I.layer(1, 1), tex: tex(I.layer(1, 1)), stateOnly: true, frames: 0, lights: [] } };
|
|
480
815
|
});
|
|
481
816
|
// particles fall over all that is seen: in the canvas's pixels
|
|
482
817
|
const particles = (spec.particles || []).map(p => ({ p, c: col(p.color || '#ffffff'), list: I.makeParticles(p, CW, CH, rnd) }));
|
|
483
818
|
// draw far to near, layers and objects together (a house behind a hill is hidden by it)
|
|
484
|
-
|
|
485
|
-
|
|
819
|
+
// cut by others (clip): a mask target to paint them in
|
|
820
|
+
const ids = new Set(objects.map(ob => ob.o.id).filter(id => id != null));
|
|
821
|
+
for (const ob of objects) ob.clip = I.clipOf(ob.spec, ids);
|
|
822
|
+
const S = {
|
|
823
|
+
W, H, SW, u, x0, span, yh: dz.yh, dz, lim: I.limits(spec, W, H), sky, stars, celestials, layers, objects, inserts, particles, textures,
|
|
824
|
+
shadows: shadowsOf(spec, objects), lands: objects.filter(ob => ob.plane?.drape), plain: target(CW, CH), litScene: null, copy: target(CW, CH),
|
|
825
|
+
mask: objects.some(ob => ob.clip) ? target(CW, CH) : null, out, rules: I.orderRules(spec),
|
|
826
|
+
};
|
|
827
|
+
for (const [ob, recs] of parted) paintParts(S, ob, recs, null);
|
|
828
|
+
S.order = orderOf(S);
|
|
829
|
+
// the planes that reflect (a browser that can't write depths draws no mirror)
|
|
830
|
+
S.mirrors = canLight ? objects.filter(ob => ob.plane?.mirror && !ob.unseen).map(ob => ob.plane) : [];
|
|
831
|
+
return S;
|
|
832
|
+
}
|
|
833
|
+
// draw far to near, layers, objects and their parts together (a house behind a hill is hidden by
|
|
834
|
+
// it) — as the elements say (behind, inFront)
|
|
835
|
+
const idOf = it => (it.ob ? it.ob.o.id : it.part ? it.part.owner.o.id : null) ?? null;
|
|
836
|
+
function orderOf(S) {
|
|
837
|
+
const list = [...S.layers.map(l => ({ l, Z: l.Z })), ...S.objects.map(ob => ({ ob, Z: ob.Z }))];
|
|
838
|
+
for (const ob of S.objects) for (const part of ob.parts || []) list.push({ part, Z: part.Z });
|
|
839
|
+
return I.arrange(list.sort((a, b) => (b.Z - a.Z) || 0), S.rules, idOf);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ── parts ────────────────────────────────────────────────────────────
|
|
843
|
+
// An element painting parts (e.part: a fence's posts, the people of a crowd) — each part stands
|
|
844
|
+
// at its own depth, in a texture of its own, drawn among the rest where it stands. A part: { owner,
|
|
845
|
+
// rec: {dx, dz, box, paint}, o: where it is now (depths().part), Z, x, y, s, its box on the stage,
|
|
846
|
+
// tex, lights: its glows as fractions of its box }. They are painted when their element is
|
|
847
|
+
// (repaint), as sharp as each is seen from cam (as from the start without one).
|
|
848
|
+
function placePart(S, part) {
|
|
849
|
+
const n = S.dz.part(part.owner.spec, part.rec.dx, part.rec.dz, part.rec.box), o = n.o;
|
|
850
|
+
Object.assign(part, { o, Z: n.Z, x: o.x * SW, y: o.y * H, s: Math.max(2, o.size * H), p: ROUND ? n.at : null, at: n.at }, I.objectBox(o, SW, H));
|
|
851
|
+
}
|
|
852
|
+
function paintParts(S, ob, recs, cam) {
|
|
853
|
+
const list = ob.parts;
|
|
854
|
+
for (const gone of list.splice(recs.length)) { gl.deleteTexture(gone.tex); S.textures.delete(gone.tex); }
|
|
855
|
+
recs.forEach((rec, i) => {
|
|
856
|
+
const part = list[i] ||= { owner: ob, canvas: I.layer(1, 1), tex: null, lights: [] };
|
|
857
|
+
part.rec = rec;
|
|
858
|
+
placePart(S, part);
|
|
859
|
+
const r = { x: part.bx, y: part.by, w: part.bw, h: part.bh }, seen = cam && matOf(part, cam);
|
|
860
|
+
const k = liveScale(r, seen ? Math.max(LIVE_AWAY, seen.m[0]) : 1), c = part.canvas, w = Math.max(1, Math.round(r.w * k)), h = Math.max(1, Math.round(r.h * k));
|
|
861
|
+
if (c.width !== w || c.height !== h) { c.width = w; c.height = h; }
|
|
862
|
+
const g = c.getContext('2d'), lights = [], [ax, ay, ppm] = I.anchorOf(part.o, SW, H);
|
|
863
|
+
g.setTransform(1, 0, 0, 1, 0, 0); g.clearRect(0, 0, w, h);
|
|
864
|
+
g.setTransform(k, 0, 0, k, -r.x * k, -r.y * k);
|
|
865
|
+
rec.paint(g, ax, ay, ppm, lights);
|
|
866
|
+
if (part.tex) { flush(); gl.bindTexture(gl.TEXTURE_2D, part.tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c); curTex = null; }
|
|
867
|
+
else { part.tex = texture(c); S.textures.add(part.tex); }
|
|
868
|
+
part.lights = lights.map(l => ({ u: (l.x - r.x) / r.w, v: (l.y - r.y) / r.h, r: l.r / r.w, c: col(l.col), a: l.a, flicker: l.flicker, id: l.id }));
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
// as each frame starts: the elements with parts painted if they are due (their parts with them),
|
|
872
|
+
// and their parts put where they are now
|
|
873
|
+
function prepareParts(S, cam, t) {
|
|
874
|
+
for (const ob of S.objects) {
|
|
875
|
+
if (!ob.parts) continue;
|
|
876
|
+
if (ob.live) {
|
|
877
|
+
for (const id of Array.isArray(ob.o.after) ? ob.o.after : []) { const other = BYID.get(id), oat = other?.live && matOf(other, cam); if (oat) textureOf(other, oat.m, t); }
|
|
878
|
+
// (itself out of sight — behind the eye — coarsely; its parts as they are seen: paintParts)
|
|
879
|
+
const at = matOf(ob, cam);
|
|
880
|
+
if (at) textureOf(ob, at.m, t); else unseen(ob, t);
|
|
881
|
+
}
|
|
882
|
+
for (const part of ob.parts) placePart(S, part);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
function drawPartAt(part, cam, t) {
|
|
886
|
+
const at = matOf(part, cam);
|
|
887
|
+
if (!at) return;
|
|
888
|
+
MD = at.D;
|
|
889
|
+
const fade = I.nearFade(at.D), r = { x: part.bx, y: part.by, w: part.bw, h: part.bh };
|
|
890
|
+
const line = fade > 0 ? landOver(at.m, r, at.D) : false;
|
|
891
|
+
if (line === false) return;
|
|
892
|
+
part.shown = { tx: part.tex, r, m: at.m, frame: frameNo };
|
|
893
|
+
LT = part.owner.o.glows ? UNLIT : [1, at.D];
|
|
894
|
+
if (line) rectAbove(part.tex, at.m, r.x, r.y, r.w, r.h, UV, fade, line);
|
|
895
|
+
else rect(part.tex, at.m, r.x, r.y, r.w, r.h, WHITE, fade);
|
|
896
|
+
LT = UNLIT;
|
|
897
|
+
for (const lt of part.lights) {
|
|
898
|
+
const lx = r.x + lt.u * r.w, ly = r.y + lt.v * r.h;
|
|
899
|
+
if (line && under(line, ...apply(at.m, lx, ly))) continue;
|
|
900
|
+
const fl = lt.flicker != null ? 1 - lt.flicker * 0.5 * (0.5 + 0.5 * Math.sin(t * 9 + lx) * Math.sin(t * 5.3 + ly)) : 1;
|
|
901
|
+
sprite(T.glow, at.m, lx, ly, 2 * lt.r * r.w, 2 * lt.r * r.w, 0, lt.c, lt.a * 0.6 * fl * fade, true);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// ── cut by others ────────────────────────────────────────────────────
|
|
906
|
+
// An item of the order drawn — an element or a part of one — unless it is unseen (only a shape
|
|
907
|
+
// others are cut by); one with `clip` through the mask of what those it names paint now (list:
|
|
908
|
+
// the frame's order, where they are)
|
|
909
|
+
function drawItem(S, it, cam, t, list) {
|
|
910
|
+
const ob = it.ob || it.part.owner;
|
|
911
|
+
if (ob.unseen) return;
|
|
912
|
+
// (in a mirror: what is above its plane — not what is cut by others, the mask is the frame's)
|
|
913
|
+
if (MIR && (ob.clip || ob.ground || ob === MIR.ob || (ob.plane && ob.plane.Y <= MIR.Y + 0.01))) return;
|
|
914
|
+
if (ob.clip) {
|
|
915
|
+
maskOf(S, ob.clip.ids, cam, t, list);
|
|
916
|
+
CLIP = { tex: S.mask.tex, inside: ob.clip.inside };
|
|
917
|
+
}
|
|
918
|
+
if (it.part) drawPartAt(it.part, cam, t); else drawObjectAt(it.ob, cam, t);
|
|
919
|
+
if (ob.clip) { flush(); CLIP = null; }
|
|
920
|
+
}
|
|
921
|
+
// the elements `ids` (and their parts) painted in the mask target, as they are now (once a frame
|
|
922
|
+
// for all cut by the same ones)
|
|
923
|
+
function maskOf(S, ids, cam, t, list) {
|
|
924
|
+
const key = `${frameNo}${MIR ? ' mirrored' : ''}\n${ids.join('\n')}`;
|
|
925
|
+
if (S.maskKey === key) return;
|
|
926
|
+
S.maskKey = key;
|
|
927
|
+
flush();
|
|
928
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, S.mask.fbo);
|
|
929
|
+
gl.clearColor(0, 0, 0, 0);
|
|
930
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
931
|
+
for (const it of list) {
|
|
932
|
+
if (!ids.includes(idOf(it)) || it.l) continue;
|
|
933
|
+
if (it.part) drawPartAt(it.part, cam, t); else drawObjectAt(it.ob, cam, t);
|
|
934
|
+
}
|
|
935
|
+
flush();
|
|
936
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, S.scene.fbo);
|
|
486
937
|
}
|
|
487
938
|
|
|
488
939
|
// Paints an object at t = 0 — the region (bx, by, bw, bh) of it — into a canvas at scale
|
|
489
940
|
// k. Light rects and cones are baked in; glows stay sprites so they can flicker.
|
|
490
|
-
|
|
491
|
-
|
|
941
|
+
// (and the parts it paints, e.part: each to be painted on its own — paintParts)
|
|
942
|
+
// With `whole` (its box) while the scene is checked (cutOf): painted with a margin, looked at
|
|
943
|
+
// there and the margin cleared. → { canvas, lights, parts, region: what the canvas holds, cut }
|
|
944
|
+
function paintObject(o, bx, by, bw, bh, k, t = 0, into = null, whole = null) {
|
|
945
|
+
// (the margin within the largest texture there may be)
|
|
946
|
+
if (whole && checked(o)) k = Math.min(k, (MAX_SIDE - 2 * CUT_MARGIN) / bw, (MAX_SIDE - 2 * CUT_MARGIN) / bh);
|
|
947
|
+
const m = whole && checked(o) ? CUT_MARGIN / k : 0;
|
|
948
|
+
if (m) { bx -= m; by -= m; bw += 2 * m; bh += 2 * m; }
|
|
949
|
+
const lights = [], parts = [];
|
|
492
950
|
const draw = (g) => {
|
|
493
951
|
g.setTransform(k, 0, 0, k, -bx * k, -by * k);
|
|
494
|
-
I.drawObject(g, o, o._sw || SW, H, t, lights);
|
|
952
|
+
I.drawObject(g, o, o._sw || SW, H, t, lights, parts);
|
|
953
|
+
if (o.insert) I.iris(g, o, SW, H, I.opening(o), lights);
|
|
495
954
|
for (const l of lights) {
|
|
496
955
|
if (l.rect) { g.fillStyle = `rgba(${l.col},${Math.min(1, l.a * 2.2)})`; g.fillRect(...l.rect); }
|
|
497
956
|
if (l.cone) {
|
|
@@ -512,7 +971,156 @@ const MooodGL = (() => {
|
|
|
512
971
|
g.clearRect(0, 0, w, h);
|
|
513
972
|
draw(g);
|
|
514
973
|
} else canvas = paint(bw * k, bh * k, draw);
|
|
515
|
-
|
|
974
|
+
const region = { x: bx, y: by, w: bw, h: bh };
|
|
975
|
+
return { canvas, lights, parts, region, cut: m ? cutOf(canvas, region, whole, m) : null };
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// ── cut off by its box ───────────────────────────────────────────────
|
|
979
|
+
// An element is painted within its box: paint going on past the box's edge is cut there, a
|
|
980
|
+
// straight line wherever that edge comes into the frame. While the scene is checked (a render,
|
|
981
|
+
// ?debug: engine.js CHECK — reading pixels back is not free) a custom element is painted with a
|
|
982
|
+
// margin of CUT_MARGIN px round its box (a live one at most once a second): what it paints in the
|
|
983
|
+
// margin's outer pixel runs on past the box (a painting that ends at its edge does not), and is
|
|
984
|
+
// said once for each edge as it shows (cutShown), so the artist widens its "box"; the margin is
|
|
985
|
+
// cleared — the picture is as unchecked. [left, top, right]: not the bottom — an element's foot is
|
|
986
|
+
// mostly hidden by the ground and what stands nearer: said, it would mostly be noise.
|
|
987
|
+
const EDGES = ['left', 'top', 'right'], CUT_MARGIN = 2;
|
|
988
|
+
const checked = o => I.CHECK.on && (o.kind === 'custom' || !o.kind) && !o.sky && !o.ground && !o.plane && !o.insert;
|
|
989
|
+
// the canvas painted over region r with a margin m (r's units) round the part of it that is to be
|
|
990
|
+
// seen: the edges of that part that are its box's (whole) and are painted past → [left, top,
|
|
991
|
+
// right] or null; the margin cleared
|
|
992
|
+
function cutOf(canvas, r, whole, m) {
|
|
993
|
+
const g = canvas.getContext('2d'), w = canvas.width, h = canvas.height, M = Math.round(m * w / r.w);
|
|
994
|
+
const hit = (x, y, sw, sh) => { const d = g.getImageData(x, y, sw, sh).data; let n = 0; for (let i = 3; i < d.length; i += 4) if (d[i] > 40) n++; return n >= 3; };
|
|
995
|
+
const at = [Math.abs(r.x + m - whole.x) < 1, Math.abs(r.y + m - whole.y) < 1, Math.abs(r.x + r.w - m - whole.x - whole.w) < 1];
|
|
996
|
+
const cut = w > 2 * M + 8 && h > 2 * M + 8 ? [at[0] && hit(0, M, 1, h - 2 * M), at[1] && hit(M, 0, w - 2 * M, 1), at[2] && hit(w - 1, M, 1, h - 2 * M)] : [];
|
|
997
|
+
g.setTransform(1, 0, 0, 1, 0, 0);
|
|
998
|
+
g.clearRect(0, 0, M, h); g.clearRect(w - M, 0, M, h); g.clearRect(0, 0, w, M); g.clearRect(0, h - M, w, M);
|
|
999
|
+
return cut.some(Boolean) ? cut : null;
|
|
1000
|
+
}
|
|
1001
|
+
// the edges it is cut at that are in the frame now (m: its screen transform)
|
|
1002
|
+
function cutShown(ob, m) {
|
|
1003
|
+
const x0 = m[0] * ob.bx + m[4], x1 = x0 + m[0] * ob.bw, y0 = m[3] * ob.by + m[5], y1 = y0 + m[3] * ob.bh;
|
|
1004
|
+
const across = x1 > 0 && x0 < W, down = y1 > 0 && y0 < H;
|
|
1005
|
+
const seen = [down && x0 > 0 && x0 < W, across && y0 > 0 && y0 < H, down && x1 > 0 && x1 < W];
|
|
1006
|
+
ob.cut.forEach((c, i) => { if (c && seen[i]) I.warn(`${ob.o.name || ob.o.id}: its painting is cut off by its box at the ${EDGES[i]} edge, in the frame — widen its "box"`); });
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// ── shadows ──────────────────────────────────────────────────────────
|
|
1010
|
+
// A shadow (engine.js: shadowsOf) on the ground is drawn just before the element that casts it;
|
|
1011
|
+
// on a plane or an upright element, right after that one, only where it is painted (its alpha).
|
|
1012
|
+
// The caster's painting is laid on the receiver's plane as a grid of cells, each corner where the
|
|
1013
|
+
// ray from the light through it meets that plane (cells straddling another receiver met first are
|
|
1014
|
+
// split, then left to it), blurred as much as the light's size (`soft`) spreads its edge there:
|
|
1015
|
+
// sharp at the contact, softer away from it.
|
|
1016
|
+
const SH_GRID = [12, 20], SH_DEPTH = 3;
|
|
1017
|
+
// the scene's shadows (engine.js: shadowsOf) with their elements: { ob, onto: ['ground' | ob], … }
|
|
1018
|
+
function shadowsOf(spec, objects) {
|
|
1019
|
+
const byId = new Map(objects.filter(x => x.o.id != null).map(x => [String(x.o.id), x]));
|
|
1020
|
+
return I.shadowsOf(spec).map(sh => ({ ...sh, ob: byId.get(sh.id), onto: sh.onto.map(id => (id === 'ground' ? id : byId.get(id))) }));
|
|
1021
|
+
}
|
|
1022
|
+
// what a shadow falls on, as a plane of the world: a point q on it, its normal n, its texture
|
|
1023
|
+
// (what it is painted with now: the shadow shows only where that is) and where a world point is
|
|
1024
|
+
// on that texture — or null (a card that turns to face the eye)
|
|
1025
|
+
// (the ground: level where the caster stands, A — over the land, its height there; what isn't
|
|
1026
|
+
// drawn this frame takes none)
|
|
1027
|
+
function receiver(R, A) {
|
|
1028
|
+
if (R === 'ground') return { q: [0, I.groundAt(A.X * I.EYE, A.Z * I.EYE) / I.EYE, 0], n: [0, 1, 0], tex: T.white, uv: () => [0.5, 0.5] };
|
|
1029
|
+
if (!R.shown || R.shown.frame !== frameNo) return null;
|
|
1030
|
+
const { tx, r } = R.shown;
|
|
1031
|
+
if (R.plane) {
|
|
1032
|
+
const P = R.plane;
|
|
1033
|
+
return { q: [P.Xc, P.Y, P.Zc], n: [0, 1, 0], tex: tx, uv: S => { const dx = S[0] - P.Xc, dz = S[2] - P.Zc; return [(dx * P.cos + dz * P.sin + P.w / 2) / P.w, (dx * P.sin - dz * P.cos + P.d / 2) / P.d]; } };
|
|
1034
|
+
}
|
|
1035
|
+
const p = ROUND ? R.p : R.at;
|
|
1036
|
+
if (!p || (ROUND && typeof R.o.facing !== 'number')) return null;
|
|
1037
|
+
const k = H / p.Zp, f = ROUND ? R.o.facing * Math.PI / 180 : Math.PI, ux = -Math.cos(f), uz = Math.sin(f);
|
|
1038
|
+
return { q: [p.X, p.Y, p.Z], n: [uz, 0, -ux], tex: tx, uv: S => [(R.x + ((S[0] - p.X) * ux + (S[2] - p.Z) * uz) * k - r.x) / r.w, (R.y - (S[1] - p.Y) * k - r.y) / r.h] };
|
|
1039
|
+
}
|
|
1040
|
+
// what casts a shadow: an element, or a part of one (e.part) — { tx: its painting this frame, r:
|
|
1041
|
+
// the region of it that is, at: where it stands (depths().now), x, y: its anchor on the stage }
|
|
1042
|
+
function caster(it, cam, t) {
|
|
1043
|
+
if (it.part) { const p = it.part; return { tx: p.tex, r: { x: p.bx, y: p.by, w: p.bw, h: p.bh }, at: p.at, x: p.x, y: p.y }; }
|
|
1044
|
+
const ob = it.ob, at = matOf(ob, cam);
|
|
1045
|
+
if (!at) return null;
|
|
1046
|
+
const { tx, r } = textureOf(ob, at.m, t);
|
|
1047
|
+
return { tx, r, at: ob.at, x: ob.x, y: ob.y };
|
|
1048
|
+
}
|
|
1049
|
+
// the shadows falling on R now: on 'ground', the item's (an element or a part), before it is
|
|
1050
|
+
// drawn; on an element R, all that are cast on it — of the elements and their parts
|
|
1051
|
+
function castOn(S, R, it, cam, t) {
|
|
1052
|
+
if (MIR) return;
|
|
1053
|
+
const owner = it && (it.ob || it.part.owner);
|
|
1054
|
+
for (const sh of S.shadows) {
|
|
1055
|
+
if (!sh.onto.includes(R)) continue;
|
|
1056
|
+
// (each caster's on each receiver: its grid kept while nothing it depends on moves — slot)
|
|
1057
|
+
if (R === 'ground') { if (sh.ob === owner) drawShadow(S, sh, R, caster(it, cam, t), cam, `ground ${it.part ? owner.parts.indexOf(it.part) : -1}`); continue; }
|
|
1058
|
+
[{ ob: sh.ob }, ...(sh.ob.parts || []).map(part => ({ part }))].forEach((c, i) => drawShadow(S, sh, R, caster(c, cam, t), cam, `${R.o.id} ${i - 1}`));
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
function drawShadow(S, sh, R, cs, cam, slot) {
|
|
1062
|
+
const src = cs && I.shadowLight(S.sources, sh, cs.at), pl = src && receiver(R, cs.at);
|
|
1063
|
+
if (!pl) return;
|
|
1064
|
+
const others = sh.onto.filter(x => x !== R).map(x => receiver(x, cs.at)).filter(Boolean);
|
|
1065
|
+
// the caster's painting this frame (painted first, if it is to be), and its points in the world
|
|
1066
|
+
const { tx, r } = cs, A = cs.at, k = A.Zp / H;
|
|
1067
|
+
const ax = ROUND ? Math.cos(cam.yaw || 0) : 1, az = ROUND ? -Math.sin(cam.yaw || 0) : 0;
|
|
1068
|
+
const world = (f, g) => { const u = (r.x + r.w * f - cs.x) * k; return [A.X + u * ax, A.Y - (r.y + r.h * g - cs.y) * k, A.Z + u * az]; };
|
|
1069
|
+
flush();
|
|
1070
|
+
gl.useProgram(sprog);
|
|
1071
|
+
gl.bindVertexArray(svao);
|
|
1072
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, svbo);
|
|
1073
|
+
gl.uniform2f(shu.canvas, CW, CH);
|
|
1074
|
+
gl.uniform2f(shu.off, OX, OY);
|
|
1075
|
+
gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, pl.tex); gl.uniform1i(shu.mask, 1);
|
|
1076
|
+
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, tx); gl.uniform1i(shu.tex, 0);
|
|
1077
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
1078
|
+
if (S.lit) gl.drawBuffers(COLOUR_ONLY);
|
|
1079
|
+
// the grid laid as it was, while the caster, its light, what it falls on and the camera are where
|
|
1080
|
+
// they were (a still frame: none of it made again)
|
|
1081
|
+
const key = [!!MIR, W, H, A.X, A.Y, A.Z, r.x, r.y, r.w, r.h, ...(src.at || []), ...(src.dir || []), src.strength, sh.strength, S.ambient, S.lit,
|
|
1082
|
+
cam.x, cam.y, cam.z, cam.h, cam.yaw, ...pl.q, R.shown ? Object.values(R.shown.r).join() : '', ...others.flatMap(q => q.q), VX0, VY0, VX1, VY1].join();
|
|
1083
|
+
const grids = sh.grids ||= new Map();
|
|
1084
|
+
let verts = grids.get(slot)?.key === key ? grids.get(slot).verts : null;
|
|
1085
|
+
if (!verts) {
|
|
1086
|
+
const wk = r.w * k, hk = r.h * k, out = [];
|
|
1087
|
+
// a corner: [screen x, y, its u v on the caster, on the receiver, how dark, how blurred (u, v)] —
|
|
1088
|
+
// or null where its ray meets another receiver first, or none. The penumbra of a light `soft`
|
|
1089
|
+
// across: soft·(t − 1) on the receiver from a point (t: how much farther than the caster it is),
|
|
1090
|
+
// soft·t from far off — on the caster's painting that over the magnification
|
|
1091
|
+
const corner = (f, g) => {
|
|
1092
|
+
const C = world(f, g), h = I.rayHit(pl, C, src.at, src.dir);
|
|
1093
|
+
if (!h || others.some(q => { const o2 = I.rayHit(q, C, src.at, src.dir); return o2 && o2[1] < h[1]; })) return null;
|
|
1094
|
+
const p = I.project(cam, ...h[0], W, H, YH);
|
|
1095
|
+
if (!p) return null;
|
|
1096
|
+
const b = src.dir ? sh.soft * h[1] : sh.soft * (h[1] - 1) / h[1];
|
|
1097
|
+
return [p[0], p[1], f, g, ...pl.uv(h[0]), I.shadowAt(sh, src, h[0], S.ambient, S.lit), b / wk, b / hk];
|
|
1098
|
+
};
|
|
1099
|
+
const cell = (f0, g0, f1, g1, depth) => {
|
|
1100
|
+
const c = [corner(f0, g0), corner(f1, g0), corner(f1, g1), corner(f0, g1)], ok = c.filter(Boolean).length;
|
|
1101
|
+
if (ok === 0) return;
|
|
1102
|
+
if (ok < 4) {
|
|
1103
|
+
if (depth >= SH_DEPTH) return;
|
|
1104
|
+
const fm = (f0 + f1) / 2, gm = (g0 + g1) / 2;
|
|
1105
|
+
cell(f0, g0, fm, gm, depth + 1); cell(fm, g0, f1, gm, depth + 1); cell(f0, gm, fm, g1, depth + 1); cell(fm, gm, f1, g1, depth + 1);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (Math.max(...c.map(q => q[0])) < VX0 || Math.min(...c.map(q => q[0])) > VX1 || Math.max(...c.map(q => q[1])) < VY0 || Math.min(...c.map(q => q[1])) > VY1) return;
|
|
1109
|
+
out.push(c[0], c[1], c[2], c[0], c[2], c[3]);
|
|
1110
|
+
};
|
|
1111
|
+
const [GC, GR] = SH_GRID;
|
|
1112
|
+
for (let gi = 0; gi < GR; gi++) for (let fi = 0; fi < GC; fi++) cell(fi / GC, gi / GR, (fi + 1) / GC, (gi + 1) / GR, 0);
|
|
1113
|
+
verts = new Float32Array(out.length * SH_STRIDE);
|
|
1114
|
+
out.forEach((q, i) => verts.set(q, i * SH_STRIDE));
|
|
1115
|
+
grids.set(slot, { key, verts });
|
|
1116
|
+
}
|
|
1117
|
+
for (let i = 0; i < verts.length; i += SH_MAX * SH_STRIDE) {
|
|
1118
|
+
const n = Math.min(SH_MAX * SH_STRIDE, verts.length - i);
|
|
1119
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, verts, i, n);
|
|
1120
|
+
gl.drawArrays(gl.TRIANGLES, 0, n / SH_STRIDE);
|
|
1121
|
+
}
|
|
1122
|
+
if (S.lit) gl.drawBuffers(ALL_BUFFERS);
|
|
1123
|
+
curTex = null;
|
|
516
1124
|
}
|
|
517
1125
|
|
|
518
1126
|
// ── sharp close-ups ──────────────────────────────────────────────────
|
|
@@ -549,9 +1157,9 @@ const MooodGL = (() => {
|
|
|
549
1157
|
// objects: re-painted at the needed scale, only the part on screen (a tall tower
|
|
550
1158
|
// close up would otherwise be a huge canvas)
|
|
551
1159
|
for (const ob of [...S.objects].reverse()) {
|
|
552
|
-
if (ob.o.kind === 'bird' || ob.live || ob.ground || ob.plane || ob.o.sky || (ROUND && typeof ob.o.facing === 'number')) continue;
|
|
1160
|
+
if (ob.o.kind === 'bird' || ob.live || ob.parts || ob.ground || ob.plane || ob.o.sky || (ROUND && typeof ob.o.facing === 'number')) continue;
|
|
553
1161
|
const at = matOf(ob, c);
|
|
554
|
-
if (!at || at.D < 0.
|
|
1162
|
+
if (!at || at.D < 0.05) continue;
|
|
555
1163
|
const k = Math.pow(LEVEL, levelFor(at.m[0]));
|
|
556
1164
|
if (k <= 1) { if (ob.detail && c === cam) setDetail(S, ob, null); continue; }
|
|
557
1165
|
const v = visible(at.m), box = { x: ob.bx, y: ob.by, w: ob.bw, h: ob.bh };
|
|
@@ -581,6 +1189,7 @@ const MooodGL = (() => {
|
|
|
581
1189
|
}
|
|
582
1190
|
|
|
583
1191
|
function drawWater(S, l, wy, t) {
|
|
1192
|
+
if (MIR) return;
|
|
584
1193
|
flush();
|
|
585
1194
|
if (wy >= VY1) return;
|
|
586
1195
|
// copy what is drawn so far, then shade the water below the horizon from that copy
|
|
@@ -588,39 +1197,44 @@ const MooodGL = (() => {
|
|
|
588
1197
|
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, S.copy.fbo);
|
|
589
1198
|
gl.blitFramebuffer(0, 0, CW, CH, 0, 0, CW, CH, gl.COLOR_BUFFER_BIT, gl.NEAREST);
|
|
590
1199
|
gl.bindFramebuffer(gl.FRAMEBUFFER, S.scene.fbo);
|
|
591
|
-
gl.useProgram(wprog);
|
|
1200
|
+
gl.useProgram(P.wprog);
|
|
592
1201
|
gl.activeTexture(gl.TEXTURE0);
|
|
593
1202
|
gl.bindTexture(gl.TEXTURE_2D, S.copy.tex);
|
|
594
|
-
gl.uniform1i(wu.tex, 0);
|
|
595
|
-
gl.uniform2f(wu.res, W, H);
|
|
596
|
-
gl.uniform2f(wu.canvas, CW, CH);
|
|
597
|
-
gl.uniform2f(wu.off, OX, OY);
|
|
598
|
-
gl.uniform1f(wu.wy, Math.max(0, wy));
|
|
599
|
-
gl.uniform1f(wu.k, Math.min(1.2, Math.max(0, wy) / (H - wy)));
|
|
600
|
-
gl.uniform1f(wu.t, t);
|
|
601
|
-
gl.uniform1f(wu.speed, 1 + Math.abs(l.L.drift ?? 0) * 2);
|
|
602
|
-
gl.uniform1f(wu.u, S.u);
|
|
603
|
-
gl.uniform3f(wu.col, ...l.c);
|
|
604
|
-
band(wprog, Math.max(VY0, wy), VY1);
|
|
1203
|
+
gl.uniform1i(P.wu.tex, 0);
|
|
1204
|
+
gl.uniform2f(P.wu.res, W, H);
|
|
1205
|
+
gl.uniform2f(P.wu.canvas, CW, CH);
|
|
1206
|
+
gl.uniform2f(P.wu.off, OX, OY);
|
|
1207
|
+
gl.uniform1f(P.wu.wy, Math.max(0, wy));
|
|
1208
|
+
gl.uniform1f(P.wu.k, Math.min(1.2, Math.max(0, wy) / (H - wy)));
|
|
1209
|
+
gl.uniform1f(P.wu.t, t);
|
|
1210
|
+
gl.uniform1f(P.wu.speed, 1 + Math.abs(l.L.drift ?? 0) * 2);
|
|
1211
|
+
gl.uniform1f(P.wu.u, S.u);
|
|
1212
|
+
gl.uniform3f(P.wu.col, ...l.c);
|
|
1213
|
+
band(P.wprog, Math.max(VY0, wy), VY1);
|
|
605
1214
|
}
|
|
606
1215
|
|
|
607
1216
|
const wrapX = (x, S, w) => ((x % (S.span + w)) + S.span + w) % (S.span + w) + S.x0 - w;
|
|
608
1217
|
const WINDOW = col('#ffd08a'), DOT = col('255,245,220');
|
|
609
1218
|
|
|
610
|
-
// how finely a live element's region r is repainted: the scale it is seen at
|
|
1219
|
+
// how finely a live element's region r is repainted: the scale it is seen at
|
|
611
1220
|
const LIVE_AWAY = 0.05; // the scale a live element out of sight is painted at (for its lights)
|
|
612
|
-
|
|
1221
|
+
// (a budget of pixels a repaint takes: what is on screen is painted only where it is seen — the
|
|
1222
|
+
// screen and a margin at most — so as sharp as it is seen on a screen up to LIVE_BUDGET; a bigger
|
|
1223
|
+
// one, a little softer: a 4K screen's iGPU is not to repaint 20 million pixels a frame)
|
|
1224
|
+
const LIVE_BUDGET = 8e6;
|
|
1225
|
+
const liveScale = (r, seen) => Math.min(seen, MAX_SIDE / r.w, MAX_SIDE / r.h, Math.sqrt(Math.min(LIVE_BUDGET, Math.max(4e6, 1.3 * CW * CH)) / (r.w * r.h)));
|
|
613
1226
|
|
|
614
1227
|
// A ground plane (water, fields, a road) is painted as seen from the start. Each row of it
|
|
615
1228
|
// lies at a depth — a row h below the horizon is 1/h eye heights away — so as you walk, the
|
|
616
1229
|
// rows are laid out again in perspective: drawn as thin strips, each with its own haze.
|
|
617
|
-
function drawGround(ob, cam, t) {
|
|
1230
|
+
function drawGround(ob, cam, t, draw) {
|
|
618
1231
|
let tx = ob.tex;
|
|
619
1232
|
if (ob.live) {
|
|
620
1233
|
const whole = { x: ob.bx, y: ob.by, w: ob.bw, h: ob.bh }, k = liveScale(whole, 1);
|
|
621
|
-
if (stale(ob
|
|
1234
|
+
if (stale(ob, k, whole)) repaint(ob, k, t, whole);
|
|
622
1235
|
tx = ob.live.tex;
|
|
623
1236
|
}
|
|
1237
|
+
if (!draw) return;
|
|
624
1238
|
const yhH = YH * H, top = Math.max(ob.by, yhH + 0.5), bot = ob.by + ob.bh, N = 120, look = -cam.y * H;
|
|
625
1239
|
const row = y => {
|
|
626
1240
|
const Z0 = H / (y - yhH), D = Z0 - cam.z;
|
|
@@ -634,13 +1248,15 @@ const MooodGL = (() => {
|
|
|
634
1248
|
if (!b) break;
|
|
635
1249
|
const y0 = top + (bot - top) * ((i - 1) / N) ** 1.6, xl = ob.bx, xr = ob.bx + ob.bw;
|
|
636
1250
|
if (b.y > VY0 && a.y < VY1) {
|
|
637
|
-
HZ = haze(a.Z0, a.Z0 - cam.z);
|
|
638
|
-
|
|
1251
|
+
HZ = haze(a.Z0, a.Z0 - cam.z); LT = ob.o.glows ? UNLIT : [1, 0];
|
|
1252
|
+
const da = a.Z0 - cam.z, db = b.Z0 - cam.z;
|
|
1253
|
+
quad(tx, [a.s * xl + a.e, a.y, a.s * xr + a.e, a.y, b.s * xr + b.e, b.y, b.s * xl + b.e, b.y], [0, (y0 - ob.by) / ob.bh, 1, (y1 - ob.by) / ob.bh], [da, da, db, db]);
|
|
639
1254
|
}
|
|
640
1255
|
if (a.y > VY1) break;
|
|
641
1256
|
a = b;
|
|
642
1257
|
}
|
|
643
|
-
HZ = NOHAZE;
|
|
1258
|
+
HZ = NOHAZE; LT = UNLIT;
|
|
1259
|
+
glowsOf(ob, ob.live?.r ? ob.live.lights : ob.lights, cam, t);
|
|
644
1260
|
}
|
|
645
1261
|
|
|
646
1262
|
// ── horizontal planes ────────────────────────────────────────────────
|
|
@@ -653,146 +1269,228 @@ const MooodGL = (() => {
|
|
|
653
1269
|
// World units here are eye heights, as everywhere in the camera: a point (X, Y, Z) is seen
|
|
654
1270
|
// at x = W/2 + H·(X − cam.x)/(Z − cam.z), y = yh·H + H·(1 − Y)/(Z − cam.z) − cam.y·H.
|
|
655
1271
|
// Live elements: repainted every frame if their code read the time as it was painted; if it
|
|
656
|
-
// read only the scene's state (e.state, e.was), when
|
|
657
|
-
// the old painting would blur. What it reads is looked at again at every
|
|
1272
|
+
// read only the scene's state (e.state, e.was), when a key it read changed (engine.js: moved) —
|
|
1273
|
+
// or when seen at a scale the old painting would blur. What it reads is looked at again at every
|
|
1274
|
+
// repaint.
|
|
1275
|
+
// One with `redraw` (engine.js: redrawOf) is repainted only when the keys it names change, whatever
|
|
1276
|
+
// else it reads; one reading the light falling on it (e.lights), when the light does.
|
|
1277
|
+
const timedOf = (o, frames) => o.redraw == null && (frames > 1 || !!I.READS.get(o)?.time);
|
|
658
1278
|
function liveOf(o, frames, tex) {
|
|
659
|
-
const read = I.READS.get(o) || {}, timed = frames
|
|
660
|
-
return timed || read.state ? { canvas: I.layer(1, 1), tex: tex(I.layer(1, 1)), stateOnly: !timed, frames, lights: [] } : null;
|
|
661
|
-
}
|
|
662
|
-
// stale: painted at another state, or at a scale the painting would blur, or not over
|
|
663
|
-
// (painted this frame already — an element that reads it got it painted first — it is not)
|
|
664
|
-
function stale(
|
|
665
|
-
const
|
|
666
|
-
if (live.frame === frameNo &&
|
|
1279
|
+
const read = I.READS.get(o) || {}, timed = timedOf(o, frames);
|
|
1280
|
+
return timed || read.state || read.lights || o.redraw != null ? { canvas: I.layer(1, 1), tex: tex(I.layer(1, 1)), stateOnly: !timed, frames, lights: [] } : null;
|
|
1281
|
+
}
|
|
1282
|
+
// stale: painted at another state (or light), or at a scale the painting would blur, or not over
|
|
1283
|
+
// all of `need` (painted this frame already — an element that reads it got it painted first — it is not)
|
|
1284
|
+
function stale(ob, k, need) {
|
|
1285
|
+
const live = ob.live, f = live.k ? k / live.k : 0, sharp = f > 0.83 && f < 1.2 && live.r && covers(live.r, need);
|
|
1286
|
+
if (live.frame === frameNo && sharp) return false;
|
|
667
1287
|
if (!live.stateOnly) return true;
|
|
668
|
-
|
|
669
|
-
|
|
1288
|
+
if (!sharp) return true;
|
|
1289
|
+
if (ob.o.redraw != null) return live.snap !== I.redrawOf(ob.o);
|
|
1290
|
+
if (live.ver !== I.STATE.ver) { if (I.moved(live.read)) return true; live.ver = I.STATE.ver; }
|
|
1291
|
+
return live.readsLights && live.src !== I.SOURCES.ver;
|
|
670
1292
|
}
|
|
671
|
-
// repaints the region r of a live element (all of it, or what is on screen close up)
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
1293
|
+
// repaints the region r of a live element (all of it, or what is on screen close up); `seen`:
|
|
1294
|
+
// painted to be seen — only then is it looked at for being cut off by its box
|
|
1295
|
+
function repaint(ob, k, t, r, seen = true) {
|
|
1296
|
+
// (looked at for being cut off by its box at most once a second, when it is to be seen and not
|
|
1297
|
+
// too coarse to tell: a few pixels across)
|
|
1298
|
+
const whole = { x: ob.bx, y: ob.by, w: ob.bw, h: ob.bh }, look = seen && !(t - ob.cutAt < 1) && r.w * k >= 16 && r.h * k >= 16 && checked(ob.o);
|
|
1299
|
+
const p = paintObject(ob.o, r.x, r.y, r.w, r.h, k, t, ob.live.canvas, look ? whole : null);
|
|
1300
|
+
if (look) { ob.cutAt = t; ob.cut = p.cut; }
|
|
1301
|
+
ob.live.r = p.region; ob.live.frame = frameNo;
|
|
1302
|
+
if (p.parts.length || ob.parts) { ob.parts ||= []; paintParts(SCENE, ob, p.parts, CAM); }
|
|
675
1303
|
flush(); // quads already batched must be drawn with their own texture first
|
|
1304
|
+
const t0 = I.PERF.on ? I.clock() : 0;
|
|
676
1305
|
gl.bindTexture(gl.TEXTURE_2D, ob.live.tex);
|
|
677
1306
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, p.canvas);
|
|
1307
|
+
if (I.PERF.on) I.PERF.add(ob.o, I.clock() - t0, true); // (its upload too)
|
|
678
1308
|
curTex = null;
|
|
679
|
-
ob.live.ver = I.STATE.ver; ob.live.k = k;
|
|
680
|
-
ob.live.stateOnly = !(ob.live.frames
|
|
681
|
-
|
|
1309
|
+
ob.live.ver = I.STATE.ver; ob.live.k = k; ob.live.src = I.SOURCES.ver; ob.live.snap = I.redrawOf(ob.o); ob.live.read = I.READS.get(ob.o);
|
|
1310
|
+
ob.live.stateOnly = !timedOf(ob.o, ob.live.frames); ob.live.readsLights = !!ob.live.read?.lights;
|
|
1311
|
+
I.PERF.repainted(ob.o);
|
|
1312
|
+
ob.live.lights = p.lights.map(l => ({ x: l.x, y: l.y, r: l.r, c: col(l.col), a: l.a, dot: l.dot, flicker: l.flicker, id: l.id }));
|
|
682
1313
|
}
|
|
683
1314
|
const PLANE_TEX = 2048;
|
|
684
1315
|
function planeOf(o0, dz, tex) {
|
|
685
|
-
const E = I.EYE,
|
|
686
|
-
const at = dz.objectAt(o0), Zc = at.Z, Xc = at.X, rot = (o0.rotation || 0) * Math.PI / 180;
|
|
1316
|
+
const E = I.EYE, plane = I.planeOf(o0, dz), w = plane.w * E, d = plane.d * E, Zc = plane.Zc;
|
|
687
1317
|
// painted as a flat (sky-like) element: code units are metres, 1 m = H px before scaling
|
|
688
|
-
const o = { ...o0, sky: true, plane: true };
|
|
1318
|
+
const o = { ...o0, sky: true, plane: true, _plane: plane };
|
|
689
1319
|
const k = PLANE_TEX / (Math.max(w, d) * H), bx = -w / 2 * H, by = -d / 2 * H, bw = w * H, bh = d * H;
|
|
690
1320
|
const r0 = paintObject(o, bx, by, bw, bh, k, 0);
|
|
691
1321
|
const live = liveOf(o, 0, tex);
|
|
692
|
-
const c = Math.abs(
|
|
1322
|
+
const c = Math.abs(plane.cos), s = Math.abs(plane.sin);
|
|
693
1323
|
return {
|
|
694
|
-
o, plane
|
|
695
|
-
|
|
696
|
-
live,
|
|
1324
|
+
o, plane, Z: Zc + (d * c + w * s) / 2 / E, bx, by, bw, bh, k, tex: tex(r0.canvas), frames: [],
|
|
1325
|
+
lights: r0.lights.map(l => ({ x: l.x, y: l.y, r: l.r, c: col(l.col), a: l.a, dot: l.dot, flicker: l.flicker, id: l.id })),
|
|
1326
|
+
live, spec: o0, unseen: !!o0.unseen,
|
|
697
1327
|
};
|
|
698
1328
|
}
|
|
699
|
-
// A flat piece of the world drawn truly in perspective: world(f, g) → [X, Y,
|
|
700
|
-
//
|
|
701
|
-
//
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const rx = X - cam.x, rz = Z - cam.z, c = Math.cos(cam.yaw || 0), s = Math.sin(cam.yaw || 0);
|
|
706
|
-
return [rx * c - rz * s, (cam.h ?? 1) - Y, rx * s + rz * c];
|
|
707
|
-
};
|
|
708
|
-
function surface(cam, tex, world, C, R, hazeOf) {
|
|
709
|
-
const cell = (f0, g0, f1, g1, depth) => {
|
|
710
|
-
let poly = [[f0, g0], [f1, g0], [f1, g1], [f0, g1]].map(([f, g]) => ({ e: eyeOf(cam, ...world(f, g)), f, g }));
|
|
711
|
-
const zs = poly.map(k => k.e[2]);
|
|
712
|
-
if (Math.max(...zs) < NEAR) return;
|
|
713
|
-
const size = Math.hypot(poly[0].e[0] - poly[2].e[0], poly[0].e[1] - poly[2].e[1], poly[0].e[2] - poly[2].e[2]);
|
|
714
|
-
if (depth < 4 && Math.min(...zs) < size) {
|
|
715
|
-
const fm = (f0 + f1) / 2, gm = (g0 + g1) / 2;
|
|
716
|
-
cell(f0, g0, fm, gm, depth + 1); cell(fm, g0, f1, gm, depth + 1); cell(f0, gm, fm, g1, depth + 1); cell(fm, gm, f1, g1, depth + 1);
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
if (Math.min(...zs) < NEAR) {
|
|
720
|
-
const cut = [];
|
|
721
|
-
for (let i = 0; i < poly.length; i++) {
|
|
722
|
-
const a = poly[i], b = poly[(i + 1) % poly.length], ain = a.e[2] >= NEAR, bin = b.e[2] >= NEAR;
|
|
723
|
-
if (ain) cut.push(a);
|
|
724
|
-
if (ain !== bin) {
|
|
725
|
-
const k = (NEAR - a.e[2]) / (b.e[2] - a.e[2]);
|
|
726
|
-
cut.push({ e: a.e.map((v, j) => v + (b.e[j] - v) * k), f: a.f + (b.f - a.f) * k, g: a.g + (b.g - a.g) * k });
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
poly = cut;
|
|
730
|
-
if (poly.length < 3) return;
|
|
731
|
-
}
|
|
732
|
-
const pt = poly.map(k => [W / 2 + H * k.e[0] / k.e[2], YH * H + H * k.e[1] / k.e[2] - cam.y * H, k.f, k.g]);
|
|
733
|
-
const xs = pt.map(q => q[0]), ys = pt.map(q => q[1]);
|
|
734
|
-
if (Math.max(...xs) < VX0 || Math.min(...xs) > VX1 || Math.max(...ys) < VY0 || Math.min(...ys) > VY1) return;
|
|
735
|
-
HZ = hazeOf(poly.reduce((d, k) => d + k.e[2], 0) / poly.length);
|
|
1329
|
+
// A flat piece of the world drawn truly in perspective (engine.js: cellsOf — world(f, g) → [X, Y,
|
|
1330
|
+
// Z], C × R cells; or its cells made already: the land's, landCells), each cell with the haze of
|
|
1331
|
+
// its distance (hazeOf(D)), lit or not
|
|
1332
|
+
function surface(cam, tex, world, C, R, hazeOf, lit, made) {
|
|
1333
|
+
const quads = pt => {
|
|
1334
|
+
HZ = hazeOf(pt.reduce((d, q) => d + q[4], 0) / pt.length); LT = lit ? [1, 0] : UNLIT;
|
|
736
1335
|
if (pt.length === 3) quadAt(tex, pt[0], pt[1], pt[2], pt[2]);
|
|
737
1336
|
else quadAt(tex, pt[0], pt[1], pt[2], pt[3]);
|
|
738
1337
|
if (pt.length === 5) quadAt(tex, pt[0], pt[3], pt[4], pt[4]);
|
|
739
1338
|
};
|
|
740
|
-
|
|
741
|
-
HZ = NOHAZE;
|
|
1339
|
+
if (made) made.forEach(quads); else I.cellsOf(cam, world, C, R, W, H, YH, [VX0, VY0, VX1, VY1], quads);
|
|
1340
|
+
HZ = NOHAZE; LT = UNLIT;
|
|
1341
|
+
}
|
|
1342
|
+
// the land this frame: its draped planes' cells, as they are drawn (drawPlane) and what stands
|
|
1343
|
+
// behind them is cut along (engine.js: landLine) — the view's and above it
|
|
1344
|
+
function landCells(S, cam) {
|
|
1345
|
+
for (const ob of S.lands) { ob.cells = []; I.cellsOf(cam, I.onPlane(ob.plane), ...I.PLANE_CELLS.webgl, W, H, YH, [VX0, -Infinity, VX1, VY1], pts => ob.cells.push(pts)); }
|
|
1346
|
+
}
|
|
1347
|
+
// the glows of a plane's or the ground's lights, where they are in the world
|
|
1348
|
+
function glowsOf(ob, lights, cam, t) {
|
|
1349
|
+
for (const lt of lights) {
|
|
1350
|
+
const q = lightPlace(ob, lt), p = q && I.project(cam, ...q.at, W, H, YH);
|
|
1351
|
+
if (!p) continue;
|
|
1352
|
+
const r = q.reach / 2 * H / p[2], fl = flickerOf(lt, 0, 0, t);
|
|
1353
|
+
MD = p[2];
|
|
1354
|
+
sprite(T.glow, ID, p[0], p[1], 2 * r, 2 * r, 0, lt.c, lt.a * 0.6 * fl, true);
|
|
1355
|
+
if (lt.dot) sprite(T.glow, ID, p[0], p[1], 6 * lt.dot * r / lt.r, 6 * lt.dot * r / lt.r, 0, DOT, 0.9, true);
|
|
1356
|
+
}
|
|
742
1357
|
}
|
|
743
1358
|
|
|
744
|
-
|
|
1359
|
+
const planeHaze = D => { const h = hz(D); return [1 - h, h]; };
|
|
1360
|
+
// a pass over a plane's cells, as it is drawn, with a program of its own (mirrorPass, poolPass)
|
|
1361
|
+
function overPlane(ob, cam, tx, pass) {
|
|
1362
|
+
flush(); PASS = pass;
|
|
1363
|
+
surface(cam, tx, I.onPlane(ob.plane), ...I.PLANE_CELLS.webgl, planeHaze, false, ob.cells);
|
|
1364
|
+
flush(); PASS = null;
|
|
1365
|
+
}
|
|
1366
|
+
// Each reflecting plane in view: what is above it drawn as its mirror shows it (MIR) into the
|
|
1367
|
+
// plane's mirror target — at half size, only where the plane is seen (the scissor), its mip levels
|
|
1368
|
+
// made for the blur — before the frame: it is laid on the plane as the plane is drawn (mirrorPass).
|
|
1369
|
+
// Its own `reflect` copy is not drawn over it; what is cut by others (clip) is not mirrored.
|
|
1370
|
+
function mirrors(S, spec, cam, t, mSky) {
|
|
1371
|
+
for (const ob of S.objects) {
|
|
1372
|
+
ob.mirrored = null;
|
|
1373
|
+
const Pl = ob.plane;
|
|
1374
|
+
if (!S.mirrors.includes(Pl)) continue;
|
|
1375
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
1376
|
+
I.cellsOf(cam, I.onPlane(Pl), 4, 4, W, H, YH, [VX0, VY0, VX1, VY1], pts => { for (const q of pts) { x0 = Math.min(x0, q[0]); x1 = Math.max(x1, q[0]); y0 = Math.min(y0, q[1]); y1 = Math.max(y1, q[1]); } });
|
|
1377
|
+
x0 = Math.max(x0, VX0); x1 = Math.min(x1, VX1); y0 = Math.max(y0, VY0); y1 = Math.min(y1, VY1);
|
|
1378
|
+
if (!(x1 > x0 && y1 > y0)) continue;
|
|
1379
|
+
const R = ob.mirrorT ??= mirrorTarget(Math.ceil(CW / 2), Math.ceil(CH / 2)), keep = P;
|
|
1380
|
+
flush();
|
|
1381
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, R.fbo);
|
|
1382
|
+
gl.viewport(0, 0, R.w, R.h);
|
|
1383
|
+
gl.clearColor(0, 0, 0, 0);
|
|
1384
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
1385
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
1386
|
+
gl.scissor(Math.floor((x0 + OX) / 2) - 1, Math.floor((CH - y1 - OY) / 2) - 1, Math.ceil((x1 - x0) / 2) + 3, Math.ceil((y1 - y0) / 2) + 3);
|
|
1387
|
+
P = MIRP ??= mirrorProgram();
|
|
1388
|
+
MIR = { ob, Y: Pl.Y, A: (YH - cam.y) * H, B: H * ((cam.h ?? 1) - Pl.Y) }; MD = Infinity;
|
|
1389
|
+
if (S.sky) rect(S.sky, mSky, S.x0, -H, S.span, 3 * H, WHITE, 1, false, [0, -1, 1, 2]);
|
|
1390
|
+
if (ROUND) drawAround(S, cam, t); else drawAhead(S, spec, cam, t, mSky);
|
|
1391
|
+
flush();
|
|
1392
|
+
MIR = null; P = keep;
|
|
1393
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
1394
|
+
for (const tex of [R.tex, R.lit, R.glow]) { gl.bindTexture(gl.TEXTURE_2D, tex); gl.generateMipmap(gl.TEXTURE_2D); }
|
|
1395
|
+
curTex = null;
|
|
1396
|
+
ob.mirrored = R;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
// the plane's mirror laid on it (RFS)
|
|
1400
|
+
function mirrorPass(ob) {
|
|
1401
|
+
const M = ob.plane.mirror, R = ob.mirrored;
|
|
1402
|
+
return {
|
|
1403
|
+
prog: P.rprog, u: P.ru, add: false, set: u => {
|
|
1404
|
+
[R.tex, R.lit, R.glow].forEach((tex, i) => { gl.activeTexture(gl.TEXTURE1 + i); gl.bindTexture(gl.TEXTURE_2D, tex); });
|
|
1405
|
+
gl.uniform1i(u.mc, 1); gl.uniform1i(u.mh, 2); gl.uniform1i(u.mg, 3);
|
|
1406
|
+
gl.uniform1f(u.strength, M.strength); gl.uniform1f(u.fade, M.fade); gl.uniform1f(u.blur, M.blur);
|
|
1407
|
+
},
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
// the scene's point lights near a plane that is `lit`, laid on it as much as the scene is not lit
|
|
1411
|
+
// (LFS) — or null
|
|
1412
|
+
const POOL_LIGHTS = 8;
|
|
1413
|
+
function poolPass(ob, t) {
|
|
1414
|
+
const Pl = ob.plane, k = Pl.pool * (1 - SCENE.lit), half = Math.hypot(Pl.w, Pl.d) / 2;
|
|
1415
|
+
if (k <= 0) return null;
|
|
1416
|
+
const pts = SCENE.sources.filter(s => !s.dir && s.strength > 0.002 && s.at[1] >= Pl.Y - 0.03 && Math.hypot(s.at[0] - Pl.Xc, s.at[2] - Pl.Zc) - half < s.reach);
|
|
1417
|
+
if (!pts.length) return null;
|
|
1418
|
+
pts.sort((a, b) => b.strength * b.reach - a.strength * a.reach).length = Math.min(pts.length, POOL_LIGHTS);
|
|
1419
|
+
const lp = new Float32Array(4 * POOL_LIGHTS), lc = new Float32Array(3 * POOL_LIGHTS);
|
|
1420
|
+
pts.forEach((s, i) => {
|
|
1421
|
+
const f = s.sprite ? flickerOf(s.sprite.lt, s.sprite.x, s.sprite.y, t) : I.flickerNow(s, t);
|
|
1422
|
+
lp.set([...s.at, s.reach], 4 * i);
|
|
1423
|
+
lc.set(s.rgb.map(v => v / 255 * s.strength * f * k), 3 * i);
|
|
1424
|
+
});
|
|
1425
|
+
return {
|
|
1426
|
+
prog: P.lprog, u: P.lu, add: true, set: u => {
|
|
1427
|
+
gl.uniform3f(u.pc, Pl.Xc, Pl.Y, Pl.Zc); gl.uniform4f(u.pa, Pl.w, Pl.d, Pl.cos, Pl.sin);
|
|
1428
|
+
gl.uniform1i(u.n, pts.length); gl.uniform4fv(u.lp, lp); gl.uniform3fv(u.lc, lc);
|
|
1429
|
+
},
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
function drawPlane(ob, cam, t, draw) {
|
|
745
1433
|
let tx = ob.tex;
|
|
746
1434
|
if (ob.live) {
|
|
747
1435
|
const whole = { x: ob.bx, y: ob.by, w: ob.bw, h: ob.bh }, k = Math.min(ob.k, 1024 / Math.max(ob.bw, ob.bh));
|
|
748
|
-
if (stale(ob
|
|
1436
|
+
if (stale(ob, k, whole)) repaint(ob, k, t, whole);
|
|
749
1437
|
tx = ob.live.tex;
|
|
750
1438
|
}
|
|
751
|
-
|
|
752
|
-
// above): all of it is the air at its own distance
|
|
753
|
-
|
|
754
|
-
surface(cam, tx, (
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
1439
|
+
if (!draw) return;
|
|
1440
|
+
// painted without haze (seen from above): all of it is the air at its own distance; draped
|
|
1441
|
+
// (spec.relief), it lies on the land
|
|
1442
|
+
surface(cam, tx, I.onPlane(ob.plane), ...I.PLANE_CELLS.webgl, planeHaze, !ob.o.glows, ob.cells);
|
|
1443
|
+
ob.shown = { tx, r: { x: ob.bx, y: ob.by, w: ob.bw, h: ob.bh }, frame: frameNo };
|
|
1444
|
+
if (ob.mirrored && !MIR) overPlane(ob, cam, tx, mirrorPass(ob));
|
|
1445
|
+
const pool = !MIR && ob.plane.pool && poolPass(ob, t);
|
|
1446
|
+
if (pool) overPlane(ob, cam, tx, pool);
|
|
1447
|
+
glowsOf(ob, ob.live?.r ? ob.live.lights : ob.lights, cam, t);
|
|
758
1448
|
}
|
|
759
1449
|
|
|
1450
|
+
// A live element out of sight: painted all the same, coarsely, so its lights are where they are
|
|
1451
|
+
// now (a lantern carried off-screen still lights the edge it is behind) and what keeps a memory
|
|
1452
|
+
// of its own goes on — the elements it reads (`after`) first
|
|
1453
|
+
function unseen(ob, t) {
|
|
1454
|
+
for (const id of Array.isArray(ob.o.after) ? ob.o.after : []) { const other = BYID.get(id); if (other?.live) unseen(other, t); }
|
|
1455
|
+
const whole = { x: ob.bx, y: ob.by, w: ob.bw, h: ob.bh }, k = Math.min(LIVE_AWAY, liveScale(whole, 1));
|
|
1456
|
+
if (stale(ob, k, whole)) repaint(ob, k, t, whole, false);
|
|
1457
|
+
return { tx: ob.live.tex, r: ob.live.r, lights: ob.live.lights };
|
|
1458
|
+
}
|
|
760
1459
|
// the texture an element is shown with now: its live painting (repainted when due), else its
|
|
761
1460
|
// frame of the loop — the sharp close-up when that covers what is seen of it
|
|
762
1461
|
function textureOf(ob, m, t) {
|
|
763
1462
|
const n = ob.frames.length, fi = n > 1 ? Math.floor((((t / ob.period) % 1) + 1) % 1 * n) % n : 0, whole = { x: ob.bx, y: ob.by, w: ob.bw, h: ob.bh };
|
|
764
1463
|
const seen = ob.live && m ? intersect(whole, visible(m)) : null;
|
|
765
1464
|
if (ob.live && (!m || seen)) {
|
|
766
|
-
// close up only what is on screen
|
|
767
|
-
//
|
|
768
|
-
const need = seen || whole;
|
|
769
|
-
const r = seen ? intersect(whole, { x: seen.x - seen.w *
|
|
1465
|
+
// close up only what is on screen is painted, as sharp as it is seen — with a margin, wide for
|
|
1466
|
+
// one repainted only as the state changes (a small move of the camera needn't repaint it)
|
|
1467
|
+
const need = seen || whole, mg = ob.live.stateOnly ? 0.15 : 0.02;
|
|
1468
|
+
const r = seen ? intersect(whole, { x: seen.x - seen.w * mg, y: seen.y - seen.h * mg, w: seen.w * (1 + 2 * mg), h: seen.h * (1 + 2 * mg) }) : whole;
|
|
770
1469
|
const k = liveScale(r, m ? m[0] : 1);
|
|
771
|
-
if (stale(ob
|
|
772
|
-
return { tx: ob.live.tex, r: ob.live.r, lights: ob.live.lights };
|
|
773
|
-
}
|
|
774
|
-
if (ob.live) {
|
|
775
|
-
// out of sight: painted all the same, coarsely, so its lights are where they are now (a
|
|
776
|
-
// lantern carried off-screen still lights the edge it is behind)
|
|
777
|
-
const k = Math.min(LIVE_AWAY, liveScale(whole, 1));
|
|
778
|
-
if (stale(ob.live, k, whole)) repaint(ob, k, t, whole);
|
|
1470
|
+
if (stale(ob, k, need)) repaint(ob, k, t, r);
|
|
779
1471
|
return { tx: ob.live.tex, r: ob.live.r, lights: ob.live.lights };
|
|
780
1472
|
}
|
|
1473
|
+
if (ob.live) return unseen(ob, t);
|
|
781
1474
|
const det = ob.detail, onScreen = det && m && intersect(whole, visible(m));
|
|
782
1475
|
if (det && m && (!onScreen || covers(det.region, onScreen))) return { tx: det.frames[fi], r: det.region, lights: ob.lights };
|
|
783
|
-
|
|
1476
|
+
// (painted with a margin round its box, when the scene was checked: pad, fractions of it)
|
|
1477
|
+
const [px, py] = ob.pad;
|
|
1478
|
+
return { tx: ob.frames[fi], r: { x: ob.bx - px * ob.bw, y: ob.by - py * ob.bh, w: ob.bw * (1 + 2 * px), h: ob.bh * (1 + 2 * py) }, lights: ob.lights };
|
|
784
1479
|
}
|
|
785
1480
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
if (ob.
|
|
789
|
-
if (
|
|
790
|
-
if (ROUND &&
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1481
|
+
// draw: false — only what drawing it paints is painted (a frame unseen, painted as when drawn: tick)
|
|
1482
|
+
function drawObjectAt(ob, cam, t, draw = true) {
|
|
1483
|
+
if (ob.plane) { drawPlane(ob, cam, t, draw); return; }
|
|
1484
|
+
if (ob.ground) { drawGround(ob, cam, t, draw); return; }
|
|
1485
|
+
if (ROUND && ob.o.sky) { drawSkyAround(ob, cam, t, draw); return; }
|
|
1486
|
+
if (ROUND && typeof ob.o.facing === 'number') { drawCard(ob, cam, t, draw); return; }
|
|
1487
|
+
// (out of sight — behind the eye, passed — a live element goes on all the same, painted coarsely,
|
|
1488
|
+
// as a frame unseen paints it: tick)
|
|
1489
|
+
const at = matOf(ob, cam), fade = at ? I.nearFade(at.D) : 0;
|
|
1490
|
+
if (fade <= 0) { if (ob.live) unseen(ob, t); return; }
|
|
1491
|
+
MD = at.D;
|
|
795
1492
|
const { o, x, y, s } = ob;
|
|
1493
|
+
if (draw && ob.cut) cutShown(ob, at.m);
|
|
796
1494
|
// a platformer's hero: moved and turned from where it is painted
|
|
797
1495
|
const m = cam.hero && o.id === cam.hero.id ? mul(at.m, I.heroMatrix(cam.hero, x, s / (o.height || 2))) : at.m;
|
|
798
1496
|
if (o.kind === 'bird') {
|
|
@@ -821,46 +1519,59 @@ const MooodGL = (() => {
|
|
|
821
1519
|
const other = BYID.get(id), oat = other?.live && matOf(other, cam);
|
|
822
1520
|
if (oat) textureOf(other, oat.m, t);
|
|
823
1521
|
}
|
|
824
|
-
const
|
|
1522
|
+
const { tx, r, lights } = textureOf(ob, m, t);
|
|
1523
|
+
if (!draw) return;
|
|
1524
|
+
const ma = a === ID ? m : mul(m, a);
|
|
1525
|
+
// the land in front of it (spec.relief) hides what is behind its rise
|
|
1526
|
+
const line = o.sky ? null : landOver(ma, r, at.D);
|
|
1527
|
+
if (line === false) return;
|
|
1528
|
+
ob.shown = { tx, r, m: ma, frame: frameNo };
|
|
1529
|
+
LT = o.sky || o.glows ? UNLIT : [1, at.D];
|
|
825
1530
|
// on water: a mirrored, fainter copy about the water surface under it (for something
|
|
826
|
-
// raised above the ground, e.g. sitting on a pier, that is below its base)
|
|
827
|
-
|
|
1531
|
+
// raised above the ground, e.g. sitting on a pier, that is below its base), fading away from it —
|
|
1532
|
+
// not over a reflecting plane: that mirrors it
|
|
1533
|
+
if (ob.reflect && !MIR && !I.overMirror(SCENE.mirrors, ob.at)) {
|
|
828
1534
|
const gy = isFinite(ob.Z) ? Math.max(y, (YH + 1 / ob.Z) * H) : YH * H; // sky: about the horizon
|
|
829
|
-
rect(tx, mul(ma, [1, 0, 0, -1, 0, 2 * gy]), r.x, r.y, r.w, r.h, WHITE, 0.3 * fade);
|
|
1535
|
+
rect(tx, mul(ma, [1, 0, 0, -1, 0, 2 * gy]), r.x, r.y, r.w, r.h, WHITE, 0.08 * fade, false, UV, 0.3 * fade);
|
|
830
1536
|
}
|
|
831
|
-
|
|
1537
|
+
if (line) rectAbove(tx, ma, r.x, r.y, r.w, r.h, UV, fade, line);
|
|
1538
|
+
else rect(tx, ma, r.x, r.y, r.w, r.h, WHITE, fade);
|
|
1539
|
+
LT = UNLIT;
|
|
832
1540
|
for (const lt of lights) {
|
|
833
|
-
const fl = lt.fire ? 0.8 + 0.2 * Math.sin(t * 11)
|
|
834
|
-
: lt.flicker != null ? 1 - lt.flicker * 0.5 * (0.5 + 0.5 * Math.sin(t * 9 + lt.x) * Math.sin(t * 5.3 + lt.y))
|
|
835
|
-
: 0.9 + 0.1 * Math.sin(t * 7 + x) * Math.sin(t * 3.1 + y);
|
|
836
1541
|
const [lx, ly] = apply(a, lt.x, lt.y);
|
|
837
|
-
|
|
1542
|
+
if (line && under(line, ...apply(m, lx, ly))) continue;
|
|
1543
|
+
sprite(T.glow, m, lx, ly, 2 * lt.r, 2 * lt.r, 0, lt.c, lt.a * 0.6 * flickerOf(lt, x, y, t) * fade, true);
|
|
838
1544
|
if (lt.dot) sprite(T.glow, m, lx, ly, 6 * lt.dot, 6 * lt.dot, 0, DOT, 0.9 * fade, true);
|
|
839
1545
|
}
|
|
840
|
-
|
|
1546
|
+
}
|
|
841
1547
|
|
|
842
1548
|
// Looking around, an element with `facing` stands turned its own way (a wall, a door, a
|
|
843
1549
|
// cupboard): its painting is a vertical card in the world — the painted point (px, py) is
|
|
844
1550
|
// (px − x, py − y)/ppm metres from its anchor along the card and down — projected truly,
|
|
845
1551
|
// as a grid of small cells like a plane, each with the haze of its own distance.
|
|
846
|
-
function drawCard(ob, cam, t) {
|
|
1552
|
+
function drawCard(ob, cam, t, draw) {
|
|
847
1553
|
const p = ob.p, f = ob.o.facing * Math.PI / 180, ppm = H / p.Zp; // painted px per eye height
|
|
848
1554
|
const tx_ = -Math.cos(f), tz = Math.sin(f); // along the card, to the right of one facing it
|
|
849
1555
|
const { tx, r, lights } = textureOf(ob, null, t);
|
|
1556
|
+
if (!draw) return;
|
|
1557
|
+
ob.shown = { tx, r, frame: frameNo };
|
|
850
1558
|
const world = (px, py) => { const u = (px - ob.x) / ppm, v = (py - ob.y) / ppm; return [p.X + u * tx_, p.Y - v, p.Z + u * tz]; };
|
|
851
|
-
surface(cam, tx, (f, g) => world(r.x + r.w * f, r.y + r.h * g), 16, 10, D => haze(p.Zp, D));
|
|
1559
|
+
surface(cam, tx, (f, g) => world(r.x + r.w * f, r.y + r.h * g), 16, 10, D => haze(p.Zp, D), !ob.o.glows);
|
|
852
1560
|
const at = (px, py) => I.project(cam, ...world(px, py), W, H, YH);
|
|
853
1561
|
for (const lt of lights) {
|
|
854
1562
|
const q = at(lt.x, lt.y);
|
|
855
1563
|
if (!q) continue;
|
|
1564
|
+
MD = q[2];
|
|
856
1565
|
const k = p.Zp / q[2], fl = lt.flicker != null ? 1 - lt.flicker * 0.5 * (0.5 + 0.5 * Math.sin(t * 9 + lt.x) * Math.sin(t * 5.3 + lt.y)) : 1;
|
|
857
1566
|
sprite(T.glow, ID, q[0], q[1], 2 * lt.r * k, 2 * lt.r * k, 0, lt.c, lt.a * 0.6 * fl, true);
|
|
858
1567
|
}
|
|
859
1568
|
}
|
|
860
1569
|
// The sky all around: its painting is the full circle (2π stage heights, straight ahead in
|
|
861
1570
|
// the middle); each narrow column of the frame shows the bearing it looks at.
|
|
862
|
-
function drawSkyAround(ob, cam, t) {
|
|
1571
|
+
function drawSkyAround(ob, cam, t, draw) {
|
|
863
1572
|
const { tx } = textureOf(ob, null, t), C = 48, top = ob.by - cam.y * H;
|
|
1573
|
+
if (!draw) return;
|
|
1574
|
+
MD = Infinity;
|
|
864
1575
|
for (let i = 0; i < C; i++) {
|
|
865
1576
|
const xa = VX0 + (VX1 - VX0) * i / C, xb = VX0 + (VX1 - VX0) * (i + 1) / C;
|
|
866
1577
|
const ua = (cam.yaw + Math.atan((xa - W / 2) / H)) / I.TAU + 0.5, ub = (cam.yaw + Math.atan((xb - W / 2) / H)) / I.TAU + 0.5;
|
|
@@ -875,6 +1586,7 @@ const MooodGL = (() => {
|
|
|
875
1586
|
|
|
876
1587
|
// An element that moves through the world (its `place`): put where the state has it now
|
|
877
1588
|
// (depths().now) — its size, its depth, its haze and what it is drawn in front of or behind follow.
|
|
1589
|
+
// → whether anything moved
|
|
878
1590
|
function placeMoving(S) {
|
|
879
1591
|
let moved = false;
|
|
880
1592
|
for (const ob of S.objects) {
|
|
@@ -882,22 +1594,101 @@ const MooodGL = (() => {
|
|
|
882
1594
|
const n = S.dz.now(ob.from), key = `${n.o.x}|${n.o.y}|${n.o.size}`;
|
|
883
1595
|
if (key === ob.placed) continue;
|
|
884
1596
|
ob.placed = key; moved = true;
|
|
885
|
-
const o = { ...n.o, _sw: ob.o._sw };
|
|
886
|
-
Object.assign(ob, { o, Z: n.Z, x: o.x * SW, y: o.y * H, s: Math.max(2, o.size * H) }, I.objectBox(o, SW, H));
|
|
1597
|
+
const o = { ...n.o, _sw: ob.o._sw }, was = { bx: ob.bx, by: ob.by, bw: ob.bw };
|
|
1598
|
+
Object.assign(ob, { o, Z: n.Z, x: o.x * SW, y: o.y * H, s: Math.max(2, o.size * H), at: n.at }, I.objectBox(o, SW, H));
|
|
887
1599
|
if (ROUND && !o.sky) ob.p = n.at;
|
|
1600
|
+
// its painting as painted is laid over its box where it is now: its glows go with it (a live
|
|
1601
|
+
// one's are where it is repainted)
|
|
1602
|
+
const k = ob.bw / was.bw;
|
|
1603
|
+
if (!ob.live) ob.lights = ob.lights.map(l => ({ ...l, x: ob.bx + (l.x - was.bx) * k, y: ob.by + (l.y - was.by) * k, r: l.r * k, dot: l.dot && l.dot * k }));
|
|
1604
|
+
}
|
|
1605
|
+
return moved;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// Where an element's light (l: in its painting's stage px, as lights are kept) is in the world, and
|
|
1609
|
+
// how far it reaches (twice its glow): { at, reach } in eye heights, or null (the sky's)
|
|
1610
|
+
function lightPlace(ob, l) {
|
|
1611
|
+
if (ob.plane) {
|
|
1612
|
+
const P = ob.plane, u = l.x / H / I.EYE, v = l.y / H / I.EYE;
|
|
1613
|
+
return { at: [P.Xc + u * P.cos + v * P.sin, P.Y, P.Zc - v * P.cos + u * P.sin], reach: 2 * l.r / H / I.EYE };
|
|
1614
|
+
}
|
|
1615
|
+
if (ob.ground) {
|
|
1616
|
+
// a row h below the horizon lies 1/h eye heights away, on the ground
|
|
1617
|
+
const below = l.y - YH * H;
|
|
1618
|
+
if (below < 1) return null;
|
|
1619
|
+
const Z = H / below;
|
|
1620
|
+
return { at: [(l.x - SW / 2) / H * Z, 0, Z], reach: 2 * l.r / H * Z };
|
|
1621
|
+
}
|
|
1622
|
+
if (ob.o.sky || !ob.at) return null;
|
|
1623
|
+
const k = ob.at.Zp / H;
|
|
1624
|
+
return { at: [ob.at.X + (l.x - ob.x) * k, ob.at.Y - (l.y - ob.y) * k, ob.at.Z], reach: 2 * l.r * k };
|
|
1625
|
+
}
|
|
1626
|
+
// a light's flicker now, as its glow flickers (drawObjectAt)
|
|
1627
|
+
const flickerOf = (lt, x, y, t) => (lt.fire ? 0.8 + 0.2 * Math.sin(t * 11)
|
|
1628
|
+
: lt.flicker != null ? 1 - lt.flicker * 0.5 * (0.5 + 0.5 * Math.sin(t * 9 + lt.x) * Math.sin(t * 5.3 + lt.y))
|
|
1629
|
+
: 0.9 + 0.1 * Math.sin(t * 7 + x) * Math.sin(t * 3.1 + y));
|
|
1630
|
+
// the light in the air (engine.js: airOf), where it is seen: a source of the spec's halo (`glow`),
|
|
1631
|
+
// or the haze round a point source (the light's hazeGlow)
|
|
1632
|
+
function halo(S, { h, fog }, cam, t) {
|
|
1633
|
+
const p = I.project(cam, ...h.at, W, H, YH);
|
|
1634
|
+
if (!p) return;
|
|
1635
|
+
MD = p[2];
|
|
1636
|
+
const c = h.rgb.map(v => v / 255);
|
|
1637
|
+
if (fog) { const r = h.reach * H / p[2]; sprite(T.glow, ID, p[0], p[1], 2 * r, 2 * r, 0, c, I.airGlow(S.look, h, p[2], t), true); return; }
|
|
1638
|
+
const r = h.halo * H / p[2], a = Math.min(1, h.strength) * I.flickerNow(h, t);
|
|
1639
|
+
sprite(T.glow, ID, p[0], p[1], 2 * r, 2 * r, 0, c, 0.6 * a, true);
|
|
1640
|
+
sprite(T.glow, ID, p[0], p[1], 0.5 * r, 0.5 * r, 0, DOT, 0.8 * a, true);
|
|
1641
|
+
}
|
|
1642
|
+
// the scene's sources now (engine.js: sourcesNow): its own, and its elements' lights as last painted
|
|
1643
|
+
// (sprite: the glow an element's light is drawn with — the light flickers as it does)
|
|
1644
|
+
function sourcesOf(S, spec) {
|
|
1645
|
+
const from = [];
|
|
1646
|
+
for (const ob of S.objects) {
|
|
1647
|
+
if (ob.o.kind === 'bird') continue;
|
|
1648
|
+
for (const l of ob.live?.r ? ob.live.lights : ob.lights) {
|
|
1649
|
+
const q = lightPlace(ob, l);
|
|
1650
|
+
if (q) from.push({ id: l.id ?? String(ob.o.id ?? ob.o.kind), ...q, rgb: l.c.map(v => v * 255), strength: l.a, sprite: { lt: l, x: ob.x, y: ob.y } });
|
|
1651
|
+
}
|
|
1652
|
+
// its parts' (e.part), where each was painted
|
|
1653
|
+
for (const part of ob.parts || []) {
|
|
1654
|
+
const k = part.at.Zp / H;
|
|
1655
|
+
for (const l of part.lights) {
|
|
1656
|
+
const lx = part.bx + l.u * part.bw, ly = part.by + l.v * part.bh;
|
|
1657
|
+
from.push({ id: l.id ?? String(ob.o.id), at: [part.at.X + (lx - part.x) * k, part.at.Y - (ly - part.y) * k, part.at.Z], reach: 2 * l.r * part.bw * k, rgb: l.c.map(v => v * 255), strength: l.a, sprite: { lt: { flicker: l.flicker, x: lx, y: ly }, x: part.x, y: part.y } });
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
888
1660
|
}
|
|
889
|
-
|
|
890
|
-
for (const item of S.order) if (item.ob) item.Z = item.ob.Z;
|
|
891
|
-
S.order.sort((a, b) => (b.Z - a.Z) || 0);
|
|
1661
|
+
return I.sourcesNow(spec, S.dz, from);
|
|
892
1662
|
}
|
|
893
1663
|
|
|
1664
|
+
// the scene and the camera this frame (what repaints, painting parts, go by)
|
|
1665
|
+
let SCENE = null, CAM = null;
|
|
894
1666
|
function frame(S, spec, cam, t, dt, settled, goal) {
|
|
895
1667
|
frameNo++;
|
|
1668
|
+
SCENE = S; CAM = cam;
|
|
896
1669
|
BYID = S.byId ??= new Map(S.objects.filter(x => x.o.id != null).map(x => [x.o.id, x]));
|
|
897
|
-
placeMoving(S);
|
|
1670
|
+
const moved = placeMoving(S), parted = S.objects.some(ob => ob.parts);
|
|
1671
|
+
if (parted) prepareParts(S, cam, t);
|
|
1672
|
+
if (moved || parted) S.order = orderOf(S);
|
|
1673
|
+
// the land hiding what stands behind its rises: for the camera as it is (kept while it stands)
|
|
1674
|
+
const key = I.land() && `${cam.x}|${cam.y}|${cam.z}|${cam.h}|${cam.yaw}|${W}|${H}`;
|
|
1675
|
+
landCells(S, cam);
|
|
1676
|
+
if (key !== S.landKey) { S.landKey = key; S.land = key ? I.landLine([VX0, VY0, VX1, VY1], S.lands.flatMap(ob => ob.cells)) : null; }
|
|
1677
|
+
LAND = S.land;
|
|
1678
|
+
const look = S.look = I.lookOf(spec);
|
|
1679
|
+
HAZE = look.hazeReach; FOG = look.hazeColor.map(v => v / 255);
|
|
1680
|
+
// lit as much as the light is now (engine.js: lookOf): its programs and its three targets (made
|
|
1681
|
+
// when it is first lit; a browser that can't write depths shows it unlit)
|
|
1682
|
+
S.lit = canLight ? look.lit : 0;
|
|
1683
|
+
P = S.lit ? (LITP ??= programs(true)) : PLAIN;
|
|
1684
|
+
S.scene = S.lit ? (S.litScene ??= litTarget(CW, CH)) : S.plain;
|
|
1685
|
+
S.sources = sourcesOf(S, spec);
|
|
1686
|
+
// (its ambient level: how much a shadow takes away is the source's share of the light)
|
|
1687
|
+
S.ambient = I.ambientOf(look, S.sources).reduce((a, b) => a + b) / 3;
|
|
1688
|
+
const mSky = camMatrix(cam, Infinity);
|
|
1689
|
+
mirrors(S, spec, cam, t, mSky);
|
|
898
1690
|
gl.bindFramebuffer(gl.FRAMEBUFFER, S.scene.fbo);
|
|
899
1691
|
gl.viewport(0, 0, CW, CH);
|
|
900
|
-
const mSky = camMatrix(cam, Infinity);
|
|
901
1692
|
gl.clearColor(0, 0, 0, 0); // what nothing paints stays clear: the final pass fills it
|
|
902
1693
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
903
1694
|
if (S.sky) rect(S.sky, mSky, S.x0, -H, S.span, 3 * H, WHITE, 1, false, [0, -1, 1, 2]);
|
|
@@ -907,6 +1698,37 @@ const MooodGL = (() => {
|
|
|
907
1698
|
particlesAndPost(S, spec, cam, t, dt, goal);
|
|
908
1699
|
}
|
|
909
1700
|
|
|
1701
|
+
// A frame unseen (engine.js: stage.draw): the scene goes on as if it were drawn — what moves
|
|
1702
|
+
// through the world is put where it is now, what is live is painted (a module's springs and steps
|
|
1703
|
+
// go on, its lights and the point it marks are where they are), particles move, the sharp tiles
|
|
1704
|
+
// for where the camera is are painted ahead — and nothing is drawn: the GPU does nothing. Live
|
|
1705
|
+
// elements are painted coarsely, as what is out of sight is — or, `painted`, each as a drawn frame
|
|
1706
|
+
// paints it (as finely as it is seen: code whose memory depends on how big it is painted goes on
|
|
1707
|
+
// exactly as when drawn).
|
|
1708
|
+
function tick(S, spec, cam, t, dt, goal, painted) {
|
|
1709
|
+
frameNo++;
|
|
1710
|
+
SCENE = S; CAM = cam;
|
|
1711
|
+
BYID = S.byId ??= new Map(S.objects.filter(x => x.o.id != null).map(x => [x.o.id, x]));
|
|
1712
|
+
// as a frame does (frame, above): what moves placed, the parts where they stand (painted with
|
|
1713
|
+
// their element), the order, the light's sources (what reads e.lights is lit as when drawn)
|
|
1714
|
+
const moved = placeMoving(S), parted = S.objects.some(ob => ob.parts);
|
|
1715
|
+
if (parted) {
|
|
1716
|
+
if (painted) prepareParts(S, cam, t);
|
|
1717
|
+
else for (const ob of S.objects) if (ob.parts) { if (ob.live) unseen(ob, t); for (const part of ob.parts) placePart(S, part); }
|
|
1718
|
+
}
|
|
1719
|
+
if (moved || parted) S.order = orderOf(S);
|
|
1720
|
+
S.sources = sourcesOf(S, spec);
|
|
1721
|
+
for (const item of S.order) if (item.ob?.live) { if (painted) drawObjectAt(item.ob, cam, t, false); else unseen(item.ob, t); }
|
|
1722
|
+
const FM = [1, 0, 0, 1, (W - SW) / 2, 0];
|
|
1723
|
+
for (const ob of S.inserts) if (I.opening(ob.o) >= 0.002) { if (painted) textureOf(ob, FM, t); else unseen(ob, t); }
|
|
1724
|
+
for (const { p, list } of S.particles) {
|
|
1725
|
+
const wind = (p.wind ?? 0) * 60 * S.u;
|
|
1726
|
+
for (const q of list) I.moveParticle(p.kind, q, CW, CH, t, dt, S.u, wind);
|
|
1727
|
+
}
|
|
1728
|
+
refine(S, cam, goal || cam);
|
|
1729
|
+
evict(S);
|
|
1730
|
+
}
|
|
1731
|
+
|
|
910
1732
|
// looking around: the sky, then what stands about, far to near as seen now — planes by the
|
|
911
1733
|
// farthest reach of them
|
|
912
1734
|
function drawAround(S, cam, t) {
|
|
@@ -923,12 +1745,18 @@ const MooodGL = (() => {
|
|
|
923
1745
|
const half = typeof ob.o.facing === 'number' ? ob.bw / 2 / (H / ob.p.Zp) : 0;
|
|
924
1746
|
items.push({ ob, d: Math.hypot(ob.p.X - cam.x, ob.p.Z - cam.z) + half });
|
|
925
1747
|
}
|
|
1748
|
+
for (const part of ob.parts || []) items.push({ part, d: Math.hypot(part.p.X - cam.x, part.p.Z - cam.z) });
|
|
926
1749
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1750
|
+
for (const a of I.airOf(S.sources, S.look, h => Math.hypot(h.at[0] - cam.x, h.at[2] - cam.z))) items.push({ air: a, d: a.Z });
|
|
1751
|
+
I.arrange(items.sort((a, b) => b.d - a.d), S.rules, idOf);
|
|
1752
|
+
for (const it of items) {
|
|
1753
|
+
if (it.air) { halo(S, it.air, cam, t); continue; }
|
|
1754
|
+
const p = (it.ob || it.part).p;
|
|
1755
|
+
castOn(S, 'ground', it, cam, t);
|
|
1756
|
+
HZ = it.ob?.plane ? NOHAZE : haze(p.Zp, Math.hypot(p.X - cam.x, p.Z - cam.z));
|
|
1757
|
+
drawItem(S, it, cam, t, items);
|
|
931
1758
|
HZ = NOHAZE;
|
|
1759
|
+
if (it.ob) castOn(S, it.ob, null, cam, t);
|
|
932
1760
|
}
|
|
933
1761
|
}
|
|
934
1762
|
|
|
@@ -939,8 +1767,17 @@ const MooodGL = (() => {
|
|
|
939
1767
|
}
|
|
940
1768
|
for (const c of S.celestials) rect(c.tex, mSky, c.x, c.y, c.size, c.size);
|
|
941
1769
|
|
|
1770
|
+
// the light in the air (the halos of the scene's own lights, the haze round its sources), each
|
|
1771
|
+
// at its depth among the rest
|
|
1772
|
+
const halos = I.airOf(S.sources, S.look, h => h.at[2]);
|
|
942
1773
|
for (const item of S.order) {
|
|
943
|
-
|
|
1774
|
+
while (halos.length && halos[0].Z >= item.Z) halo(S, halos.shift(), cam, t);
|
|
1775
|
+
if (!item.l) {
|
|
1776
|
+
castOn(S, 'ground', item, cam, t);
|
|
1777
|
+
HZ = isFinite(item.Z) ? haze(item.Z, item.Z - cam.z) : NOHAZE; drawItem(S, item, cam, t, S.order); HZ = NOHAZE;
|
|
1778
|
+
if (item.ob) castOn(S, item.ob, null, cam, t);
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
944
1781
|
const l = item.l, L = l.L;
|
|
945
1782
|
if (l.Z - cam.z < 0.05) continue; // walked past it
|
|
946
1783
|
const m = camMatrix(cam, l.Z);
|
|
@@ -948,6 +1785,7 @@ const MooodGL = (() => {
|
|
|
948
1785
|
drawWater(S, l, Math.round(m[5] + m[3] * L.y * H), t);
|
|
949
1786
|
continue;
|
|
950
1787
|
}
|
|
1788
|
+
LT = [1, l.Z - cam.z]; MD = l.Z - cam.z;
|
|
951
1789
|
if (l.tex) {
|
|
952
1790
|
HZ = haze(l.Z, l.Z - cam.z);
|
|
953
1791
|
drawLayer(l, m, cam);
|
|
@@ -961,12 +1799,15 @@ const MooodGL = (() => {
|
|
|
961
1799
|
rect(T.glow, m, cx - b.rx, L.y * H + b.dy - b.ry, 2 * b.rx, 2 * b.ry, l.c, b.a * 2.2);
|
|
962
1800
|
}
|
|
963
1801
|
}
|
|
1802
|
+
LT = UNLIT;
|
|
964
1803
|
}
|
|
965
|
-
|
|
1804
|
+
for (const a of halos) halo(S, a, cam, t);
|
|
966
1805
|
}
|
|
967
1806
|
|
|
968
1807
|
function particlesAndPost(S, spec, cam, t, dt, goal) {
|
|
969
|
-
|
|
1808
|
+
// particles live in the canvas's pixels; in a lit scene a few metres off
|
|
1809
|
+
const u = S.u, PM = [1, 0, 0, 1, VX0, VY0];
|
|
1810
|
+
LT = [1, 3];
|
|
970
1811
|
for (const { p, c, list } of S.particles) {
|
|
971
1812
|
const wind = (p.wind ?? 0) * 60 * u;
|
|
972
1813
|
for (const q of list) {
|
|
@@ -987,36 +1828,71 @@ const MooodGL = (() => {
|
|
|
987
1828
|
}
|
|
988
1829
|
}
|
|
989
1830
|
}
|
|
1831
|
+
LT = UNLIT;
|
|
1832
|
+
// inserts over it all, fixed to the frame
|
|
1833
|
+
const FM = [1, 0, 0, 1, (W - SW) / 2, 0];
|
|
1834
|
+
for (const ob of S.inserts) {
|
|
1835
|
+
if (I.opening(ob.o) < 0.002) continue;
|
|
1836
|
+
const { tx, r } = textureOf(ob, FM, t);
|
|
1837
|
+
rect(tx, FM, r.x, r.y, r.w, r.h);
|
|
1838
|
+
}
|
|
990
1839
|
flush();
|
|
991
1840
|
|
|
992
1841
|
// final pass to the screen, the whole canvas: the frame at its place in it (S.out)
|
|
993
1842
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
994
1843
|
gl.viewport(0, 0, CW, CH);
|
|
995
|
-
const lt = spec
|
|
996
|
-
gl.useProgram(pprog);
|
|
1844
|
+
const lt = I.lookOf(spec);
|
|
1845
|
+
gl.useProgram(P.pprog);
|
|
997
1846
|
gl.activeTexture(gl.TEXTURE0);
|
|
998
1847
|
gl.bindTexture(gl.TEXTURE_2D, S.scene.tex);
|
|
999
|
-
gl.uniform1i(pu.scene, 0);
|
|
1000
|
-
gl.uniform2f(pu.res, W, H);
|
|
1001
|
-
gl.uniform2f(pu.at, OX, CH - OY - H);
|
|
1002
|
-
gl.uniform2f(pu.canvas, CW, CH);
|
|
1003
|
-
gl.uniform2f(pu.off, OX, OY);
|
|
1004
|
-
gl.uniform3f(pu.tint, ...(lt.tint ?
|
|
1005
|
-
gl.uniform1f(pu.tintA, lt.tint ? I.clamp(lt.tintStrength
|
|
1006
|
-
gl.uniform1f(pu.vig, 0.75 * I.clamp(lt.vignette
|
|
1007
|
-
gl.uniform1f(pu.grain, 0.1 * I.clamp(lt.grain
|
|
1008
|
-
gl.uniform1f(pu.
|
|
1848
|
+
gl.uniform1i(P.pu.scene, 0);
|
|
1849
|
+
gl.uniform2f(P.pu.res, W, H);
|
|
1850
|
+
gl.uniform2f(P.pu.at, OX, CH - OY - H);
|
|
1851
|
+
gl.uniform2f(P.pu.canvas, CW, CH);
|
|
1852
|
+
gl.uniform2f(P.pu.off, OX, OY);
|
|
1853
|
+
gl.uniform3f(P.pu.tint, ...(lt.tint ? lt.tint.map(v => v / 255) : WHITE));
|
|
1854
|
+
gl.uniform1f(P.pu.tintA, lt.tint ? I.clamp(lt.tintStrength, 0, 0.5) : 0);
|
|
1855
|
+
gl.uniform1f(P.pu.vig, 0.75 * I.clamp(lt.vignette, 0, 1));
|
|
1856
|
+
gl.uniform1f(P.pu.grain, 0.1 * I.clamp(lt.grain, 0, 1));
|
|
1857
|
+
gl.uniform1f(P.pu.bright, Math.max(0, lt.brightness));
|
|
1858
|
+
gl.uniform1f(P.pu.sat, Math.max(0, lt.saturation));
|
|
1859
|
+
gl.uniform1f(P.pu.seed, Math.floor(Math.random() * 4096));
|
|
1860
|
+
if (S.lit) lightUniforms(S, lt, cam, t);
|
|
1009
1861
|
// a film's cut: the frame fades to a colour (state: fade 0…1, fade_color — black by default)
|
|
1010
1862
|
const sv = I.STATE.values;
|
|
1011
|
-
gl.uniform1f(pu.fade, typeof sv.fade === 'number' ? I.clamp(sv.fade, 0, 1) : 0);
|
|
1012
|
-
gl.uniform3f(pu.fadeCol, ...(typeof sv.fade_color === 'string' ? col(sv.fade_color) : [0, 0, 0]));
|
|
1013
|
-
band(pprog, VY0, VY1);
|
|
1863
|
+
gl.uniform1f(P.pu.fade, typeof sv.fade === 'number' ? I.clamp(sv.fade, 0, 1) : 0);
|
|
1864
|
+
gl.uniform3f(P.pu.fadeCol, ...(typeof sv.fade_color === 'string' ? col(sv.fade_color) : [0, 0, 0]));
|
|
1865
|
+
band(P.pprog, VY0, VY1);
|
|
1014
1866
|
|
|
1015
1867
|
refine(S, cam, goal || cam);
|
|
1016
1868
|
evict(S);
|
|
1017
1869
|
}
|
|
1018
1870
|
|
|
1019
|
-
|
|
1871
|
+
// A lit scene's light for the final pass: the ambient (engine.js: ambientOf), and the LIGHTS point
|
|
1872
|
+
// sources that light most of what is seen from here — the strongest, by how near the camera their
|
|
1873
|
+
// reach comes (one far off lights little of the frame)
|
|
1874
|
+
function lightUniforms(S, lt, cam, t) {
|
|
1875
|
+
const amb = I.ambientOf(lt, S.sources), points = S.sources.filter(s => !s.dir && s.strength > 0.002);
|
|
1876
|
+
const weight = s => s.strength * s.reach / Math.max(s.reach, Math.hypot(s.at[0] - cam.x, s.at[1] - (cam.h ?? 1), s.at[2] - cam.z));
|
|
1877
|
+
points.sort((a, b) => weight(b) - weight(a)).length = Math.min(points.length, LIGHTS);
|
|
1878
|
+
const lp = new Float32Array(4 * LIGHTS), lc = new Float32Array(3 * LIGHTS);
|
|
1879
|
+
points.forEach((s, i) => {
|
|
1880
|
+
const f = s.sprite ? flickerOf(s.sprite.lt, s.sprite.x, s.sprite.y, t) : I.flickerNow(s, t);
|
|
1881
|
+
lp.set([...s.at, s.reach], 4 * i);
|
|
1882
|
+
lc.set(s.rgb.map(v => v / 255 * s.strength * f * lt.lit), 3 * i);
|
|
1883
|
+
});
|
|
1884
|
+
gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, S.scene.lit); gl.uniform1i(P.pu.lit, 1);
|
|
1885
|
+
gl.activeTexture(gl.TEXTURE2); gl.bindTexture(gl.TEXTURE_2D, S.scene.glow); gl.uniform1i(P.pu.glow, 2);
|
|
1886
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1887
|
+
gl.uniform3f(P.pu.amb, ...amb);
|
|
1888
|
+
gl.uniform1i(P.pu.n, points.length);
|
|
1889
|
+
gl.uniform4fv(P.pu.lp, lp);
|
|
1890
|
+
gl.uniform3fv(P.pu.lc, lc);
|
|
1891
|
+
gl.uniform4f(P.pu.cam, cam.x, cam.z, cam.h ?? 1, cam.yaw || 0);
|
|
1892
|
+
gl.uniform2f(P.pu.view, cam.y, YH);
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
return { name: 'webgl', fpsIdle: 60, pixelBudget: Infinity, resMax: 2, build, frame, tick };
|
|
1020
1896
|
}
|
|
1021
1897
|
|
|
1022
1898
|
return { create };
|