@godot-scene-web/canvas-effects 0.1.0
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/LICENSE +21 -0
- package/dist/webgl.d.mts +409 -0
- package/dist/webgl.d.mts.map +1 -0
- package/dist/webgl.mjs +2298 -0
- package/dist/webgl.mjs.map +1 -0
- package/dist/webgpu.d.mts +335 -0
- package/dist/webgpu.d.mts.map +1 -0
- package/dist/webgpu.mjs +1349 -0
- package/dist/webgpu.mjs.map +1 -0
- package/package.json +44 -0
package/dist/webgl.mjs
ADDED
|
@@ -0,0 +1,2298 @@
|
|
|
1
|
+
import { INSTANCE_STRIDE, InstanceBuffer, createParticleState, normalizeParticleRenderConfig, packParticleInstances, preprocessParticles, simulateParticles } from "@godot-scene-web/effects/particles";
|
|
2
|
+
import { UnsupportedShaderError, transpileGodotShader } from "@godot-scene-web/effects/shaders";
|
|
3
|
+
//#region src/particle-webgl.ts
|
|
4
|
+
function compileProgram$2(gl, vertexSource, fragmentSource) {
|
|
5
|
+
const vertex = gl.createShader(gl.VERTEX_SHADER);
|
|
6
|
+
const fragment = gl.createShader(gl.FRAGMENT_SHADER);
|
|
7
|
+
if (!vertex || !fragment) {
|
|
8
|
+
if (vertex) gl.deleteShader(vertex);
|
|
9
|
+
if (fragment) gl.deleteShader(fragment);
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
gl.shaderSource(vertex, vertexSource);
|
|
13
|
+
gl.shaderSource(fragment, fragmentSource);
|
|
14
|
+
gl.compileShader(vertex);
|
|
15
|
+
gl.compileShader(fragment);
|
|
16
|
+
if (!gl.getShaderParameter(vertex, gl.COMPILE_STATUS) || !gl.getShaderParameter(fragment, gl.COMPILE_STATUS)) {
|
|
17
|
+
gl.deleteShader(vertex);
|
|
18
|
+
gl.deleteShader(fragment);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const program = gl.createProgram();
|
|
22
|
+
if (!program) {
|
|
23
|
+
gl.deleteShader(vertex);
|
|
24
|
+
gl.deleteShader(fragment);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
gl.attachShader(program, vertex);
|
|
28
|
+
gl.attachShader(program, fragment);
|
|
29
|
+
gl.linkProgram(program);
|
|
30
|
+
gl.deleteShader(vertex);
|
|
31
|
+
gl.deleteShader(fragment);
|
|
32
|
+
if (gl.getProgramParameter(program, gl.LINK_STATUS)) return program;
|
|
33
|
+
gl.deleteProgram(program);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
/** GPU residency is deliberately external to the portable instance data. */
|
|
37
|
+
const instanceAllocations = /* @__PURE__ */ new WeakMap();
|
|
38
|
+
function allocationsFor(gl) {
|
|
39
|
+
let allocations = instanceAllocations.get(gl);
|
|
40
|
+
if (!allocations) {
|
|
41
|
+
allocations = /* @__PURE__ */ new WeakMap();
|
|
42
|
+
instanceAllocations.set(gl, allocations);
|
|
43
|
+
}
|
|
44
|
+
return allocations;
|
|
45
|
+
}
|
|
46
|
+
function uploadInstances(gl, instances) {
|
|
47
|
+
const allocations = allocationsFor(gl);
|
|
48
|
+
let allocation = allocations.get(instances);
|
|
49
|
+
if (!allocation) {
|
|
50
|
+
const buffer = gl.createBuffer();
|
|
51
|
+
if (!buffer) return null;
|
|
52
|
+
allocation = {
|
|
53
|
+
buffer,
|
|
54
|
+
bytes: 0
|
|
55
|
+
};
|
|
56
|
+
allocations.set(instances, allocation);
|
|
57
|
+
}
|
|
58
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, allocation.buffer);
|
|
59
|
+
if (instances.data.byteLength > allocation.bytes) {
|
|
60
|
+
gl.bufferData(gl.ARRAY_BUFFER, instances.data, gl.DYNAMIC_DRAW);
|
|
61
|
+
allocation.bytes = instances.data.byteLength;
|
|
62
|
+
} else gl.bufferSubData(gl.ARRAY_BUFFER, 0, instances.data, 0, instances.count * INSTANCE_STRIDE);
|
|
63
|
+
return allocation.buffer;
|
|
64
|
+
}
|
|
65
|
+
/** Release one portable buffer's WebGL allocation on its owning borrowed context. */
|
|
66
|
+
function disposeParticleInstanceBuffer(gl, instances) {
|
|
67
|
+
const allocations = instanceAllocations.get(gl);
|
|
68
|
+
const allocation = allocations?.get(instances);
|
|
69
|
+
if (!allocation) return;
|
|
70
|
+
gl.deleteBuffer(allocation.buffer);
|
|
71
|
+
allocations?.delete(instances);
|
|
72
|
+
}
|
|
73
|
+
/** Context-loss path: forget driver names without issuing GL commands. */
|
|
74
|
+
function invalidateParticleInstanceBuffer(gl, instances) {
|
|
75
|
+
instanceAllocations.get(gl)?.delete(instances);
|
|
76
|
+
}
|
|
77
|
+
const VERTEX_SRC = `#version 300 es
|
|
78
|
+
layout(location = 0) in vec2 a_corner; // unit quad corner, [-0.5, 0.5]
|
|
79
|
+
layout(location = 1) in vec2 a_center; // particle center, device px
|
|
80
|
+
layout(location = 2) in vec2 a_scale; // sprite size, device px (base * scale)
|
|
81
|
+
layout(location = 3) in float a_rotation;
|
|
82
|
+
layout(location = 4) in vec4 a_color;
|
|
83
|
+
layout(location = 5) in float a_frame;
|
|
84
|
+
uniform vec2 u_viewport; // canvas size, device px
|
|
85
|
+
uniform float u_hframes;
|
|
86
|
+
uniform float u_vframes;
|
|
87
|
+
out vec2 v_uv;
|
|
88
|
+
out vec2 v_quad;
|
|
89
|
+
out vec2 v_cell;
|
|
90
|
+
out vec4 v_color;
|
|
91
|
+
void main() {
|
|
92
|
+
float c = cos(a_rotation);
|
|
93
|
+
float s = sin(a_rotation);
|
|
94
|
+
vec2 rotated = vec2(a_corner.x * c - a_corner.y * s, a_corner.x * s + a_corner.y * c);
|
|
95
|
+
vec2 px = a_center + rotated * a_scale;
|
|
96
|
+
vec2 clip = (px / u_viewport) * 2.0 - 1.0;
|
|
97
|
+
clip.y = -clip.y; // canvas Y-down -> clip Y-up
|
|
98
|
+
gl_Position = vec4(clip, 0.0, 1.0);
|
|
99
|
+
vec2 uv01 = a_corner + 0.5; // 0..1 across the SPRITE quad
|
|
100
|
+
vec2 cell = vec2(mod(a_frame, u_hframes), floor(a_frame / u_hframes));
|
|
101
|
+
v_uv = (cell + uv01) / vec2(u_hframes, u_vframes);
|
|
102
|
+
// The flipbook cell INDEX, so a fragment that re-derives its own sprite-local UV (the polar remap) can map
|
|
103
|
+
// it back into the same cell instead of over the whole sheet.
|
|
104
|
+
v_cell = cell;
|
|
105
|
+
// Quad-local 0..1, INDEPENDENT of the flipbook grid: the untextured dot is drawn from this.
|
|
106
|
+
// Deriving it from the atlas-mapped v_uv put the dot's center at the SHEET's center, so any
|
|
107
|
+
// grid > 1x1 left the fallback dot off-center and clipped to a sliver of one cell.
|
|
108
|
+
v_quad = uv01;
|
|
109
|
+
v_color = a_color;
|
|
110
|
+
}`;
|
|
111
|
+
const FRAGMENT_SRC = `#version 300 es
|
|
112
|
+
precision mediump float;
|
|
113
|
+
uniform sampler2D u_texture;
|
|
114
|
+
uniform sampler2D u_lutTex; // per-TEXEL color LUT, 1px tall (see u_lut)
|
|
115
|
+
uniform sampler2D u_maskTex; // quad-shaped coverage mask, unit 2 (see u_mask)
|
|
116
|
+
// The flipbook grid, ALSO declared in the vertex stage: a uniform shared across stages must match in type AND
|
|
117
|
+
// PRECISION, and the vertex stage's float default is highp while this one's is mediump — declaring these as a
|
|
118
|
+
// plain float here fails to LINK (silently: no particle program, so no particle canvases at all).
|
|
119
|
+
uniform highp float u_hframes;
|
|
120
|
+
uniform highp float u_vframes;
|
|
121
|
+
uniform int u_textured;
|
|
122
|
+
uniform int u_lut; // 1 = recolor through the LUT, indexed by the source RED channel
|
|
123
|
+
uniform int u_additive; // 1 = additive: emit the particle's LIGHT (Godot ADD: src.rgb * src.a)
|
|
124
|
+
uniform int u_alphaFromRed; // 1 = coverage comes from the source RED channel, not its alpha
|
|
125
|
+
uniform int u_erode; // 1 = apply the constant-erosion smoothstep below
|
|
126
|
+
uniform vec2 u_erodeFactors; // (threshold, softness) for that smoothstep
|
|
127
|
+
uniform int u_mask; // 1 = multiply coverage by u_maskTex's red, sampled over the QUAD
|
|
128
|
+
uniform int u_uvPolar; // 1 = sample the sheet through Godot's polar_coordinates remap
|
|
129
|
+
in vec2 v_uv;
|
|
130
|
+
in vec2 v_quad;
|
|
131
|
+
in vec2 v_cell;
|
|
132
|
+
in vec4 v_color;
|
|
133
|
+
out vec4 fragColor;
|
|
134
|
+
void main() {
|
|
135
|
+
vec2 uv = v_uv;
|
|
136
|
+
if (u_uvPolar == 1) {
|
|
137
|
+
// Godot polar_coordinates(UV, vec2(0.5), 1, 1) (shaders/vfx/_util/polar_coordinates.gdshaderinc):
|
|
138
|
+
// x = radius from the sprite center (0..1.41 at the corners), y = the angle mapped to 0..1, both wrapped.
|
|
139
|
+
// A radial sheet (common_ring_polar_a) is a RING only through this; sampled flat it is a vertical BAR.
|
|
140
|
+
vec2 dir = v_quad - 0.5;
|
|
141
|
+
float radius = length(dir) * 2.0;
|
|
142
|
+
float angle = atan(dir.y, dir.x) * (1.0 / (3.1416 * 2.0));
|
|
143
|
+
uv = (v_cell + mod(vec2(radius, angle), 1.0)) / vec2(u_hframes, u_vframes);
|
|
144
|
+
}
|
|
145
|
+
vec4 tex;
|
|
146
|
+
if (u_textured == 1) {
|
|
147
|
+
tex = texture(u_texture, uv);
|
|
148
|
+
} else {
|
|
149
|
+
// Soft round dot when the system has no texture — measured across the QUAD, not the
|
|
150
|
+
// atlas-mapped UV, so it stays centered whatever the (meaningless, textureless) grid is.
|
|
151
|
+
float r = length(v_quad - 0.5) * 2.0;
|
|
152
|
+
tex = vec4(1.0, 1.0, 1.0, 1.0 - smoothstep(0.7, 1.0, r));
|
|
153
|
+
}
|
|
154
|
+
// COVERAGE — taken PRE-LUT, because the LUT is a color lookup INDEXED by that same red channel: reading it
|
|
155
|
+
// afterwards would sample the LUT's own (usually white) output instead of the sheet's shape. STS2's
|
|
156
|
+
// grayscale VFX sheets are alpha-less PNGs, so tex.a is 1.0 everywhere and the alpha branch draws a SQUARE.
|
|
157
|
+
float coverage = u_alphaFromRed == 1 ? tex.r : tex.a;
|
|
158
|
+
if (u_lut == 1) {
|
|
159
|
+
// Godot's VFX particle-shader family: COLOR = vec4(texture(lut, texture_color.rr).rgb,
|
|
160
|
+
// alpha) * vertex_color. The sprite sheet is a single-channel MASK, so its own RGB is
|
|
161
|
+
// meaningless (it reads as a red/orange block); the LUT holds the real colors. Sampled
|
|
162
|
+
// AFTER the texture/dot resolve so both branches are recolored, and the source ALPHA is
|
|
163
|
+
// preserved untouched — only RGB comes from the LUT.
|
|
164
|
+
tex = vec4(texture(u_lutTex, vec2(tex.r, 0.5)).rgb, tex.a);
|
|
165
|
+
}
|
|
166
|
+
if (u_erode == 1) {
|
|
167
|
+
// Godot erosion_from_factors(vec2(threshold, softness), coverage) — a CONSTANT erosion curve, i.e. the
|
|
168
|
+
// threshold does not sweep over the particle's life (see ParticleSpecConfig.alphaErode).
|
|
169
|
+
coverage = smoothstep(u_erodeFactors.x, u_erodeFactors.x + u_erodeFactors.y, coverage);
|
|
170
|
+
}
|
|
171
|
+
if (u_mask == 1) {
|
|
172
|
+
// Godot's mask sampler reads the sprite's own UV, NOT the flipbook cell — it shapes the whole quad.
|
|
173
|
+
coverage *= texture(u_maskTex, v_quad).r;
|
|
174
|
+
}
|
|
175
|
+
tex.a = coverage;
|
|
176
|
+
vec4 col = tex * v_color;
|
|
177
|
+
if (u_additive == 1) {
|
|
178
|
+
// Additive sprites contribute light = color x alpha (Godot BLEND_MODE_ADD adds
|
|
179
|
+
// src.rgb * src.a to the framebuffer, nothing where light is 0 -- so an opaque-black
|
|
180
|
+
// glow background or an alpha-shaped sprite's transparent area both add nothing).
|
|
181
|
+
// Emit that light RAW and SUM it across particles (blendFunc ONE, ONE into the
|
|
182
|
+
// accumulator FBO); the resolve pass then derives the per-pixel TOTAL's peak-channel
|
|
183
|
+
// coverage ONCE and presents the pair premultiplied. Normalizing per PARTICLE here
|
|
184
|
+
// (the old path) amplified every faint texel to full brightness and let overlaps
|
|
185
|
+
// clamp to white while stacking coverage -- a subtle 5-particle fog rendered as an
|
|
186
|
+
// opaque white haze wall.
|
|
187
|
+
fragColor = vec4(col.rgb * tex.a * v_color.a, 0.0);
|
|
188
|
+
} else {
|
|
189
|
+
// PREMULTIPLIED, under blendFactorsFor(0)'s ONE / ONE_MINUS_SRC_ALPHA -- the shared canvas
|
|
190
|
+
// declares premultipliedAlpha: true (../webgl/shared-gl.ts) and this is that contract.
|
|
191
|
+
// Algebraically identical to emitting straight col under SRC_ALPHA / ONE_MINUS_SRC_ALPHA
|
|
192
|
+
// (both land col.rgb*col.a + dst*(1-col.a)), and written THIS way so the fragment and the
|
|
193
|
+
// canvas state the same thing -- the same pairing render-webgpu.ts's fs_particles uses, since
|
|
194
|
+
// a GPUCanvasContext has no straight-alpha mode to differ with.
|
|
195
|
+
fragColor = vec4(col.rgb * col.a, col.a);
|
|
196
|
+
}
|
|
197
|
+
}`;
|
|
198
|
+
const RESOLVE_VERTEX_SRC = `#version 300 es
|
|
199
|
+
layout(location = 0) in vec2 a_pos; // full-screen clip-space quad (shared-gl quad buffer)
|
|
200
|
+
void main() {
|
|
201
|
+
gl_Position = vec4(a_pos, 0.0, 1.0);
|
|
202
|
+
}`;
|
|
203
|
+
const RESOLVE_FRAGMENT_SRC = `#version 300 es
|
|
204
|
+
precision mediump float;
|
|
205
|
+
uniform sampler2D u_accum;
|
|
206
|
+
out vec4 fragColor;
|
|
207
|
+
void main() {
|
|
208
|
+
// Same viewport rect + origin as the accumulate pass, so fragment coords match texels.
|
|
209
|
+
vec3 light = texelFetch(u_accum, ivec2(gl_FragCoord.xy), 0).rgb;
|
|
210
|
+
float cov = max(max(light.r, light.g), light.b);
|
|
211
|
+
fragColor = vec4(light, cov);
|
|
212
|
+
}`;
|
|
213
|
+
const cached = /* @__PURE__ */ new WeakMap();
|
|
214
|
+
function getParticleProgram(gl) {
|
|
215
|
+
const existing = cached.get(gl);
|
|
216
|
+
if (existing) {
|
|
217
|
+
existing.pinned = true;
|
|
218
|
+
return existing.program;
|
|
219
|
+
}
|
|
220
|
+
const program = compileProgram$2(gl, VERTEX_SRC, FRAGMENT_SRC);
|
|
221
|
+
const resolveProgram = compileProgram$2(gl, RESOLVE_VERTEX_SRC, RESOLVE_FRAGMENT_SRC);
|
|
222
|
+
if (!program || !resolveProgram) {
|
|
223
|
+
if (program) gl.deleteProgram(program);
|
|
224
|
+
if (resolveProgram) gl.deleteProgram(resolveProgram);
|
|
225
|
+
cached.set(gl, {
|
|
226
|
+
program: null,
|
|
227
|
+
refs: 0,
|
|
228
|
+
pinned: true
|
|
229
|
+
});
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const cornerBuffer = gl.createBuffer();
|
|
233
|
+
if (!cornerBuffer) {
|
|
234
|
+
gl.deleteProgram(program);
|
|
235
|
+
gl.deleteProgram(resolveProgram);
|
|
236
|
+
cached.set(gl, {
|
|
237
|
+
program: null,
|
|
238
|
+
refs: 0,
|
|
239
|
+
pinned: true
|
|
240
|
+
});
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, cornerBuffer);
|
|
244
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
|
245
|
+
-.5,
|
|
246
|
+
-.5,
|
|
247
|
+
.5,
|
|
248
|
+
-.5,
|
|
249
|
+
-.5,
|
|
250
|
+
.5,
|
|
251
|
+
.5,
|
|
252
|
+
.5
|
|
253
|
+
]), gl.STATIC_DRAW);
|
|
254
|
+
const result = {
|
|
255
|
+
program,
|
|
256
|
+
cornerBuffer,
|
|
257
|
+
uViewport: gl.getUniformLocation(program, "u_viewport"),
|
|
258
|
+
uHframes: gl.getUniformLocation(program, "u_hframes"),
|
|
259
|
+
uVframes: gl.getUniformLocation(program, "u_vframes"),
|
|
260
|
+
uTexture: gl.getUniformLocation(program, "u_texture"),
|
|
261
|
+
uLutTex: gl.getUniformLocation(program, "u_lutTex"),
|
|
262
|
+
uMaskTex: gl.getUniformLocation(program, "u_maskTex"),
|
|
263
|
+
uTextured: gl.getUniformLocation(program, "u_textured"),
|
|
264
|
+
uLut: gl.getUniformLocation(program, "u_lut"),
|
|
265
|
+
uAdditive: gl.getUniformLocation(program, "u_additive"),
|
|
266
|
+
uAlphaFromRed: gl.getUniformLocation(program, "u_alphaFromRed"),
|
|
267
|
+
uErode: gl.getUniformLocation(program, "u_erode"),
|
|
268
|
+
uErodeFactors: gl.getUniformLocation(program, "u_erodeFactors"),
|
|
269
|
+
uMask: gl.getUniformLocation(program, "u_mask"),
|
|
270
|
+
uUvPolar: gl.getUniformLocation(program, "u_uvPolar"),
|
|
271
|
+
resolveProgram,
|
|
272
|
+
uAccum: gl.getUniformLocation(resolveProgram, "u_accum"),
|
|
273
|
+
accum: null
|
|
274
|
+
};
|
|
275
|
+
cached.set(gl, {
|
|
276
|
+
program: result,
|
|
277
|
+
refs: 0,
|
|
278
|
+
pinned: true
|
|
279
|
+
});
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
/** Acquire a shared program lease for a producer that will later release it. */
|
|
283
|
+
function acquireParticleProgram(gl) {
|
|
284
|
+
const entry = cached.get(gl);
|
|
285
|
+
if (entry) {
|
|
286
|
+
if (!entry.program) return null;
|
|
287
|
+
entry.refs += 1;
|
|
288
|
+
return entry.program;
|
|
289
|
+
}
|
|
290
|
+
const program = getParticleProgram(gl);
|
|
291
|
+
const created = cached.get(gl);
|
|
292
|
+
created.pinned = false;
|
|
293
|
+
created.refs = 1;
|
|
294
|
+
return program;
|
|
295
|
+
}
|
|
296
|
+
function destroyCachedProgram(gl, program) {
|
|
297
|
+
gl.deleteProgram(program.program);
|
|
298
|
+
gl.deleteProgram(program.resolveProgram);
|
|
299
|
+
gl.deleteBuffer(program.cornerBuffer);
|
|
300
|
+
if (program.accum) {
|
|
301
|
+
gl.deleteFramebuffer(program.accum.fbo);
|
|
302
|
+
gl.deleteTexture(program.accum.texture);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/** Release a producer lease. Pinned HTML consumers keep their shared program alive. */
|
|
306
|
+
function releaseParticleProgram(gl, contextLost = false) {
|
|
307
|
+
const entry = cached.get(gl);
|
|
308
|
+
if (!entry) return;
|
|
309
|
+
entry.refs = Math.max(0, entry.refs - 1);
|
|
310
|
+
if (entry.refs !== 0 || entry.pinned) return;
|
|
311
|
+
cached.delete(gl);
|
|
312
|
+
if (entry.program && !contextLost) destroyCachedProgram(gl, entry.program);
|
|
313
|
+
}
|
|
314
|
+
function ensureAccumTarget(gl, program, w, h) {
|
|
315
|
+
let accum = program.accum;
|
|
316
|
+
if (!accum) {
|
|
317
|
+
const fbo = gl.createFramebuffer();
|
|
318
|
+
const texture = gl.createTexture();
|
|
319
|
+
if (!fbo || !texture) {
|
|
320
|
+
if (fbo) gl.deleteFramebuffer(fbo);
|
|
321
|
+
if (texture) gl.deleteTexture(texture);
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
accum = {
|
|
325
|
+
fbo,
|
|
326
|
+
texture,
|
|
327
|
+
width: 0,
|
|
328
|
+
height: 0
|
|
329
|
+
};
|
|
330
|
+
program.accum = accum;
|
|
331
|
+
}
|
|
332
|
+
if (accum.width < w || accum.height < h) {
|
|
333
|
+
const width = Math.max(accum.width, w);
|
|
334
|
+
const height = Math.max(accum.height, h);
|
|
335
|
+
gl.bindTexture(gl.TEXTURE_2D, accum.texture);
|
|
336
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
337
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
338
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
339
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
340
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
341
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, accum.fbo);
|
|
342
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, accum.texture, 0);
|
|
343
|
+
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
|
|
344
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
345
|
+
gl.deleteFramebuffer(accum.fbo);
|
|
346
|
+
gl.deleteTexture(accum.texture);
|
|
347
|
+
program.accum = null;
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
351
|
+
accum.width = width;
|
|
352
|
+
accum.height = height;
|
|
353
|
+
}
|
|
354
|
+
return accum;
|
|
355
|
+
}
|
|
356
|
+
/** Allocate additive accumulation storage before a caller mutates simulation state. */
|
|
357
|
+
function prepareParticleDraw(gl, program, opts) {
|
|
358
|
+
if (opts.blendMode !== 1) return true;
|
|
359
|
+
return ensureAccumTarget(gl, program, opts.targetW ?? opts.viewportW, opts.targetH ?? opts.viewportH) !== null;
|
|
360
|
+
}
|
|
361
|
+
function blendFactorsFor(blendMode) {
|
|
362
|
+
if (blendMode === 1) return [
|
|
363
|
+
"ONE",
|
|
364
|
+
"ONE",
|
|
365
|
+
"ONE",
|
|
366
|
+
"ONE"
|
|
367
|
+
];
|
|
368
|
+
return [
|
|
369
|
+
"ONE",
|
|
370
|
+
"ONE_MINUS_SRC_ALPHA",
|
|
371
|
+
"ONE",
|
|
372
|
+
"ONE_MINUS_SRC_ALPHA"
|
|
373
|
+
];
|
|
374
|
+
}
|
|
375
|
+
const INSTANCE_ATTRS = [
|
|
376
|
+
{
|
|
377
|
+
loc: 1,
|
|
378
|
+
size: 2,
|
|
379
|
+
offset: 0
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
loc: 2,
|
|
383
|
+
size: 2,
|
|
384
|
+
offset: 2
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
loc: 3,
|
|
388
|
+
size: 1,
|
|
389
|
+
offset: 4
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
loc: 4,
|
|
393
|
+
size: 4,
|
|
394
|
+
offset: 5
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
loc: 5,
|
|
398
|
+
size: 1,
|
|
399
|
+
offset: 9
|
|
400
|
+
}
|
|
401
|
+
];
|
|
402
|
+
function drawParticles(sharedGl, program, buffer, opts) {
|
|
403
|
+
const { gl } = sharedGl;
|
|
404
|
+
if (buffer.count <= 0) return true;
|
|
405
|
+
const instanceGpuBuffer = uploadInstances(gl, buffer);
|
|
406
|
+
if (!instanceGpuBuffer) return false;
|
|
407
|
+
const additive = opts.blendMode === 1;
|
|
408
|
+
const targetW = opts.targetW ?? opts.viewportW;
|
|
409
|
+
const targetH = opts.targetH ?? opts.viewportH;
|
|
410
|
+
if (additive) {
|
|
411
|
+
if (!prepareParticleDraw(gl, program, opts)) return false;
|
|
412
|
+
const accum = program.accum;
|
|
413
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, accum.fbo);
|
|
414
|
+
gl.viewport(0, 0, targetW, targetH);
|
|
415
|
+
gl.clearColor(0, 0, 0, 0);
|
|
416
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
417
|
+
}
|
|
418
|
+
gl.useProgram(program.program);
|
|
419
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, program.cornerBuffer);
|
|
420
|
+
gl.enableVertexAttribArray(0);
|
|
421
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
422
|
+
gl.vertexAttribDivisor(0, 0);
|
|
423
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, instanceGpuBuffer);
|
|
424
|
+
const strideBytes = INSTANCE_STRIDE * 4;
|
|
425
|
+
for (let index = 0; index < INSTANCE_ATTRS.length; index += 1) {
|
|
426
|
+
const attr = INSTANCE_ATTRS[index];
|
|
427
|
+
gl.enableVertexAttribArray(attr.loc);
|
|
428
|
+
gl.vertexAttribPointer(attr.loc, attr.size, gl.FLOAT, false, strideBytes, attr.offset * 4);
|
|
429
|
+
gl.vertexAttribDivisor(attr.loc, 1);
|
|
430
|
+
}
|
|
431
|
+
if (program.uViewport) gl.uniform2f(program.uViewport, opts.viewportW, opts.viewportH);
|
|
432
|
+
const textured = Boolean(opts.textured && opts.texture);
|
|
433
|
+
const hframes = textured ? Math.max(1, opts.hframes) : 1;
|
|
434
|
+
const vframes = textured ? Math.max(1, opts.vframes) : 1;
|
|
435
|
+
if (program.uHframes) gl.uniform1f(program.uHframes, hframes);
|
|
436
|
+
if (program.uVframes) gl.uniform1f(program.uVframes, vframes);
|
|
437
|
+
if (program.uTextured) gl.uniform1i(program.uTextured, textured ? 1 : 0);
|
|
438
|
+
if (program.uAdditive) gl.uniform1i(program.uAdditive, additive ? 1 : 0);
|
|
439
|
+
if (program.uAlphaFromRed) gl.uniform1i(program.uAlphaFromRed, opts.alphaFromRed ? 1 : 0);
|
|
440
|
+
const erode = opts.erode ?? null;
|
|
441
|
+
if (program.uErode) gl.uniform1i(program.uErode, erode ? 1 : 0);
|
|
442
|
+
if (program.uErodeFactors) gl.uniform2f(program.uErodeFactors, erode ? erode.threshold : 0, erode ? erode.softness : 0);
|
|
443
|
+
if (program.uUvPolar) gl.uniform1i(program.uUvPolar, opts.uvPolar ? 1 : 0);
|
|
444
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
445
|
+
if (opts.texture && program.uTexture) {
|
|
446
|
+
gl.bindTexture(gl.TEXTURE_2D, opts.texture);
|
|
447
|
+
gl.uniform1i(program.uTexture, 0);
|
|
448
|
+
} else gl.bindTexture(gl.TEXTURE_2D, null);
|
|
449
|
+
const lut = opts.lutTexture ?? null;
|
|
450
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
451
|
+
gl.bindTexture(gl.TEXTURE_2D, lut);
|
|
452
|
+
if (program.uLutTex) gl.uniform1i(program.uLutTex, 1);
|
|
453
|
+
if (program.uLut) gl.uniform1i(program.uLut, lut ? 1 : 0);
|
|
454
|
+
const mask = opts.maskTexture ?? null;
|
|
455
|
+
gl.activeTexture(gl.TEXTURE2);
|
|
456
|
+
gl.bindTexture(gl.TEXTURE_2D, mask);
|
|
457
|
+
if (program.uMaskTex) gl.uniform1i(program.uMaskTex, 2);
|
|
458
|
+
if (program.uMask) gl.uniform1i(program.uMask, mask ? 1 : 0);
|
|
459
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
460
|
+
gl.enable(gl.BLEND);
|
|
461
|
+
gl.blendEquation(gl.FUNC_ADD);
|
|
462
|
+
if (additive) gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
|
|
463
|
+
else gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
464
|
+
gl.disable(gl.DEPTH_TEST);
|
|
465
|
+
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, buffer.count);
|
|
466
|
+
for (let index = 0; index < INSTANCE_ATTRS.length; index += 1) {
|
|
467
|
+
const attr = INSTANCE_ATTRS[index];
|
|
468
|
+
gl.vertexAttribDivisor(attr.loc, 0);
|
|
469
|
+
gl.disableVertexAttribArray(attr.loc);
|
|
470
|
+
}
|
|
471
|
+
if (additive && program.accum) {
|
|
472
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, opts.targetFramebuffer ?? null);
|
|
473
|
+
gl.viewport(0, 0, targetW, targetH);
|
|
474
|
+
gl.useProgram(program.resolveProgram);
|
|
475
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, sharedGl.quad);
|
|
476
|
+
gl.enableVertexAttribArray(0);
|
|
477
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
478
|
+
gl.vertexAttribDivisor(0, 0);
|
|
479
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
480
|
+
gl.bindTexture(gl.TEXTURE_2D, program.accum.texture);
|
|
481
|
+
if (program.uAccum) gl.uniform1i(program.uAccum, 0);
|
|
482
|
+
if (opts.additiveResolveIntoExisting) {
|
|
483
|
+
gl.enable(gl.BLEND);
|
|
484
|
+
gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
|
|
485
|
+
gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
|
|
486
|
+
} else gl.disable(gl.BLEND);
|
|
487
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
488
|
+
}
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/shader-webgl.ts
|
|
493
|
+
/** Allocate the shared clip-space quad used by full-surface shader draws. */
|
|
494
|
+
function createWebglFullscreenQuad(gl) {
|
|
495
|
+
const quad = gl.createBuffer();
|
|
496
|
+
if (!quad) return null;
|
|
497
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, quad);
|
|
498
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
|
499
|
+
-1,
|
|
500
|
+
-1,
|
|
501
|
+
1,
|
|
502
|
+
-1,
|
|
503
|
+
-1,
|
|
504
|
+
1,
|
|
505
|
+
1,
|
|
506
|
+
1
|
|
507
|
+
]), gl.STATIC_DRAW);
|
|
508
|
+
return quad;
|
|
509
|
+
}
|
|
510
|
+
function createWebglTexture(gl) {
|
|
511
|
+
return gl.createTexture();
|
|
512
|
+
}
|
|
513
|
+
function setTextureSampling(gl, repeat, nearest = false) {
|
|
514
|
+
const wrap = repeat ? gl.REPEAT : gl.CLAMP_TO_EDGE;
|
|
515
|
+
const filter = nearest ? gl.NEAREST : gl.LINEAR;
|
|
516
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap);
|
|
517
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);
|
|
518
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
|
|
519
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
|
|
520
|
+
}
|
|
521
|
+
/** Upload bytes, an image, or a canvas with Godot's top-left UV convention. */
|
|
522
|
+
function uploadWebglTexture(gl, texture, source, width, height, opts = {}) {
|
|
523
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
524
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
525
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
526
|
+
if (source instanceof Uint8Array) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width ?? 1, height ?? 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
|
527
|
+
else gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
|
528
|
+
setTextureSampling(gl, opts.repeat === true, opts.nearest === true);
|
|
529
|
+
}
|
|
530
|
+
function createWebglPlaceholderTexture(gl, repeat) {
|
|
531
|
+
const texture = createWebglTexture(gl);
|
|
532
|
+
if (texture) uploadWebglTexture(gl, texture, new Uint8Array([
|
|
533
|
+
0,
|
|
534
|
+
0,
|
|
535
|
+
0,
|
|
536
|
+
0
|
|
537
|
+
]), 1, 1, { repeat });
|
|
538
|
+
return texture;
|
|
539
|
+
}
|
|
540
|
+
const COMPILE_POLL_MS = 1;
|
|
541
|
+
const COMPILE_MAX_WAIT_MS = 3e3;
|
|
542
|
+
const parallelCompileExts = /* @__PURE__ */ new WeakMap();
|
|
543
|
+
function parallelCompileExt(gl) {
|
|
544
|
+
const cached = parallelCompileExts.get(gl);
|
|
545
|
+
if (cached !== void 0) return cached;
|
|
546
|
+
let ext = null;
|
|
547
|
+
try {
|
|
548
|
+
const found = gl.getExtension("KHR_parallel_shader_compile");
|
|
549
|
+
ext = found && typeof found.COMPLETION_STATUS_KHR === "number" ? { COMPLETION_STATUS_KHR: found.COMPLETION_STATUS_KHR } : null;
|
|
550
|
+
} catch {
|
|
551
|
+
ext = null;
|
|
552
|
+
}
|
|
553
|
+
parallelCompileExts.set(gl, ext);
|
|
554
|
+
return ext;
|
|
555
|
+
}
|
|
556
|
+
const FAILED_PROGRAM = {
|
|
557
|
+
ready: () => true,
|
|
558
|
+
finish: () => null
|
|
559
|
+
};
|
|
560
|
+
/** Kick compilation and linking without synchronously asking the driver for status. */
|
|
561
|
+
function startProgram(gl, vertexSrc, fragmentSrc, beforeLink) {
|
|
562
|
+
const vs = startShader(gl, gl.VERTEX_SHADER, vertexSrc);
|
|
563
|
+
const fs = startShader(gl, gl.FRAGMENT_SHADER, fragmentSrc);
|
|
564
|
+
if (!vs || !fs) {
|
|
565
|
+
if (vs) gl.deleteShader(vs);
|
|
566
|
+
if (fs) gl.deleteShader(fs);
|
|
567
|
+
return FAILED_PROGRAM;
|
|
568
|
+
}
|
|
569
|
+
const program = gl.createProgram();
|
|
570
|
+
if (!program) {
|
|
571
|
+
gl.deleteShader(vs);
|
|
572
|
+
gl.deleteShader(fs);
|
|
573
|
+
return FAILED_PROGRAM;
|
|
574
|
+
}
|
|
575
|
+
gl.attachShader(program, vs);
|
|
576
|
+
gl.attachShader(program, fs);
|
|
577
|
+
beforeLink?.(program);
|
|
578
|
+
gl.linkProgram(program);
|
|
579
|
+
const ext = parallelCompileExt(gl);
|
|
580
|
+
let completed = false;
|
|
581
|
+
return {
|
|
582
|
+
ready() {
|
|
583
|
+
if (completed || !ext) return true;
|
|
584
|
+
completed = gl.getProgramParameter(program, ext.COMPLETION_STATUS_KHR) === true;
|
|
585
|
+
return completed;
|
|
586
|
+
},
|
|
587
|
+
finish() {
|
|
588
|
+
if (gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
589
|
+
gl.deleteShader(vs);
|
|
590
|
+
gl.deleteShader(fs);
|
|
591
|
+
return program;
|
|
592
|
+
}
|
|
593
|
+
let compileFailed = false;
|
|
594
|
+
for (const shader of [vs, fs]) {
|
|
595
|
+
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) continue;
|
|
596
|
+
compileFailed = true;
|
|
597
|
+
console.warn("[gsw webgl] shader compile failed:", gl.getShaderInfoLog(shader));
|
|
598
|
+
}
|
|
599
|
+
if (!compileFailed) console.warn("[gsw webgl] program link failed:", gl.getProgramInfoLog(program));
|
|
600
|
+
gl.deleteShader(vs);
|
|
601
|
+
gl.deleteShader(fs);
|
|
602
|
+
gl.deleteProgram(program);
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
/** Compile and link synchronously for callers that cannot yield. */
|
|
608
|
+
function compileProgram(gl, vertexSrc, fragmentSrc, beforeLink) {
|
|
609
|
+
return startProgram(gl, vertexSrc, fragmentSrc, beforeLink).finish();
|
|
610
|
+
}
|
|
611
|
+
const compileWaiters = /* @__PURE__ */ new Set();
|
|
612
|
+
let compilePollTimer = null;
|
|
613
|
+
function performanceNow() {
|
|
614
|
+
return typeof performance !== "undefined" && performance.now ? performance.now() : 0;
|
|
615
|
+
}
|
|
616
|
+
function pollCompiles() {
|
|
617
|
+
compilePollTimer = null;
|
|
618
|
+
const now = performanceNow();
|
|
619
|
+
for (const waiter of [...compileWaiters]) {
|
|
620
|
+
if (!waiter.pending.ready() && now < waiter.deadline) continue;
|
|
621
|
+
compileWaiters.delete(waiter);
|
|
622
|
+
waiter.wake();
|
|
623
|
+
}
|
|
624
|
+
if (compileWaiters.size > 0) scheduleCompilePoll();
|
|
625
|
+
}
|
|
626
|
+
function scheduleCompilePoll() {
|
|
627
|
+
if (compilePollTimer === null) compilePollTimer = setTimeout(pollCompiles, COMPILE_POLL_MS);
|
|
628
|
+
}
|
|
629
|
+
/** Finish when the parallel-compile extension says its status query will not block. */
|
|
630
|
+
async function compileProgramAsync(gl, vertexSrc, fragmentSrc, beforeLink) {
|
|
631
|
+
const pending = startProgram(gl, vertexSrc, fragmentSrc, beforeLink);
|
|
632
|
+
if (!pending.ready() && typeof setTimeout === "function") await new Promise((wake) => {
|
|
633
|
+
compileWaiters.add({
|
|
634
|
+
pending,
|
|
635
|
+
deadline: performanceNow() + COMPILE_MAX_WAIT_MS,
|
|
636
|
+
wake
|
|
637
|
+
});
|
|
638
|
+
scheduleCompilePoll();
|
|
639
|
+
});
|
|
640
|
+
return pending.finish();
|
|
641
|
+
}
|
|
642
|
+
/** Execute one Godot shader frame in a borrowed context; no DOM, scheduling, or capture work. */
|
|
643
|
+
function drawGodotWebglShaderFrame(gl, frame) {
|
|
644
|
+
const loc = (name) => frame.locations.get(name) ?? null;
|
|
645
|
+
const one = (name, value) => {
|
|
646
|
+
const l = loc(name);
|
|
647
|
+
if (l) gl.uniform1f(l, value);
|
|
648
|
+
};
|
|
649
|
+
const two = (name, value) => {
|
|
650
|
+
const l = loc(name);
|
|
651
|
+
if (l) gl.uniform2f(l, value[0] ?? 0, value[1] ?? 0);
|
|
652
|
+
};
|
|
653
|
+
const four = (name, value) => {
|
|
654
|
+
const l = loc(name);
|
|
655
|
+
if (l) gl.uniform4f(l, value[0] ?? 0, value[1] ?? 0, value[2] ?? 0, value[3] ?? 0);
|
|
656
|
+
};
|
|
657
|
+
gl.viewport(0, 0, frame.width, frame.height);
|
|
658
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
659
|
+
gl.disable(gl.DEPTH_TEST);
|
|
660
|
+
gl.disable(gl.BLEND);
|
|
661
|
+
gl.useProgram(frame.program);
|
|
662
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, frame.quad);
|
|
663
|
+
gl.enableVertexAttribArray(0);
|
|
664
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
665
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
666
|
+
gl.bindTexture(gl.TEXTURE_2D, frame.texture);
|
|
667
|
+
{
|
|
668
|
+
const l = loc("TEXTURE");
|
|
669
|
+
if (l) gl.uniform1i(l, 0);
|
|
670
|
+
}
|
|
671
|
+
if (frame.time !== void 0) one("TIME", frame.time);
|
|
672
|
+
if (frame.texturePixelSize) two("TEXTURE_PIXEL_SIZE", frame.texturePixelSize);
|
|
673
|
+
four("MODULATE", frame.modulate);
|
|
674
|
+
two("_godot_uv_fit", frame.uvFit);
|
|
675
|
+
four("_godot_uv_window", frame.uvWindow);
|
|
676
|
+
if (frame.screenOrigin) two("_godot_screen_origin", frame.screenOrigin);
|
|
677
|
+
if (frame.screenSize) two("_godot_screen_size", frame.screenSize);
|
|
678
|
+
if (frame.screenTexture) {
|
|
679
|
+
const unit = frame.screenTextureUnit ?? 1;
|
|
680
|
+
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
681
|
+
gl.bindTexture(gl.TEXTURE_2D, frame.screenTexture);
|
|
682
|
+
const l = loc("SCREEN_TEXTURE");
|
|
683
|
+
if (l) gl.uniform1i(l, unit);
|
|
684
|
+
}
|
|
685
|
+
if (frame.screenPixelSize) two("SCREEN_PIXEL_SIZE", frame.screenPixelSize);
|
|
686
|
+
for (const sampler of frame.samplers) {
|
|
687
|
+
gl.activeTexture(gl.TEXTURE0 + sampler.unit);
|
|
688
|
+
gl.bindTexture(gl.TEXTURE_2D, sampler.texture);
|
|
689
|
+
const l = loc(sampler.name);
|
|
690
|
+
if (l) gl.uniform1i(l, sampler.unit);
|
|
691
|
+
}
|
|
692
|
+
for (const uniform of frame.uniforms) uploadGodotShaderUniform(gl, loc(uniform.name), uniform, frame.params[uniform.name], frame.paramKinds[uniform.name]);
|
|
693
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
694
|
+
}
|
|
695
|
+
/** Exact Godot scalar/vector/array upload rules. Callers retain their own binding model. */
|
|
696
|
+
function uploadGodotShaderUniform(gl, location, uniform, raw, paramKind) {
|
|
697
|
+
if (!location) return;
|
|
698
|
+
const value = raw ?? uniform.default;
|
|
699
|
+
const values = Array.isArray(value) ? [...value] : [];
|
|
700
|
+
const pad = (length) => {
|
|
701
|
+
const out = values.slice(0, length);
|
|
702
|
+
while (out.length < length) out.push(0);
|
|
703
|
+
return out;
|
|
704
|
+
};
|
|
705
|
+
if (uniform.arrayLength) {
|
|
706
|
+
const n = uniform.arrayLength;
|
|
707
|
+
if (uniform.type === "float") gl.uniform1fv(location, new Float32Array(pad(n)));
|
|
708
|
+
else if (uniform.type === "int" || uniform.type === "bool") gl.uniform1iv(location, new Int32Array(pad(n).map(Math.round)));
|
|
709
|
+
else if (uniform.type === "vec2") gl.uniform2fv(location, new Float32Array(pad(n * 2)));
|
|
710
|
+
else if (uniform.type === "vec3") {
|
|
711
|
+
const flat = paramKind === "PackedColorArray" && values.length % 4 === 0 ? values.filter((_, i) => i % 4 !== 3) : values;
|
|
712
|
+
while (flat.length < n * 3) flat.push(0);
|
|
713
|
+
gl.uniform3fv(location, new Float32Array(flat.slice(0, n * 3)));
|
|
714
|
+
} else if (uniform.type === "vec4") gl.uniform4fv(location, new Float32Array(pad(n * 4)));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (uniform.type === "float") gl.uniform1f(location, typeof value === "number" ? value : 0);
|
|
718
|
+
else if (uniform.type === "int" || uniform.type === "bool") gl.uniform1i(location, typeof value === "number" ? Math.round(value) : 0);
|
|
719
|
+
else if (uniform.type === "vec2") gl.uniform2f(location, values[0] ?? 0, values[1] ?? 0);
|
|
720
|
+
else if (uniform.type === "vec3") gl.uniform3f(location, values[0] ?? 0, values[1] ?? 0, values[2] ?? 0);
|
|
721
|
+
else if (uniform.type === "vec4") gl.uniform4f(location, values[0] ?? 0, values[1] ?? 0, values[2] ?? 0, values[3] ?? 0);
|
|
722
|
+
}
|
|
723
|
+
function startShader(gl, type, source) {
|
|
724
|
+
const shader = gl.createShader(type);
|
|
725
|
+
if (!shader) return null;
|
|
726
|
+
gl.shaderSource(shader, source);
|
|
727
|
+
gl.compileShader(shader);
|
|
728
|
+
return shader;
|
|
729
|
+
}
|
|
730
|
+
/** Clear a supplied framebuffer's draw region; callers control its lifetime and binding. */
|
|
731
|
+
function clearWebglSurface(gl, width, height, scissorToViewport = false) {
|
|
732
|
+
gl.viewport(0, 0, width, height);
|
|
733
|
+
if (scissorToViewport) {
|
|
734
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
735
|
+
gl.scissor(0, 0, width, height);
|
|
736
|
+
}
|
|
737
|
+
gl.clearColor(0, 0, 0, 0);
|
|
738
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
739
|
+
if (scissorToViewport) gl.disable(gl.SCISSOR_TEST);
|
|
740
|
+
}
|
|
741
|
+
/** Release an owned texture. Borrowed texture handles must not be passed here. */
|
|
742
|
+
function deleteWebglTexture(gl, texture) {
|
|
743
|
+
gl.deleteTexture(texture);
|
|
744
|
+
}
|
|
745
|
+
//#endregion
|
|
746
|
+
//#region src/webgl.ts
|
|
747
|
+
/**
|
|
748
|
+
* DOM-free effect targets for a canvas stage.
|
|
749
|
+
*
|
|
750
|
+
* This module deliberately accepts an already-created WebGL2 context. It never
|
|
751
|
+
* asks for a canvas, creates an element, or schedules a frame: the caller owns
|
|
752
|
+
* the stage, painter ordering, and clock. The small raw-GLSL and procedural
|
|
753
|
+
* particle subsets below are useful without claiming to be a Godot material or
|
|
754
|
+
* GPUParticles interpreter.
|
|
755
|
+
*/
|
|
756
|
+
/** Allocate reusable, DOM-free simulation and instance storage for a normalized Godot particle spec. */
|
|
757
|
+
function createGodotParticleScratch(config, maxInstances) {
|
|
758
|
+
const normalized = normalizeParticleRenderConfig(config);
|
|
759
|
+
return {
|
|
760
|
+
config: normalized,
|
|
761
|
+
state: createParticleState(normalized, maxInstances),
|
|
762
|
+
instances: new InstanceBuffer(Math.max(1, maxInstances ?? normalized.amount))
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const FULLSCREEN_VERTEX = `#version 300 es
|
|
766
|
+
out vec2 v_uv;
|
|
767
|
+
const vec2 POSITIONS[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
|
768
|
+
void main() {
|
|
769
|
+
vec2 position = POSITIONS[gl_VertexID];
|
|
770
|
+
v_uv = position * 0.5 + 0.5;
|
|
771
|
+
gl_Position = vec4(position, 0.0, 1.0);
|
|
772
|
+
}`;
|
|
773
|
+
const GODOT_DEFAULT_FIT = [1, 1];
|
|
774
|
+
const GODOT_DEFAULT_WINDOW = [
|
|
775
|
+
0,
|
|
776
|
+
0,
|
|
777
|
+
1,
|
|
778
|
+
1
|
|
779
|
+
];
|
|
780
|
+
const GODOT_DEFAULT_MODULATE = [
|
|
781
|
+
1,
|
|
782
|
+
1,
|
|
783
|
+
1,
|
|
784
|
+
1
|
|
785
|
+
];
|
|
786
|
+
const GODOT_DEFAULT_SCREEN_RECT = [
|
|
787
|
+
0,
|
|
788
|
+
0,
|
|
789
|
+
1,
|
|
790
|
+
1
|
|
791
|
+
];
|
|
792
|
+
const PARTICLE_VERTEX = `#version 300 es
|
|
793
|
+
uniform float u_time;
|
|
794
|
+
uniform float u_delta;
|
|
795
|
+
uniform float u_seed;
|
|
796
|
+
uniform float u_lifetime;
|
|
797
|
+
uniform float u_emission_rate;
|
|
798
|
+
uniform float u_max_particles;
|
|
799
|
+
uniform vec2 u_position;
|
|
800
|
+
uniform float u_direction;
|
|
801
|
+
uniform float u_spread;
|
|
802
|
+
uniform vec2 u_speed;
|
|
803
|
+
uniform vec2 u_gravity;
|
|
804
|
+
uniform vec2 u_resolution;
|
|
805
|
+
uniform vec2 u_size;
|
|
806
|
+
out float v_progress;
|
|
807
|
+
out float v_alive;
|
|
808
|
+
float hash(float value) { return fract(sin(value) * 43758.5453123); }
|
|
809
|
+
void main() {
|
|
810
|
+
float id = float(gl_VertexID);
|
|
811
|
+
float emission = max(u_emission_rate, 0.0001);
|
|
812
|
+
float active = min(u_max_particles, ceil(emission * u_lifetime));
|
|
813
|
+
float period = active / emission;
|
|
814
|
+
float now = max(0.0, u_time + u_delta);
|
|
815
|
+
float birth = floor((now * emission - id) / active) * period + id / emission;
|
|
816
|
+
float age = now - birth;
|
|
817
|
+
float effectiveLifetime = min(u_lifetime, period);
|
|
818
|
+
float progress = clamp(age / effectiveLifetime, 0.0, 1.0);
|
|
819
|
+
float angle = u_direction + (hash(id + u_seed) - 0.5) * u_spread;
|
|
820
|
+
float speed = mix(u_speed.x, u_speed.y, hash(id * 7.0 + u_seed));
|
|
821
|
+
vec2 velocity = vec2(cos(angle), sin(angle)) * speed;
|
|
822
|
+
vec2 pixel = u_position + velocity * age + 0.5 * u_gravity * age * age;
|
|
823
|
+
vec2 clip = pixel / u_resolution * 2.0 - 1.0;
|
|
824
|
+
gl_Position = vec4(clip * vec2(1.0, -1.0), 0.0, 1.0);
|
|
825
|
+
gl_PointSize = mix(u_size.x, u_size.y, progress);
|
|
826
|
+
v_progress = progress;
|
|
827
|
+
v_alive = step(0.0, birth) * step(0.0, age) * step(age, effectiveLifetime);
|
|
828
|
+
}`;
|
|
829
|
+
/** Safe ceiling for the stateless point-particle subset. Larger systems are refused. */
|
|
830
|
+
const MAX_HEADLESS_PARTICLES = 16384;
|
|
831
|
+
const PARTICLE_FRAGMENT = `#version 300 es
|
|
832
|
+
precision highp float;
|
|
833
|
+
uniform vec4 u_start_color;
|
|
834
|
+
uniform vec4 u_end_color;
|
|
835
|
+
in float v_progress;
|
|
836
|
+
in float v_alive;
|
|
837
|
+
out vec4 fragColor;
|
|
838
|
+
void main() {
|
|
839
|
+
if (v_alive < 0.5) discard;
|
|
840
|
+
vec2 centered = gl_PointCoord * 2.0 - 1.0;
|
|
841
|
+
float coverage = smoothstep(1.0, 0.7, dot(centered, centered));
|
|
842
|
+
vec4 color = mix(u_start_color, u_end_color, v_progress);
|
|
843
|
+
fragColor = vec4(color.rgb * color.a, color.a) * coverage;
|
|
844
|
+
}`;
|
|
845
|
+
function ok(value) {
|
|
846
|
+
return {
|
|
847
|
+
ok: true,
|
|
848
|
+
value
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
function fail(code, message, feature) {
|
|
852
|
+
return {
|
|
853
|
+
ok: false,
|
|
854
|
+
diagnostic: {
|
|
855
|
+
code,
|
|
856
|
+
message,
|
|
857
|
+
feature
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function targetDimensions(width, height) {
|
|
862
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
|
|
863
|
+
return [Math.max(1, Math.round(width)), Math.max(1, Math.round(height))];
|
|
864
|
+
}
|
|
865
|
+
function directParticleTransform(transform) {
|
|
866
|
+
const values = transform ?? [
|
|
867
|
+
1,
|
|
868
|
+
0,
|
|
869
|
+
0,
|
|
870
|
+
1,
|
|
871
|
+
0,
|
|
872
|
+
0
|
|
873
|
+
];
|
|
874
|
+
if (!values.every(Number.isFinite)) return null;
|
|
875
|
+
const [xx, xy, yx, yy, originX, originY] = values;
|
|
876
|
+
const xLength = Math.hypot(xx, xy);
|
|
877
|
+
const yLength = Math.hypot(yx, yy);
|
|
878
|
+
const tolerance = Math.max(1, xLength * yLength) * 1e-6;
|
|
879
|
+
if (xLength <= 1e-9 || Math.abs(xLength - yLength) > tolerance || Math.abs(xx * yx + xy * yy) > tolerance || xx * yy - xy * yx <= 0) return null;
|
|
880
|
+
return {
|
|
881
|
+
xx,
|
|
882
|
+
xy,
|
|
883
|
+
yx,
|
|
884
|
+
yy,
|
|
885
|
+
originX,
|
|
886
|
+
originY,
|
|
887
|
+
scale: xLength,
|
|
888
|
+
rotation: Math.atan2(xy, xx)
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
function directParticleModulate(modulate) {
|
|
892
|
+
const value = modulate ?? [
|
|
893
|
+
1,
|
|
894
|
+
1,
|
|
895
|
+
1,
|
|
896
|
+
1
|
|
897
|
+
];
|
|
898
|
+
return value.every(Number.isFinite) ? [
|
|
899
|
+
value[0],
|
|
900
|
+
value[1],
|
|
901
|
+
value[2],
|
|
902
|
+
value[3]
|
|
903
|
+
] : null;
|
|
904
|
+
}
|
|
905
|
+
function compileProgram$1(gl, vertexSource, fragmentSource) {
|
|
906
|
+
const vertex = gl.createShader(gl.VERTEX_SHADER);
|
|
907
|
+
const fragment = gl.createShader(gl.FRAGMENT_SHADER);
|
|
908
|
+
if (!vertex || !fragment) {
|
|
909
|
+
if (vertex) gl.deleteShader(vertex);
|
|
910
|
+
if (fragment) gl.deleteShader(fragment);
|
|
911
|
+
return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not allocate an effect shader.");
|
|
912
|
+
}
|
|
913
|
+
gl.shaderSource(vertex, vertexSource);
|
|
914
|
+
gl.shaderSource(fragment, fragmentSource);
|
|
915
|
+
gl.compileShader(vertex);
|
|
916
|
+
gl.compileShader(fragment);
|
|
917
|
+
if (!gl.getShaderParameter(vertex, gl.COMPILE_STATUS) || !gl.getShaderParameter(fragment, gl.COMPILE_STATUS)) {
|
|
918
|
+
const log = gl.getShaderInfoLog(vertex) || gl.getShaderInfoLog(fragment) || "unknown compiler error";
|
|
919
|
+
gl.deleteShader(vertex);
|
|
920
|
+
gl.deleteShader(fragment);
|
|
921
|
+
return fail("SHADER_COMPILE_FAILED", log);
|
|
922
|
+
}
|
|
923
|
+
const program = gl.createProgram();
|
|
924
|
+
if (!program) {
|
|
925
|
+
gl.deleteShader(vertex);
|
|
926
|
+
gl.deleteShader(fragment);
|
|
927
|
+
return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not allocate an effect program.");
|
|
928
|
+
}
|
|
929
|
+
gl.attachShader(program, vertex);
|
|
930
|
+
gl.attachShader(program, fragment);
|
|
931
|
+
gl.linkProgram(program);
|
|
932
|
+
gl.deleteShader(vertex);
|
|
933
|
+
gl.deleteShader(fragment);
|
|
934
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
935
|
+
const log = gl.getProgramInfoLog(program) || "unknown linker error";
|
|
936
|
+
gl.deleteProgram(program);
|
|
937
|
+
return fail("SHADER_COMPILE_FAILED", log);
|
|
938
|
+
}
|
|
939
|
+
const vao = gl.createVertexArray();
|
|
940
|
+
if (!vao) {
|
|
941
|
+
gl.deleteProgram(program);
|
|
942
|
+
return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not allocate an effect vertex array.");
|
|
943
|
+
}
|
|
944
|
+
return ok({
|
|
945
|
+
program,
|
|
946
|
+
vao,
|
|
947
|
+
locations: /* @__PURE__ */ new Map()
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
function destroyProgram(gl, program) {
|
|
951
|
+
if (!program) return;
|
|
952
|
+
gl.deleteVertexArray(program.vao);
|
|
953
|
+
gl.deleteProgram(program.program);
|
|
954
|
+
}
|
|
955
|
+
function shaderDiagnostic(pass) {
|
|
956
|
+
const known = new Set([
|
|
957
|
+
"screen-texture",
|
|
958
|
+
"time",
|
|
959
|
+
"delta",
|
|
960
|
+
"seed",
|
|
961
|
+
"external-textures",
|
|
962
|
+
"multiple-render-targets",
|
|
963
|
+
"transform-feedback",
|
|
964
|
+
"godot-source"
|
|
965
|
+
]);
|
|
966
|
+
const seen = /* @__PURE__ */ new Set();
|
|
967
|
+
for (const feature of pass.features ?? []) {
|
|
968
|
+
if (!known.has(feature)) return {
|
|
969
|
+
code: "UNSUPPORTED_SHADER_FEATURE",
|
|
970
|
+
message: `Unknown headless shader capability ${feature}.`,
|
|
971
|
+
feature
|
|
972
|
+
};
|
|
973
|
+
if (seen.has(feature)) return {
|
|
974
|
+
code: "UNSUPPORTED_SHADER_FEATURE",
|
|
975
|
+
message: `Duplicate headless shader capability ${feature}.`,
|
|
976
|
+
feature
|
|
977
|
+
};
|
|
978
|
+
seen.add(feature);
|
|
979
|
+
}
|
|
980
|
+
for (const [name, value] of Object.entries(pass.uniforms ?? {})) {
|
|
981
|
+
const components = typeof value === "number" ? [value] : value;
|
|
982
|
+
if (!/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(name) || typeof value !== "number" && (value.length < 2 || value.length > 4) || !Array.from(components).every(Number.isFinite)) return {
|
|
983
|
+
code: "UNSUPPORTED_SHADER_FEATURE",
|
|
984
|
+
message: "Shader uniform names and values must be finite GLSL scalar/vector values.",
|
|
985
|
+
feature: `uniform:${name}`
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
const unsupported = new Set([
|
|
989
|
+
"external-textures",
|
|
990
|
+
"multiple-render-targets",
|
|
991
|
+
"transform-feedback",
|
|
992
|
+
"godot-source"
|
|
993
|
+
]);
|
|
994
|
+
const requested = pass.features?.find((feature) => unsupported.has(feature));
|
|
995
|
+
if (requested) return {
|
|
996
|
+
code: "UNSUPPORTED_SHADER_FEATURE",
|
|
997
|
+
message: `Headless shader passes do not support ${requested}.`,
|
|
998
|
+
feature: requested
|
|
999
|
+
};
|
|
1000
|
+
if (!/^\s*#version\s+300\s+es\b/m.test(pass.fragmentSource) || !/\bvoid\s+main\s*\(/.test(pass.fragmentSource)) return {
|
|
1001
|
+
code: "UNSUPPORTED_SHADER_FEATURE",
|
|
1002
|
+
message: "A headless shader pass must be a complete GLSL ES 3 fragment shader.",
|
|
1003
|
+
feature: "raw-glsl300es"
|
|
1004
|
+
};
|
|
1005
|
+
const samplers = [...pass.fragmentSource.matchAll(/uniform\s+sampler\w+\s+(\w+)/g)].map((match) => match[1]);
|
|
1006
|
+
if (samplers.some((name) => name !== "u_screenTexture")) return {
|
|
1007
|
+
code: "UNSUPPORTED_SHADER_FEATURE",
|
|
1008
|
+
message: "Only the explicit u_screenTexture sampler is supported by this headless pass.",
|
|
1009
|
+
feature: "external-textures"
|
|
1010
|
+
};
|
|
1011
|
+
if (samplers.includes("u_screenTexture") && !pass.features?.includes("screen-texture")) return {
|
|
1012
|
+
code: "UNSUPPORTED_SHADER_FEATURE",
|
|
1013
|
+
message: "u_screenTexture requires the explicit screen-texture capability declaration.",
|
|
1014
|
+
feature: "screen-texture"
|
|
1015
|
+
};
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
function particleDiagnostic(parameters) {
|
|
1019
|
+
const features = parameters.features;
|
|
1020
|
+
if (features?.emissionShape && features.emissionShape !== "point") return {
|
|
1021
|
+
code: "UNSUPPORTED_PARTICLE_FEATURE",
|
|
1022
|
+
message: "Only point particle emission is supported.",
|
|
1023
|
+
feature: `emissionShape:${features.emissionShape}`
|
|
1024
|
+
};
|
|
1025
|
+
for (const feature of [
|
|
1026
|
+
"collision",
|
|
1027
|
+
"attractors",
|
|
1028
|
+
"textureAtlas"
|
|
1029
|
+
]) if (features?.[feature]) return {
|
|
1030
|
+
code: "UNSUPPORTED_PARTICLE_FEATURE",
|
|
1031
|
+
message: `Particle ${feature} is not supported by the stateless headless path.`,
|
|
1032
|
+
feature
|
|
1033
|
+
};
|
|
1034
|
+
if (![
|
|
1035
|
+
parameters.maxParticles,
|
|
1036
|
+
parameters.emissionRate,
|
|
1037
|
+
parameters.lifetimeSeconds,
|
|
1038
|
+
parameters.position[0],
|
|
1039
|
+
parameters.position[1],
|
|
1040
|
+
parameters.directionRadians,
|
|
1041
|
+
parameters.spreadRadians,
|
|
1042
|
+
parameters.speedMin,
|
|
1043
|
+
parameters.speedMax,
|
|
1044
|
+
parameters.gravity[0],
|
|
1045
|
+
parameters.gravity[1],
|
|
1046
|
+
parameters.startSizePx,
|
|
1047
|
+
parameters.endSizePx,
|
|
1048
|
+
...parameters.startColor,
|
|
1049
|
+
...parameters.endColor
|
|
1050
|
+
].every(Number.isFinite)) return {
|
|
1051
|
+
code: "UNSUPPORTED_PARTICLE_FEATURE",
|
|
1052
|
+
message: "Particle parameters must all be finite numbers.",
|
|
1053
|
+
feature: "parameters"
|
|
1054
|
+
};
|
|
1055
|
+
if (!Number.isInteger(parameters.maxParticles) || parameters.maxParticles <= 0 || parameters.maxParticles > 16384 || parameters.emissionRate <= 0 || parameters.lifetimeSeconds <= 0 || parameters.speedMin < 0 || parameters.speedMax < parameters.speedMin || parameters.startSizePx < 0 || parameters.endSizePx < 0 || parameters.startColor.some((value) => value < 0 || value > 1) || parameters.endColor.some((value) => value < 0 || value > 1)) return {
|
|
1056
|
+
code: "UNSUPPORTED_PARTICLE_FEATURE",
|
|
1057
|
+
message: "Particle counts, emission rate, and lifetime must be positive.",
|
|
1058
|
+
feature: "parameters"
|
|
1059
|
+
};
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
1062
|
+
function renderInputDiagnostic(input) {
|
|
1063
|
+
if (!Number.isFinite(input.time) || !Number.isFinite(input.delta) || input.time < 0 || input.delta < 0 || "seed" in input && input.seed !== void 0 && !Number.isFinite(input.seed)) return {
|
|
1064
|
+
code: "INVALID_RENDER_INPUT",
|
|
1065
|
+
message: "Effect time, delta, and seed must be finite; time and delta must be non-negative.",
|
|
1066
|
+
feature: "render-input"
|
|
1067
|
+
};
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
function applyGodotBlend(gl, blend) {
|
|
1071
|
+
gl.enable(gl.BLEND);
|
|
1072
|
+
if (blend === "sub") {
|
|
1073
|
+
gl.blendEquationSeparate(gl.FUNC_REVERSE_SUBTRACT, gl.FUNC_ADD);
|
|
1074
|
+
gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
|
|
1078
|
+
if (blend === "add") {
|
|
1079
|
+
gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (blend === "mul") {
|
|
1083
|
+
gl.blendFuncSeparate(gl.DST_COLOR, gl.ZERO, gl.DST_ALPHA, gl.ZERO);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
1087
|
+
}
|
|
1088
|
+
/** Create a DOM-free owner for effect FBOs and their ordered GPU passes. */
|
|
1089
|
+
function createHeadlessEffectsStage(gl) {
|
|
1090
|
+
const targets = /* @__PURE__ */ new Set();
|
|
1091
|
+
const invalidatables = /* @__PURE__ */ new Set();
|
|
1092
|
+
let disposed = false;
|
|
1093
|
+
let snapshot = null;
|
|
1094
|
+
let opaqueWhite = null;
|
|
1095
|
+
let opaqueWhiteFilled = false;
|
|
1096
|
+
let opaqueWhiteResult = null;
|
|
1097
|
+
function state() {
|
|
1098
|
+
if (disposed) return fail("DISPOSED", "The headless effects stage has been disposed.");
|
|
1099
|
+
if (gl.isContextLost()) return fail("CONTEXT_LOST", "The WebGL context is lost; invalidate and wait for restoration.");
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
function makeTarget() {
|
|
1103
|
+
const target = {
|
|
1104
|
+
texture: null,
|
|
1105
|
+
framebuffer: null,
|
|
1106
|
+
width: 0,
|
|
1107
|
+
height: 0
|
|
1108
|
+
};
|
|
1109
|
+
targets.add(target);
|
|
1110
|
+
return target;
|
|
1111
|
+
}
|
|
1112
|
+
function discardTarget(target, callGl) {
|
|
1113
|
+
if (callGl) {
|
|
1114
|
+
if (target.framebuffer) gl.deleteFramebuffer(target.framebuffer);
|
|
1115
|
+
if (target.texture) gl.deleteTexture(target.texture);
|
|
1116
|
+
}
|
|
1117
|
+
target.texture = null;
|
|
1118
|
+
target.framebuffer = null;
|
|
1119
|
+
target.width = 0;
|
|
1120
|
+
target.height = 0;
|
|
1121
|
+
}
|
|
1122
|
+
function resizeTarget(target, width, height) {
|
|
1123
|
+
const current = state();
|
|
1124
|
+
if (current) return current;
|
|
1125
|
+
const dimensions = targetDimensions(width, height);
|
|
1126
|
+
if (!dimensions) return fail("INVALID_DIMENSIONS", "Effect targets need finite dimensions greater than zero.");
|
|
1127
|
+
const [nextWidth, nextHeight] = dimensions;
|
|
1128
|
+
if (target.texture && target.framebuffer && target.width === nextWidth && target.height === nextHeight) return ok(void 0);
|
|
1129
|
+
const texture = gl.createTexture();
|
|
1130
|
+
const framebuffer = gl.createFramebuffer();
|
|
1131
|
+
if (!texture || !framebuffer) {
|
|
1132
|
+
if (texture) gl.deleteTexture(texture);
|
|
1133
|
+
if (framebuffer) gl.deleteFramebuffer(framebuffer);
|
|
1134
|
+
return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not allocate an RGBA8 effect target.");
|
|
1135
|
+
}
|
|
1136
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
1137
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
1138
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1139
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1140
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1141
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, nextWidth, nextHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
1142
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
1143
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
1144
|
+
const complete = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
|
|
1145
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1146
|
+
if (!complete) {
|
|
1147
|
+
gl.deleteFramebuffer(framebuffer);
|
|
1148
|
+
gl.deleteTexture(texture);
|
|
1149
|
+
return fail("FRAMEBUFFER_INCOMPLETE", "WebGL rejected the RGBA8 effect framebuffer.");
|
|
1150
|
+
}
|
|
1151
|
+
discardTarget(target, true);
|
|
1152
|
+
target.texture = texture;
|
|
1153
|
+
target.framebuffer = framebuffer;
|
|
1154
|
+
target.width = nextWidth;
|
|
1155
|
+
target.height = nextHeight;
|
|
1156
|
+
return ok(void 0);
|
|
1157
|
+
}
|
|
1158
|
+
function copy(source, destination, filter) {
|
|
1159
|
+
const previousRead = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING);
|
|
1160
|
+
const previousDraw = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING);
|
|
1161
|
+
try {
|
|
1162
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
1163
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, source.framebuffer);
|
|
1164
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, destination.framebuffer);
|
|
1165
|
+
gl.blitFramebuffer(0, 0, source.width, source.height, 0, 0, destination.width, destination.height, gl.COLOR_BUFFER_BIT, filter === "linear" ? gl.LINEAR : gl.NEAREST);
|
|
1166
|
+
} finally {
|
|
1167
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, previousRead);
|
|
1168
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, previousDraw);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
function snapshotFor(target) {
|
|
1172
|
+
if (!snapshot) snapshot = makeTarget();
|
|
1173
|
+
const resized = resizeTarget(snapshot, target.width, target.height);
|
|
1174
|
+
if (!resized.ok) return resized;
|
|
1175
|
+
copy(target, snapshot, "nearest");
|
|
1176
|
+
return ok(snapshot);
|
|
1177
|
+
}
|
|
1178
|
+
/** A stage-owned, opaque TEXTURE fallback for ColorRect/pure-fill shaders. */
|
|
1179
|
+
function whiteTarget() {
|
|
1180
|
+
if (!opaqueWhite) opaqueWhite = makeTarget();
|
|
1181
|
+
if (!opaqueWhite.texture || !opaqueWhite.framebuffer) {
|
|
1182
|
+
const resized = resizeTarget(opaqueWhite, 1, 1);
|
|
1183
|
+
if (!resized.ok) return resized;
|
|
1184
|
+
opaqueWhiteFilled = false;
|
|
1185
|
+
}
|
|
1186
|
+
if (opaqueWhiteFilled && opaqueWhiteResult) return opaqueWhiteResult;
|
|
1187
|
+
const previous = gl.getParameter(gl.FRAMEBUFFER_BINDING);
|
|
1188
|
+
try {
|
|
1189
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, opaqueWhite.framebuffer);
|
|
1190
|
+
gl.viewport(0, 0, 1, 1);
|
|
1191
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
1192
|
+
gl.clearColor(1, 1, 1, 1);
|
|
1193
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
1194
|
+
opaqueWhiteFilled = true;
|
|
1195
|
+
opaqueWhiteResult = ok(opaqueWhite);
|
|
1196
|
+
} finally {
|
|
1197
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, previous);
|
|
1198
|
+
}
|
|
1199
|
+
return ok(opaqueWhite);
|
|
1200
|
+
}
|
|
1201
|
+
function snapshotFramebuffer(framebuffer, width, height) {
|
|
1202
|
+
if (!snapshot) snapshot = makeTarget();
|
|
1203
|
+
const resized = resizeTarget(snapshot, width, height);
|
|
1204
|
+
if (!resized.ok) return resized;
|
|
1205
|
+
const previousRead = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING);
|
|
1206
|
+
const previousDraw = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING);
|
|
1207
|
+
try {
|
|
1208
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
1209
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, framebuffer);
|
|
1210
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, snapshot.framebuffer);
|
|
1211
|
+
gl.blitFramebuffer(0, 0, width, height, 0, 0, width, height, gl.COLOR_BUFFER_BIT, gl.NEAREST);
|
|
1212
|
+
} finally {
|
|
1213
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, previousRead);
|
|
1214
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, previousDraw);
|
|
1215
|
+
}
|
|
1216
|
+
return ok(snapshot);
|
|
1217
|
+
}
|
|
1218
|
+
function targetFor(value) {
|
|
1219
|
+
return targets.has(value) ? value : null;
|
|
1220
|
+
}
|
|
1221
|
+
function textureInputFor(value) {
|
|
1222
|
+
const owned = targetFor(value);
|
|
1223
|
+
if (owned?.texture && owned.width > 0 && owned.height > 0) return {
|
|
1224
|
+
texture: owned.texture,
|
|
1225
|
+
width: owned.width,
|
|
1226
|
+
height: owned.height,
|
|
1227
|
+
owned
|
|
1228
|
+
};
|
|
1229
|
+
const borrowed = value;
|
|
1230
|
+
if (!borrowed.texture || !Number.isFinite(borrowed.width) || !Number.isFinite(borrowed.height) || borrowed.width <= 0 || borrowed.height <= 0) return null;
|
|
1231
|
+
return {
|
|
1232
|
+
texture: borrowed.texture,
|
|
1233
|
+
width: borrowed.width,
|
|
1234
|
+
height: borrowed.height,
|
|
1235
|
+
owned: null
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function uniformLocation(program, name) {
|
|
1239
|
+
const known = program.locations.get(name);
|
|
1240
|
+
if (known !== void 0) return known;
|
|
1241
|
+
if (program.locations.has(name)) return null;
|
|
1242
|
+
const location = gl.getUniformLocation(program.program, name);
|
|
1243
|
+
program.locations.set(name, location);
|
|
1244
|
+
return location;
|
|
1245
|
+
}
|
|
1246
|
+
function applyUniform(program, name, value) {
|
|
1247
|
+
const location = uniformLocation(program, name);
|
|
1248
|
+
if (location === null) return;
|
|
1249
|
+
if (typeof value === "number") gl.uniform1f(location, value);
|
|
1250
|
+
else if (value.length === 2) gl.uniform2f(location, value[0], value[1]);
|
|
1251
|
+
else if (value.length === 3) gl.uniform3f(location, value[0], value[1], value[2]);
|
|
1252
|
+
else gl.uniform4f(location, value[0], value[1], value[2], value[3]);
|
|
1253
|
+
}
|
|
1254
|
+
function createShaderProducer(pass, width, height) {
|
|
1255
|
+
const invalid = state();
|
|
1256
|
+
if (invalid) return invalid;
|
|
1257
|
+
const diagnostic = shaderDiagnostic(pass);
|
|
1258
|
+
if (diagnostic) return {
|
|
1259
|
+
ok: false,
|
|
1260
|
+
diagnostic
|
|
1261
|
+
};
|
|
1262
|
+
const target = makeTarget();
|
|
1263
|
+
const allocated = resizeTarget(target, width, height);
|
|
1264
|
+
if (!allocated.ok) {
|
|
1265
|
+
targets.delete(target);
|
|
1266
|
+
return allocated;
|
|
1267
|
+
}
|
|
1268
|
+
let requestedWidth = target.width;
|
|
1269
|
+
let requestedHeight = target.height;
|
|
1270
|
+
const resolution = [target.width, target.height];
|
|
1271
|
+
const uniformEntries = Object.entries(pass.uniforms ?? {});
|
|
1272
|
+
const rendered = ok(target);
|
|
1273
|
+
let program = null;
|
|
1274
|
+
let producerDisposed = false;
|
|
1275
|
+
const needsScreen = pass.features?.includes("screen-texture") === true;
|
|
1276
|
+
function ensureProgram() {
|
|
1277
|
+
if (program) return ok(program);
|
|
1278
|
+
const compiled = compileProgram$1(gl, FULLSCREEN_VERTEX, pass.fragmentSource);
|
|
1279
|
+
if (compiled.ok) program = compiled.value;
|
|
1280
|
+
return compiled;
|
|
1281
|
+
}
|
|
1282
|
+
const producer = {
|
|
1283
|
+
target,
|
|
1284
|
+
warmUp() {
|
|
1285
|
+
const current = state();
|
|
1286
|
+
if (current) return current;
|
|
1287
|
+
if (producerDisposed) return fail("DISPOSED", "The shader producer has been disposed.");
|
|
1288
|
+
const ready = ensureProgram();
|
|
1289
|
+
return ready.ok ? ok(void 0) : ready;
|
|
1290
|
+
},
|
|
1291
|
+
resize(nextWidth, nextHeight) {
|
|
1292
|
+
if (producerDisposed) return fail("DISPOSED", "The shader producer has been disposed.");
|
|
1293
|
+
const resized = resizeTarget(target, nextWidth, nextHeight);
|
|
1294
|
+
if (resized.ok) {
|
|
1295
|
+
requestedWidth = target.width;
|
|
1296
|
+
requestedHeight = target.height;
|
|
1297
|
+
resolution[0] = target.width;
|
|
1298
|
+
resolution[1] = target.height;
|
|
1299
|
+
}
|
|
1300
|
+
return resized;
|
|
1301
|
+
},
|
|
1302
|
+
render(input) {
|
|
1303
|
+
const inputDiagnostic = renderInputDiagnostic(input);
|
|
1304
|
+
if (inputDiagnostic) return {
|
|
1305
|
+
ok: false,
|
|
1306
|
+
diagnostic: inputDiagnostic
|
|
1307
|
+
};
|
|
1308
|
+
const current = state();
|
|
1309
|
+
if (current) return current;
|
|
1310
|
+
if (producerDisposed) return fail("DISPOSED", "The shader producer has been disposed.");
|
|
1311
|
+
if (!target.texture || !target.framebuffer) {
|
|
1312
|
+
const reallocated = resizeTarget(target, requestedWidth, requestedHeight);
|
|
1313
|
+
if (!reallocated.ok) return reallocated;
|
|
1314
|
+
}
|
|
1315
|
+
let screen = null;
|
|
1316
|
+
if (needsScreen) {
|
|
1317
|
+
if (!input.screenTexture) return fail("SCREEN_TEXTURE_REQUIRED", "This shader pass declared screen-texture but no accumulated target was supplied.");
|
|
1318
|
+
screen = targetFor(input.screenTexture);
|
|
1319
|
+
if (!screen?.texture || !screen.framebuffer) return fail("FOREIGN_TARGET", "screenTexture must be a live target owned by this headless effects stage.");
|
|
1320
|
+
if (screen === target) {
|
|
1321
|
+
const captured = snapshotFor(screen);
|
|
1322
|
+
if (!captured.ok) return captured;
|
|
1323
|
+
screen = captured.value;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
const ready = ensureProgram();
|
|
1327
|
+
if (!ready.ok) return ready;
|
|
1328
|
+
const previousFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
|
|
1329
|
+
try {
|
|
1330
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
|
|
1331
|
+
gl.viewport(0, 0, target.width, target.height);
|
|
1332
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
1333
|
+
gl.clearColor(0, 0, 0, 0);
|
|
1334
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
1335
|
+
gl.enable(gl.BLEND);
|
|
1336
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
1337
|
+
gl.useProgram(ready.value.program);
|
|
1338
|
+
gl.bindVertexArray(ready.value.vao);
|
|
1339
|
+
applyUniform(ready.value, "u_time", input.time);
|
|
1340
|
+
applyUniform(ready.value, "u_delta", input.delta);
|
|
1341
|
+
applyUniform(ready.value, "u_seed", input.seed ?? 0);
|
|
1342
|
+
applyUniform(ready.value, "u_resolution", resolution);
|
|
1343
|
+
for (const [name, value] of uniformEntries) applyUniform(ready.value, name, value);
|
|
1344
|
+
if (screen) {
|
|
1345
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1346
|
+
gl.bindTexture(gl.TEXTURE_2D, screen.texture);
|
|
1347
|
+
const location = uniformLocation(ready.value, "u_screenTexture");
|
|
1348
|
+
if (location) gl.uniform1i(location, 0);
|
|
1349
|
+
}
|
|
1350
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
1351
|
+
gl.bindVertexArray(null);
|
|
1352
|
+
return rendered;
|
|
1353
|
+
} finally {
|
|
1354
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, previousFramebuffer);
|
|
1355
|
+
}
|
|
1356
|
+
},
|
|
1357
|
+
renderScreen(input, context) {
|
|
1358
|
+
const inputDiagnostic = renderInputDiagnostic(input);
|
|
1359
|
+
if (inputDiagnostic) return {
|
|
1360
|
+
ok: false,
|
|
1361
|
+
diagnostic: inputDiagnostic
|
|
1362
|
+
};
|
|
1363
|
+
if (!needsScreen) return fail("UNSUPPORTED_SHADER_FEATURE", "Only a screen-texture pass can be recorded as a screen effect.", "screen-texture");
|
|
1364
|
+
if (context.damage) return fail("UNSUPPORTED_SHADER_FEATURE", "Screen-dependent effects decline partial damage execution.", "partial-damage");
|
|
1365
|
+
const resized = producer.resize(context.width, context.height);
|
|
1366
|
+
if (!resized.ok) return resized;
|
|
1367
|
+
const captured = snapshotFramebuffer(context.framebuffer, context.width, context.height);
|
|
1368
|
+
if (!captured.ok) return captured;
|
|
1369
|
+
const renderedScreen = producer.render({
|
|
1370
|
+
time: input.time,
|
|
1371
|
+
delta: input.delta,
|
|
1372
|
+
seed: input.seed,
|
|
1373
|
+
screenTexture: captured.value
|
|
1374
|
+
});
|
|
1375
|
+
if (!renderedScreen.ok) return renderedScreen;
|
|
1376
|
+
const previousRead = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING);
|
|
1377
|
+
const previousDraw = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING);
|
|
1378
|
+
try {
|
|
1379
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, target.framebuffer);
|
|
1380
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, context.framebuffer);
|
|
1381
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
1382
|
+
gl.scissor(context.scissor.x, context.scissor.y, context.scissor.width, context.scissor.height);
|
|
1383
|
+
gl.blitFramebuffer(0, 0, target.width, target.height, 0, 0, context.width, context.height, gl.COLOR_BUFFER_BIT, gl.NEAREST);
|
|
1384
|
+
} finally {
|
|
1385
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, previousRead);
|
|
1386
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, previousDraw);
|
|
1387
|
+
}
|
|
1388
|
+
return ok(void 0);
|
|
1389
|
+
},
|
|
1390
|
+
invalidate() {
|
|
1391
|
+
destroyProgram(gl, program);
|
|
1392
|
+
program = null;
|
|
1393
|
+
discardTarget(target, true);
|
|
1394
|
+
},
|
|
1395
|
+
invalidateContextLoss() {
|
|
1396
|
+
program = null;
|
|
1397
|
+
discardTarget(target, false);
|
|
1398
|
+
},
|
|
1399
|
+
dispose() {
|
|
1400
|
+
if (!producerDisposed) {
|
|
1401
|
+
producerDisposed = true;
|
|
1402
|
+
destroyProgram(gl, program);
|
|
1403
|
+
program = null;
|
|
1404
|
+
discardTarget(target, true);
|
|
1405
|
+
targets.delete(target);
|
|
1406
|
+
invalidatables.delete(producer);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
invalidatables.add(producer);
|
|
1411
|
+
return ok(producer);
|
|
1412
|
+
}
|
|
1413
|
+
function createGodotShaderProducer(pass, width, height) {
|
|
1414
|
+
const invalid = state();
|
|
1415
|
+
if (invalid) return invalid;
|
|
1416
|
+
let transpiled;
|
|
1417
|
+
try {
|
|
1418
|
+
transpiled = transpileGodotShader(pass.source);
|
|
1419
|
+
} catch (error) {
|
|
1420
|
+
return fail("UNSUPPORTED_SHADER_FEATURE", error instanceof UnsupportedShaderError ? error.message : `Godot shader transpilation failed: ${error instanceof Error ? error.message : String(error)}`, "godot-source");
|
|
1421
|
+
}
|
|
1422
|
+
const suppliedSamplers = { ...pass.samplers ?? {} };
|
|
1423
|
+
for (const sampler of transpiled.samplers) {
|
|
1424
|
+
if (!suppliedSamplers[sampler.name]) return fail("UNSUPPORTED_SHADER_FEATURE", `Godot sampler ${sampler.name} needs an explicit stage-owned target.`, `sampler:${sampler.name}`);
|
|
1425
|
+
if (sampler.repeat) return fail("UNSUPPORTED_SHADER_FEATURE", `Godot sampler ${sampler.name} requests repeat sampling, which stage targets cannot mutate.`, `sampler:${sampler.name}:repeat`);
|
|
1426
|
+
}
|
|
1427
|
+
for (const uniform of transpiled.uniforms) if (uniform.arrayLength || ![
|
|
1428
|
+
"float",
|
|
1429
|
+
"int",
|
|
1430
|
+
"bool",
|
|
1431
|
+
"vec2",
|
|
1432
|
+
"vec3",
|
|
1433
|
+
"vec4"
|
|
1434
|
+
].includes(uniform.type)) return fail("UNSUPPORTED_SHADER_FEATURE", `Godot uniform ${uniform.name} (${uniform.type}) is not supported headlessly.`, `uniform:${uniform.name}`);
|
|
1435
|
+
const componentCount = (type) => type === "vec2" ? 2 : type === "vec3" ? 3 : type === "vec4" ? 4 : 1;
|
|
1436
|
+
const uniformValues = {};
|
|
1437
|
+
for (const uniform of transpiled.uniforms) {
|
|
1438
|
+
const raw = pass.uniforms?.[uniform.name] ?? uniform.default;
|
|
1439
|
+
if (raw === void 0) continue;
|
|
1440
|
+
const expected = componentCount(uniform.type);
|
|
1441
|
+
const values = typeof raw === "number" ? null : raw;
|
|
1442
|
+
if (expected === 1 && typeof raw !== "number" || expected !== 1 && (!values || values.length !== expected) || typeof raw === "number" && !Number.isFinite(raw) || values && !values.every(Number.isFinite)) return fail("UNSUPPORTED_SHADER_FEATURE", `Godot uniform ${uniform.name} must be ${expected} finite numeric component${expected === 1 ? "" : "s"}.`, `uniform:${uniform.name}`);
|
|
1443
|
+
uniformValues[uniform.name] = typeof raw === "number" ? raw : [...raw];
|
|
1444
|
+
}
|
|
1445
|
+
const fit = [...pass.uvFit ?? GODOT_DEFAULT_FIT];
|
|
1446
|
+
const window = [...pass.uvWindow ?? GODOT_DEFAULT_WINDOW];
|
|
1447
|
+
const modulate = [...pass.modulate ?? GODOT_DEFAULT_MODULATE];
|
|
1448
|
+
if (!fit.every(Number.isFinite) || !window.every(Number.isFinite) || !modulate.every(Number.isFinite)) return fail("UNSUPPORTED_SHADER_FEATURE", "Godot shader fit, window, and modulate values must be finite.", "render-parameters");
|
|
1449
|
+
const target = makeTarget();
|
|
1450
|
+
const allocated = resizeTarget(target, width, height);
|
|
1451
|
+
if (!allocated.ok) {
|
|
1452
|
+
targets.delete(target);
|
|
1453
|
+
return allocated;
|
|
1454
|
+
}
|
|
1455
|
+
let requestedWidth = target.width;
|
|
1456
|
+
let requestedHeight = target.height;
|
|
1457
|
+
let program = null;
|
|
1458
|
+
let producerDisposed = false;
|
|
1459
|
+
const rendered = ok(target);
|
|
1460
|
+
const noScreen = ok(null);
|
|
1461
|
+
const texturePixelSize = [1, 1];
|
|
1462
|
+
const screenPixelSize = [1, 1];
|
|
1463
|
+
const screenOrigin = [0, 0];
|
|
1464
|
+
const screenSize = [1, 1];
|
|
1465
|
+
const ensureProgram = () => {
|
|
1466
|
+
if (program) return ok(program);
|
|
1467
|
+
const compiled = compileProgram$1(gl, FULLSCREEN_VERTEX, transpiled.fragmentGlsl);
|
|
1468
|
+
if (compiled.ok) program = compiled.value;
|
|
1469
|
+
return compiled;
|
|
1470
|
+
};
|
|
1471
|
+
const stageTarget = (value, label) => {
|
|
1472
|
+
if (!value) return ok(null);
|
|
1473
|
+
const texture = textureInputFor(value);
|
|
1474
|
+
return texture ? ok(texture) : fail("FOREIGN_TARGET", `${label} must be a live same-context stage target or texture handle.`);
|
|
1475
|
+
};
|
|
1476
|
+
const suppliedTexture = stageTarget(pass.texture, "TEXTURE");
|
|
1477
|
+
if (!suppliedTexture.ok) {
|
|
1478
|
+
discardTarget(target, true);
|
|
1479
|
+
targets.delete(target);
|
|
1480
|
+
return suppliedTexture;
|
|
1481
|
+
}
|
|
1482
|
+
const baseTexture = suppliedTexture.value;
|
|
1483
|
+
const samplerTargets = [];
|
|
1484
|
+
for (const sampler of transpiled.samplers) {
|
|
1485
|
+
const supplied = stageTarget(suppliedSamplers[sampler.name], `sampler:${sampler.name}`);
|
|
1486
|
+
if (!supplied.ok || !supplied.value) {
|
|
1487
|
+
discardTarget(target, true);
|
|
1488
|
+
targets.delete(target);
|
|
1489
|
+
return supplied.ok ? fail("UNSUPPORTED_SHADER_FEATURE", `Godot sampler ${sampler.name} needs an explicit target.`, `sampler:${sampler.name}`) : supplied;
|
|
1490
|
+
}
|
|
1491
|
+
samplerTargets.push(supplied.value);
|
|
1492
|
+
}
|
|
1493
|
+
const applyGodotUniform = (p, uniform) => {
|
|
1494
|
+
const raw = uniformValues[uniform.name];
|
|
1495
|
+
const loc = uniformLocation(p, uniform.name);
|
|
1496
|
+
if (!loc) return;
|
|
1497
|
+
if (typeof raw === "number") {
|
|
1498
|
+
if (uniform.type === "float") gl.uniform1f(loc, raw);
|
|
1499
|
+
else if (uniform.type === "int" || uniform.type === "bool") gl.uniform1i(loc, Math.round(raw));
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
const values = raw ?? [];
|
|
1503
|
+
if (uniform.type === "float") gl.uniform1f(loc, 0);
|
|
1504
|
+
else if (uniform.type === "int" || uniform.type === "bool") gl.uniform1i(loc, 0);
|
|
1505
|
+
else if (uniform.type === "vec2") gl.uniform2f(loc, values[0] ?? 0, values[1] ?? 0);
|
|
1506
|
+
else if (uniform.type === "vec3") gl.uniform3f(loc, values[0] ?? 0, values[1] ?? 0, values[2] ?? 0);
|
|
1507
|
+
else gl.uniform4f(loc, values[0] ?? 0, values[1] ?? 0, values[2] ?? 0, values[3] ?? 0);
|
|
1508
|
+
};
|
|
1509
|
+
const liveDependencies = ok(void 0);
|
|
1510
|
+
const dependenciesLive = () => {
|
|
1511
|
+
if (baseTexture && (baseTexture.owned ? !targets.has(baseTexture.owned) || !baseTexture.owned.texture || !baseTexture.owned.framebuffer : typeof gl.isTexture === "function" && !gl.isTexture(baseTexture.texture))) return fail("FOREIGN_TARGET", "A Godot shader dependency is no longer a live stage-owned target.", "dependency");
|
|
1512
|
+
for (let i = 0; i < samplerTargets.length; i += 1) {
|
|
1513
|
+
const dependency = samplerTargets[i];
|
|
1514
|
+
if (dependency.owned ? !targets.has(dependency.owned) || !dependency.owned.texture || !dependency.owned.framebuffer : typeof gl.isTexture === "function" && !gl.isTexture(dependency.texture)) return fail("FOREIGN_TARGET", "A Godot shader dependency is no longer a live stage-owned target.", "dependency");
|
|
1515
|
+
}
|
|
1516
|
+
return liveDependencies;
|
|
1517
|
+
};
|
|
1518
|
+
const producer = {
|
|
1519
|
+
target,
|
|
1520
|
+
blend: transpiled.blend,
|
|
1521
|
+
warmUp() {
|
|
1522
|
+
const current = state();
|
|
1523
|
+
if (current) return current;
|
|
1524
|
+
if (producerDisposed) return fail("DISPOSED", "The Godot shader producer has been disposed.");
|
|
1525
|
+
const ready = ensureProgram();
|
|
1526
|
+
return ready.ok ? ok(void 0) : ready;
|
|
1527
|
+
},
|
|
1528
|
+
resize(nextWidth, nextHeight) {
|
|
1529
|
+
if (producerDisposed) return fail("DISPOSED", "The Godot shader producer has been disposed.");
|
|
1530
|
+
const resized = resizeTarget(target, nextWidth, nextHeight);
|
|
1531
|
+
if (resized.ok) {
|
|
1532
|
+
requestedWidth = target.width;
|
|
1533
|
+
requestedHeight = target.height;
|
|
1534
|
+
}
|
|
1535
|
+
return resized;
|
|
1536
|
+
},
|
|
1537
|
+
render(input) {
|
|
1538
|
+
const inputDiagnostic = renderInputDiagnostic(input);
|
|
1539
|
+
if (inputDiagnostic) return {
|
|
1540
|
+
ok: false,
|
|
1541
|
+
diagnostic: inputDiagnostic
|
|
1542
|
+
};
|
|
1543
|
+
if (input.screenRect && !input.screenRect.every(Number.isFinite)) return fail("INVALID_RENDER_INPUT", "Godot screenRect values must be finite.", "screen-rect");
|
|
1544
|
+
const current = state();
|
|
1545
|
+
if (current) return current;
|
|
1546
|
+
if (producerDisposed) return fail("DISPOSED", "The Godot shader producer has been disposed.");
|
|
1547
|
+
if (!target.texture || !target.framebuffer) {
|
|
1548
|
+
const resized = resizeTarget(target, requestedWidth, requestedHeight);
|
|
1549
|
+
if (!resized.ok) return resized;
|
|
1550
|
+
}
|
|
1551
|
+
const dependencies = dependenciesLive();
|
|
1552
|
+
if (!dependencies.ok) return dependencies;
|
|
1553
|
+
const white = baseTexture ? null : whiteTarget();
|
|
1554
|
+
if (white && !white.ok) return white;
|
|
1555
|
+
const texture = baseTexture ?? white?.value;
|
|
1556
|
+
const screen = transpiled.usesScreenTexture ? stageTarget(input.screenTexture, "SCREEN_TEXTURE") : noScreen;
|
|
1557
|
+
if (!screen.ok) return screen;
|
|
1558
|
+
if (transpiled.usesScreenTexture && !screen.value) return fail("SCREEN_TEXTURE_REQUIRED", "This Godot shader samples SCREEN_TEXTURE but no accumulated target was supplied.");
|
|
1559
|
+
const screenTarget = screen.value === target ? snapshotFor(target) : screen;
|
|
1560
|
+
if (!screenTarget.ok) return screenTarget;
|
|
1561
|
+
const ready = ensureProgram();
|
|
1562
|
+
if (!ready.ok) return ready;
|
|
1563
|
+
const previous = gl.getParameter(gl.FRAMEBUFFER_BINDING);
|
|
1564
|
+
try {
|
|
1565
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
|
|
1566
|
+
gl.viewport(0, 0, target.width, target.height);
|
|
1567
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
1568
|
+
gl.disable(gl.BLEND);
|
|
1569
|
+
gl.clearColor(0, 0, 0, 0);
|
|
1570
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
1571
|
+
gl.useProgram(ready.value.program);
|
|
1572
|
+
gl.bindVertexArray(ready.value.vao);
|
|
1573
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1574
|
+
gl.bindTexture(gl.TEXTURE_2D, texture?.texture ?? null);
|
|
1575
|
+
const textureLoc = uniformLocation(ready.value, "TEXTURE");
|
|
1576
|
+
if (textureLoc) gl.uniform1i(textureLoc, 0);
|
|
1577
|
+
if (transpiled.usesTime) applyUniform(ready.value, "TIME", input.time);
|
|
1578
|
+
if (transpiled.usesTexturePixelSize) {
|
|
1579
|
+
texturePixelSize[0] = 1 / Math.max(1, texture?.width ?? 1);
|
|
1580
|
+
texturePixelSize[1] = 1 / Math.max(1, texture?.height ?? 1);
|
|
1581
|
+
applyUniform(ready.value, "TEXTURE_PIXEL_SIZE", texturePixelSize);
|
|
1582
|
+
}
|
|
1583
|
+
applyUniform(ready.value, "MODULATE", modulate);
|
|
1584
|
+
applyUniform(ready.value, "_godot_uv_fit", fit);
|
|
1585
|
+
applyUniform(ready.value, "_godot_uv_window", window);
|
|
1586
|
+
const rect = input.screenRect ?? GODOT_DEFAULT_SCREEN_RECT;
|
|
1587
|
+
if (transpiled.usesScreenUv) {
|
|
1588
|
+
screenOrigin[0] = rect[0];
|
|
1589
|
+
screenOrigin[1] = rect[1];
|
|
1590
|
+
screenSize[0] = rect[2];
|
|
1591
|
+
screenSize[1] = rect[3];
|
|
1592
|
+
applyUniform(ready.value, "_godot_screen_origin", screenOrigin);
|
|
1593
|
+
applyUniform(ready.value, "_godot_screen_size", screenSize);
|
|
1594
|
+
}
|
|
1595
|
+
if (screenTarget.value) {
|
|
1596
|
+
gl.activeTexture(gl.TEXTURE0 + 1);
|
|
1597
|
+
gl.bindTexture(gl.TEXTURE_2D, screenTarget.value.texture);
|
|
1598
|
+
const loc = uniformLocation(ready.value, "SCREEN_TEXTURE");
|
|
1599
|
+
if (loc) gl.uniform1i(loc, 1);
|
|
1600
|
+
}
|
|
1601
|
+
if (transpiled.usesScreenPixelSize) {
|
|
1602
|
+
screenPixelSize[0] = 1 / Math.max(1, screenTarget.value?.width ?? target.width);
|
|
1603
|
+
screenPixelSize[1] = 1 / Math.max(1, screenTarget.value?.height ?? target.height);
|
|
1604
|
+
applyUniform(ready.value, "SCREEN_PIXEL_SIZE", screenPixelSize);
|
|
1605
|
+
}
|
|
1606
|
+
let unit = 2;
|
|
1607
|
+
for (let i = 0; i < transpiled.samplers.length; i += 1) {
|
|
1608
|
+
const sampler = transpiled.samplers[i];
|
|
1609
|
+
const value = samplerTargets[i];
|
|
1610
|
+
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
1611
|
+
gl.bindTexture(gl.TEXTURE_2D, value.texture);
|
|
1612
|
+
const loc = uniformLocation(ready.value, sampler.name);
|
|
1613
|
+
if (loc) gl.uniform1i(loc, unit++);
|
|
1614
|
+
}
|
|
1615
|
+
for (const uniform of transpiled.uniforms) applyGodotUniform(ready.value, uniform);
|
|
1616
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
1617
|
+
gl.bindVertexArray(null);
|
|
1618
|
+
return rendered;
|
|
1619
|
+
} finally {
|
|
1620
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, previous);
|
|
1621
|
+
}
|
|
1622
|
+
},
|
|
1623
|
+
renderScreen(input, context) {
|
|
1624
|
+
if (!transpiled.usesScreenTexture) return fail("UNSUPPORTED_SHADER_FEATURE", "Only a Godot SCREEN_TEXTURE shader can be recorded as a screen effect.", "screen-texture");
|
|
1625
|
+
if (context.damage) return fail("UNSUPPORTED_SHADER_FEATURE", "Screen-dependent effects decline partial damage execution.", "partial-damage");
|
|
1626
|
+
const dependencies = dependenciesLive();
|
|
1627
|
+
if (!dependencies.ok) return dependencies;
|
|
1628
|
+
const rect = input.screenRect;
|
|
1629
|
+
if (!rect?.every(Number.isFinite) || rect[0] < 0 || rect[1] < 0 || rect[2] <= 0 || rect[3] <= 0 || rect[0] + rect[2] > 1 || rect[1] + rect[3] > 1) return fail("UNSUPPORTED_SHADER_FEATURE", "Screen shaders need a finite, normalized axis-aligned screenRect.", "screen-rect");
|
|
1630
|
+
const resized = producer.resize(context.width, context.height);
|
|
1631
|
+
if (!resized.ok) return resized;
|
|
1632
|
+
const captured = snapshotFramebuffer(context.framebuffer, context.width, context.height);
|
|
1633
|
+
if (!captured.ok) return captured;
|
|
1634
|
+
copy(captured.value, target, "nearest");
|
|
1635
|
+
const x = Math.round(rect[0] * context.width);
|
|
1636
|
+
const x1 = Math.round((rect[0] + rect[2]) * context.width);
|
|
1637
|
+
const topY = Math.round(rect[1] * context.height);
|
|
1638
|
+
const bottomY = Math.round((rect[1] + rect[3]) * context.height);
|
|
1639
|
+
const y = context.height - bottomY;
|
|
1640
|
+
const width = x1 - x;
|
|
1641
|
+
const height = bottomY - topY;
|
|
1642
|
+
const left = Math.max(x, context.scissor.x);
|
|
1643
|
+
const bottom = Math.max(y, context.scissor.y);
|
|
1644
|
+
const right = Math.min(x1, context.scissor.x + context.scissor.width);
|
|
1645
|
+
const top = Math.min(context.height - topY, context.scissor.y + context.scissor.height);
|
|
1646
|
+
if (right <= left || top <= bottom) return ok(void 0);
|
|
1647
|
+
const ready = ensureProgram();
|
|
1648
|
+
if (!ready.ok) return ready;
|
|
1649
|
+
const white = baseTexture ? null : whiteTarget();
|
|
1650
|
+
if (white && !white.ok) return white;
|
|
1651
|
+
const texture = baseTexture ?? white?.value;
|
|
1652
|
+
const previous = gl.getParameter(gl.FRAMEBUFFER_BINDING);
|
|
1653
|
+
try {
|
|
1654
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
|
|
1655
|
+
gl.viewport(x, y, width, height);
|
|
1656
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
1657
|
+
gl.scissor(left, bottom, right - left, top - bottom);
|
|
1658
|
+
applyGodotBlend(gl, transpiled.blend);
|
|
1659
|
+
gl.useProgram(ready.value.program);
|
|
1660
|
+
gl.bindVertexArray(ready.value.vao);
|
|
1661
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1662
|
+
gl.bindTexture(gl.TEXTURE_2D, texture?.texture ?? null);
|
|
1663
|
+
const textureLoc = uniformLocation(ready.value, "TEXTURE");
|
|
1664
|
+
if (textureLoc) gl.uniform1i(textureLoc, 0);
|
|
1665
|
+
if (transpiled.usesTime) applyUniform(ready.value, "TIME", input.time);
|
|
1666
|
+
if (transpiled.usesTexturePixelSize) {
|
|
1667
|
+
texturePixelSize[0] = 1 / Math.max(1, texture?.width ?? 1);
|
|
1668
|
+
texturePixelSize[1] = 1 / Math.max(1, texture?.height ?? 1);
|
|
1669
|
+
applyUniform(ready.value, "TEXTURE_PIXEL_SIZE", texturePixelSize);
|
|
1670
|
+
}
|
|
1671
|
+
applyUniform(ready.value, "MODULATE", modulate);
|
|
1672
|
+
applyUniform(ready.value, "_godot_uv_fit", fit);
|
|
1673
|
+
applyUniform(ready.value, "_godot_uv_window", window);
|
|
1674
|
+
screenOrigin[0] = rect[0];
|
|
1675
|
+
screenOrigin[1] = rect[1];
|
|
1676
|
+
screenSize[0] = rect[2];
|
|
1677
|
+
screenSize[1] = rect[3];
|
|
1678
|
+
if (transpiled.usesScreenUv) {
|
|
1679
|
+
applyUniform(ready.value, "_godot_screen_origin", screenOrigin);
|
|
1680
|
+
applyUniform(ready.value, "_godot_screen_size", screenSize);
|
|
1681
|
+
}
|
|
1682
|
+
gl.activeTexture(gl.TEXTURE0 + 1);
|
|
1683
|
+
gl.bindTexture(gl.TEXTURE_2D, captured.value.texture);
|
|
1684
|
+
const screenLoc = uniformLocation(ready.value, "SCREEN_TEXTURE");
|
|
1685
|
+
if (screenLoc) gl.uniform1i(screenLoc, 1);
|
|
1686
|
+
if (transpiled.usesScreenPixelSize) {
|
|
1687
|
+
screenPixelSize[0] = 1 / context.width;
|
|
1688
|
+
screenPixelSize[1] = 1 / context.height;
|
|
1689
|
+
applyUniform(ready.value, "SCREEN_PIXEL_SIZE", screenPixelSize);
|
|
1690
|
+
}
|
|
1691
|
+
let unit = 2;
|
|
1692
|
+
for (let i = 0; i < transpiled.samplers.length; i += 1) {
|
|
1693
|
+
const sampler = transpiled.samplers[i];
|
|
1694
|
+
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
1695
|
+
gl.bindTexture(gl.TEXTURE_2D, samplerTargets[i].texture);
|
|
1696
|
+
const loc = uniformLocation(ready.value, sampler.name);
|
|
1697
|
+
if (loc) gl.uniform1i(loc, unit++);
|
|
1698
|
+
}
|
|
1699
|
+
for (const uniform of transpiled.uniforms) applyGodotUniform(ready.value, uniform);
|
|
1700
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
1701
|
+
gl.bindVertexArray(null);
|
|
1702
|
+
} finally {
|
|
1703
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, previous);
|
|
1704
|
+
}
|
|
1705
|
+
const previousRead = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING);
|
|
1706
|
+
const previousDraw = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING);
|
|
1707
|
+
try {
|
|
1708
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, target.framebuffer);
|
|
1709
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, context.framebuffer);
|
|
1710
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
1711
|
+
gl.scissor(context.scissor.x, context.scissor.y, context.scissor.width, context.scissor.height);
|
|
1712
|
+
gl.blitFramebuffer(0, 0, target.width, target.height, 0, 0, context.width, context.height, gl.COLOR_BUFFER_BIT, gl.NEAREST);
|
|
1713
|
+
} finally {
|
|
1714
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, previousRead);
|
|
1715
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, previousDraw);
|
|
1716
|
+
}
|
|
1717
|
+
return ok(void 0);
|
|
1718
|
+
},
|
|
1719
|
+
invalidate() {
|
|
1720
|
+
destroyProgram(gl, program);
|
|
1721
|
+
program = null;
|
|
1722
|
+
discardTarget(target, true);
|
|
1723
|
+
},
|
|
1724
|
+
invalidateContextLoss() {
|
|
1725
|
+
program = null;
|
|
1726
|
+
discardTarget(target, false);
|
|
1727
|
+
},
|
|
1728
|
+
dispose() {
|
|
1729
|
+
if (!producerDisposed) {
|
|
1730
|
+
producerDisposed = true;
|
|
1731
|
+
destroyProgram(gl, program);
|
|
1732
|
+
program = null;
|
|
1733
|
+
discardTarget(target, true);
|
|
1734
|
+
targets.delete(target);
|
|
1735
|
+
invalidatables.delete(producer);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
invalidatables.add(producer);
|
|
1740
|
+
return ok(producer);
|
|
1741
|
+
}
|
|
1742
|
+
function createParticleProducer(parameters, width, height) {
|
|
1743
|
+
const invalid = state();
|
|
1744
|
+
if (invalid) return invalid;
|
|
1745
|
+
const diagnostic = particleDiagnostic(parameters);
|
|
1746
|
+
if (diagnostic) return {
|
|
1747
|
+
ok: false,
|
|
1748
|
+
diagnostic
|
|
1749
|
+
};
|
|
1750
|
+
const target = makeTarget();
|
|
1751
|
+
const allocated = resizeTarget(target, width, height);
|
|
1752
|
+
if (!allocated.ok) {
|
|
1753
|
+
targets.delete(target);
|
|
1754
|
+
return allocated;
|
|
1755
|
+
}
|
|
1756
|
+
let requestedWidth = target.width;
|
|
1757
|
+
let requestedHeight = target.height;
|
|
1758
|
+
const resolution = [target.width, target.height];
|
|
1759
|
+
const speed = [parameters.speedMin, parameters.speedMax];
|
|
1760
|
+
const size = [parameters.startSizePx, parameters.endSizePx];
|
|
1761
|
+
const rendered = ok(target);
|
|
1762
|
+
let program = null;
|
|
1763
|
+
let producerDisposed = false;
|
|
1764
|
+
const count = Math.min(Math.floor(parameters.maxParticles), Math.max(1, Math.ceil(parameters.emissionRate * parameters.lifetimeSeconds)));
|
|
1765
|
+
function ensureProgram() {
|
|
1766
|
+
if (program) return ok(program);
|
|
1767
|
+
const compiled = compileProgram$1(gl, PARTICLE_VERTEX, PARTICLE_FRAGMENT);
|
|
1768
|
+
if (compiled.ok) program = compiled.value;
|
|
1769
|
+
return compiled;
|
|
1770
|
+
}
|
|
1771
|
+
const producer = {
|
|
1772
|
+
target,
|
|
1773
|
+
warmUp() {
|
|
1774
|
+
const current = state();
|
|
1775
|
+
if (current) return current;
|
|
1776
|
+
if (producerDisposed) return fail("DISPOSED", "The particle producer has been disposed.");
|
|
1777
|
+
const ready = ensureProgram();
|
|
1778
|
+
return ready.ok ? ok(void 0) : ready;
|
|
1779
|
+
},
|
|
1780
|
+
resize(nextWidth, nextHeight) {
|
|
1781
|
+
if (producerDisposed) return fail("DISPOSED", "The particle producer has been disposed.");
|
|
1782
|
+
const resized = resizeTarget(target, nextWidth, nextHeight);
|
|
1783
|
+
if (resized.ok) {
|
|
1784
|
+
requestedWidth = target.width;
|
|
1785
|
+
requestedHeight = target.height;
|
|
1786
|
+
resolution[0] = target.width;
|
|
1787
|
+
resolution[1] = target.height;
|
|
1788
|
+
}
|
|
1789
|
+
return resized;
|
|
1790
|
+
},
|
|
1791
|
+
render(input) {
|
|
1792
|
+
const inputDiagnostic = renderInputDiagnostic(input);
|
|
1793
|
+
if (inputDiagnostic) return {
|
|
1794
|
+
ok: false,
|
|
1795
|
+
diagnostic: inputDiagnostic
|
|
1796
|
+
};
|
|
1797
|
+
const current = state();
|
|
1798
|
+
if (current) return current;
|
|
1799
|
+
if (producerDisposed) return fail("DISPOSED", "The particle producer has been disposed.");
|
|
1800
|
+
if (!target.framebuffer) {
|
|
1801
|
+
const reallocated = resizeTarget(target, requestedWidth, requestedHeight);
|
|
1802
|
+
if (!reallocated.ok) return reallocated;
|
|
1803
|
+
}
|
|
1804
|
+
const ready = ensureProgram();
|
|
1805
|
+
if (!ready.ok) return ready;
|
|
1806
|
+
const previousFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
|
|
1807
|
+
try {
|
|
1808
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
|
|
1809
|
+
gl.viewport(0, 0, target.width, target.height);
|
|
1810
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
1811
|
+
gl.clearColor(0, 0, 0, 0);
|
|
1812
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
1813
|
+
gl.enable(gl.BLEND);
|
|
1814
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
1815
|
+
gl.useProgram(ready.value.program);
|
|
1816
|
+
gl.bindVertexArray(ready.value.vao);
|
|
1817
|
+
applyUniform(ready.value, "u_time", input.time);
|
|
1818
|
+
applyUniform(ready.value, "u_delta", input.delta);
|
|
1819
|
+
applyUniform(ready.value, "u_seed", input.seed);
|
|
1820
|
+
applyUniform(ready.value, "u_lifetime", parameters.lifetimeSeconds);
|
|
1821
|
+
applyUniform(ready.value, "u_emission_rate", parameters.emissionRate);
|
|
1822
|
+
applyUniform(ready.value, "u_max_particles", parameters.maxParticles);
|
|
1823
|
+
applyUniform(ready.value, "u_position", parameters.position);
|
|
1824
|
+
applyUniform(ready.value, "u_direction", parameters.directionRadians);
|
|
1825
|
+
applyUniform(ready.value, "u_spread", parameters.spreadRadians);
|
|
1826
|
+
applyUniform(ready.value, "u_speed", speed);
|
|
1827
|
+
applyUniform(ready.value, "u_gravity", parameters.gravity);
|
|
1828
|
+
applyUniform(ready.value, "u_resolution", resolution);
|
|
1829
|
+
applyUniform(ready.value, "u_size", size);
|
|
1830
|
+
applyUniform(ready.value, "u_start_color", parameters.startColor);
|
|
1831
|
+
applyUniform(ready.value, "u_end_color", parameters.endColor);
|
|
1832
|
+
gl.drawArrays(gl.POINTS, 0, count);
|
|
1833
|
+
gl.bindVertexArray(null);
|
|
1834
|
+
return rendered;
|
|
1835
|
+
} finally {
|
|
1836
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, previousFramebuffer);
|
|
1837
|
+
}
|
|
1838
|
+
},
|
|
1839
|
+
invalidate() {
|
|
1840
|
+
destroyProgram(gl, program);
|
|
1841
|
+
program = null;
|
|
1842
|
+
discardTarget(target, true);
|
|
1843
|
+
},
|
|
1844
|
+
invalidateContextLoss() {
|
|
1845
|
+
program = null;
|
|
1846
|
+
discardTarget(target, false);
|
|
1847
|
+
},
|
|
1848
|
+
dispose() {
|
|
1849
|
+
if (!producerDisposed) {
|
|
1850
|
+
producerDisposed = true;
|
|
1851
|
+
destroyProgram(gl, program);
|
|
1852
|
+
program = null;
|
|
1853
|
+
discardTarget(target, true);
|
|
1854
|
+
targets.delete(target);
|
|
1855
|
+
invalidatables.delete(producer);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
invalidatables.add(producer);
|
|
1860
|
+
return ok(producer);
|
|
1861
|
+
}
|
|
1862
|
+
function createGodotParticleProducer(pass, width, height) {
|
|
1863
|
+
const invalid = state();
|
|
1864
|
+
if (invalid) return invalid;
|
|
1865
|
+
if (!pass.config && !pass.scratch) return fail("UNSUPPORTED_PARTICLE_FEATURE", "A particle pass needs config or caller-owned scratch.", "config");
|
|
1866
|
+
if (pass.scratch && pass.config) return fail("UNSUPPORTED_PARTICLE_FEATURE", "When scratch is supplied, omit config: scratch is the semantic owner.", "scratch-config");
|
|
1867
|
+
const config = pass.scratch ? pass.scratch.config : normalizeParticleRenderConfig(pass.config);
|
|
1868
|
+
const sprite = pass.spriteTexture ? textureInputFor(pass.spriteTexture) : null;
|
|
1869
|
+
const lut = pass.lutTexture ? textureInputFor(pass.lutTexture) : null;
|
|
1870
|
+
const mask = pass.maskTexture ? textureInputFor(pass.maskTexture) : null;
|
|
1871
|
+
if (pass.spriteTexture && !sprite) return fail("FOREIGN_TARGET", "A textured particle system needs a live sprite target owned by this stage.", "sprite-texture");
|
|
1872
|
+
if (pass.lutTexture && !lut || pass.maskTexture && !mask) return fail("FOREIGN_TARGET", "Particle auxiliary textures must be live targets owned by this stage.", "auxiliary-texture");
|
|
1873
|
+
const unsupported = [
|
|
1874
|
+
"collision",
|
|
1875
|
+
"attractors",
|
|
1876
|
+
"trails",
|
|
1877
|
+
"customMaterial"
|
|
1878
|
+
].find((feature) => pass.features?.[feature]);
|
|
1879
|
+
if (unsupported) return fail("UNSUPPORTED_PARTICLE_FEATURE", `Headless Godot particles do not support ${unsupported}.`, unsupported);
|
|
1880
|
+
if (config.emissionShape === 4 || config.emissionShape === 5) return fail("UNSUPPORTED_PARTICLE_FEATURE", "Point-list and directed-point emission require their authored point data.", "emission-shape");
|
|
1881
|
+
const scratch = pass.scratch ?? createGodotParticleScratch(config);
|
|
1882
|
+
const ownsScratch = !pass.scratch;
|
|
1883
|
+
if (scratch.instances.data.length / 10 < scratch.state.count) return fail("UNSUPPORTED_PARTICLE_FEATURE", "Scratch cannot hold the configured particle count.", "scratch-capacity");
|
|
1884
|
+
const target = makeTarget();
|
|
1885
|
+
const allocated = resizeTarget(target, width, height);
|
|
1886
|
+
if (!allocated.ok) {
|
|
1887
|
+
targets.delete(target);
|
|
1888
|
+
return allocated;
|
|
1889
|
+
}
|
|
1890
|
+
const semanticConfig = scratch.config;
|
|
1891
|
+
const packing = {
|
|
1892
|
+
state: scratch.state,
|
|
1893
|
+
config: semanticConfig,
|
|
1894
|
+
instances: scratch.instances,
|
|
1895
|
+
textureWidth: 0,
|
|
1896
|
+
textureHeight: 0,
|
|
1897
|
+
origin: void 0,
|
|
1898
|
+
transform: void 0,
|
|
1899
|
+
modulate: void 0
|
|
1900
|
+
};
|
|
1901
|
+
let requestedWidth = target.width, requestedHeight = target.height;
|
|
1902
|
+
let program = null;
|
|
1903
|
+
let quad = null;
|
|
1904
|
+
let producerDisposed = false;
|
|
1905
|
+
let lastTime = null;
|
|
1906
|
+
let preprocessed = false;
|
|
1907
|
+
const rendered = ok(target);
|
|
1908
|
+
const warmed = ok(void 0);
|
|
1909
|
+
const particleGl = {
|
|
1910
|
+
gl,
|
|
1911
|
+
quad: null
|
|
1912
|
+
};
|
|
1913
|
+
const drawOptions = {
|
|
1914
|
+
texture: sprite?.texture ?? null,
|
|
1915
|
+
textured: Boolean(sprite),
|
|
1916
|
+
lutTexture: lut?.texture ?? null,
|
|
1917
|
+
maskTexture: mask?.texture ?? null,
|
|
1918
|
+
hframes: semanticConfig.hframes,
|
|
1919
|
+
vframes: semanticConfig.vframes,
|
|
1920
|
+
blendMode: semanticConfig.blendMode,
|
|
1921
|
+
viewportW: target.width,
|
|
1922
|
+
viewportH: target.height,
|
|
1923
|
+
alphaFromRed: semanticConfig.alphaFromRed,
|
|
1924
|
+
erode: semanticConfig.alphaErode,
|
|
1925
|
+
uvPolar: semanticConfig.uvPolar,
|
|
1926
|
+
targetFramebuffer: target.framebuffer
|
|
1927
|
+
};
|
|
1928
|
+
const ensure = () => {
|
|
1929
|
+
if (program) return readyResult;
|
|
1930
|
+
const compiled = acquireParticleProgram(gl);
|
|
1931
|
+
if (!compiled) return fail("SHADER_COMPILE_FAILED", "WebGL could not compile the shared Godot particle renderer.");
|
|
1932
|
+
quad = gl.createBuffer();
|
|
1933
|
+
if (!quad) {
|
|
1934
|
+
releaseParticleProgram(gl);
|
|
1935
|
+
return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not allocate the particle resolve quad.");
|
|
1936
|
+
}
|
|
1937
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, quad);
|
|
1938
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
|
1939
|
+
-1,
|
|
1940
|
+
-1,
|
|
1941
|
+
1,
|
|
1942
|
+
-1,
|
|
1943
|
+
-1,
|
|
1944
|
+
1,
|
|
1945
|
+
1,
|
|
1946
|
+
1
|
|
1947
|
+
]), gl.STATIC_DRAW);
|
|
1948
|
+
program = compiled;
|
|
1949
|
+
particleGl.quad = quad;
|
|
1950
|
+
readyResult = ok(compiled);
|
|
1951
|
+
return readyResult;
|
|
1952
|
+
};
|
|
1953
|
+
let readyResult = null;
|
|
1954
|
+
const dependencyLive = (value) => !(value && (value.owned ? !targets.has(value.owned) || !value.owned.texture || !value.owned.framebuffer : typeof gl.isTexture === "function" && !gl.isTexture(value.texture)));
|
|
1955
|
+
const reset = () => {
|
|
1956
|
+
const s = scratch.state;
|
|
1957
|
+
s.time = 0;
|
|
1958
|
+
s.cycle = 0;
|
|
1959
|
+
s.remainder = 0;
|
|
1960
|
+
s.emitting = true;
|
|
1961
|
+
for (let index = 0; index < s.particles.length; index += 1) s.particles[index].active = false;
|
|
1962
|
+
};
|
|
1963
|
+
const preprocess = () => {
|
|
1964
|
+
if (preprocessed) return;
|
|
1965
|
+
preprocessParticles(scratch.state);
|
|
1966
|
+
preprocessed = true;
|
|
1967
|
+
};
|
|
1968
|
+
const producer = {
|
|
1969
|
+
target,
|
|
1970
|
+
scratch,
|
|
1971
|
+
warmUp() {
|
|
1972
|
+
const current = state();
|
|
1973
|
+
if (current) return current;
|
|
1974
|
+
if (producerDisposed) return fail("DISPOSED", "The Godot particle producer has been disposed.");
|
|
1975
|
+
const ready = ensure();
|
|
1976
|
+
if (!ready.ok) return ready;
|
|
1977
|
+
if (!prepareParticleDraw(gl, ready.value, drawOptions) || false) return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not prepare particle draw resources.");
|
|
1978
|
+
return warmed;
|
|
1979
|
+
},
|
|
1980
|
+
resize(nextWidth, nextHeight) {
|
|
1981
|
+
if (producerDisposed) return fail("DISPOSED", "The Godot particle producer has been disposed.");
|
|
1982
|
+
const resized = resizeTarget(target, nextWidth, nextHeight);
|
|
1983
|
+
if (resized.ok) {
|
|
1984
|
+
requestedWidth = target.width;
|
|
1985
|
+
requestedHeight = target.height;
|
|
1986
|
+
drawOptions.viewportW = target.width;
|
|
1987
|
+
drawOptions.viewportH = target.height;
|
|
1988
|
+
drawOptions.targetFramebuffer = target.framebuffer;
|
|
1989
|
+
}
|
|
1990
|
+
return resized;
|
|
1991
|
+
},
|
|
1992
|
+
render(input) {
|
|
1993
|
+
if (input.origin !== void 0 && (input.origin.length !== 2 || !input.origin.every(Number.isFinite)) || !Number.isFinite(input.time) || !Number.isFinite(input.delta) || input.time < 0 || input.delta < 0) return fail("INVALID_RENDER_INPUT", "Particle time and delta must be finite and non-negative.", "render-input");
|
|
1994
|
+
if (!input.restart && lastTime !== null) {
|
|
1995
|
+
if (input.time < lastTime) return fail("INVALID_RENDER_INPUT", "Particle time must be monotonic unless restart is set.", "time");
|
|
1996
|
+
if (Math.abs(input.delta - (input.time - lastTime)) > 1e-6) return fail("INVALID_RENDER_INPUT", "Particle delta must reconcile with caller time.", "time-delta");
|
|
1997
|
+
}
|
|
1998
|
+
const current = state();
|
|
1999
|
+
if (current) return current;
|
|
2000
|
+
if (producerDisposed) return fail("DISPOSED", "The Godot particle producer has been disposed.");
|
|
2001
|
+
if (!target.texture || !target.framebuffer) {
|
|
2002
|
+
const resized = resizeTarget(target, requestedWidth, requestedHeight);
|
|
2003
|
+
if (!resized.ok) return resized;
|
|
2004
|
+
}
|
|
2005
|
+
if (!dependencyLive(sprite)) return fail("FOREIGN_TARGET", "Particle sprite texture is no longer live.", "sprite-texture");
|
|
2006
|
+
if (!dependencyLive(lut)) return fail("FOREIGN_TARGET", "Particle LUT texture is no longer live.", "lut-texture");
|
|
2007
|
+
if (!dependencyLive(mask)) return fail("FOREIGN_TARGET", "Particle mask texture is no longer live.", "mask-texture");
|
|
2008
|
+
const ready = ensure();
|
|
2009
|
+
if (!ready.ok) return ready;
|
|
2010
|
+
drawOptions.viewportW = target.width;
|
|
2011
|
+
drawOptions.viewportH = target.height;
|
|
2012
|
+
drawOptions.targetFramebuffer = target.framebuffer;
|
|
2013
|
+
if (!prepareParticleDraw(gl, ready.value, drawOptions)) return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not allocate the additive particle target.");
|
|
2014
|
+
if (input.restart) {
|
|
2015
|
+
reset();
|
|
2016
|
+
preprocessed = false;
|
|
2017
|
+
}
|
|
2018
|
+
preprocess();
|
|
2019
|
+
scratch.state.emitting = input.emitting;
|
|
2020
|
+
simulateParticles(scratch.state, input.delta);
|
|
2021
|
+
lastTime = input.time;
|
|
2022
|
+
const buffer = scratch.instances;
|
|
2023
|
+
packing.textureWidth = semanticConfig.textureWidth > 0 ? semanticConfig.textureWidth : sprite?.width ?? 16;
|
|
2024
|
+
packing.textureHeight = semanticConfig.textureHeight > 0 ? semanticConfig.textureHeight : sprite?.height ?? 16;
|
|
2025
|
+
packing.origin = input.origin;
|
|
2026
|
+
packParticleInstances(packing);
|
|
2027
|
+
const previous = gl.getParameter(gl.FRAMEBUFFER_BINDING);
|
|
2028
|
+
try {
|
|
2029
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
|
|
2030
|
+
gl.viewport(0, 0, target.width, target.height);
|
|
2031
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
2032
|
+
gl.clearColor(0, 0, 0, 0);
|
|
2033
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
2034
|
+
if (buffer.count === 0) return rendered;
|
|
2035
|
+
if (!drawParticles(particleGl, ready.value, buffer, drawOptions)) return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not allocate the additive particle target.");
|
|
2036
|
+
return rendered;
|
|
2037
|
+
} finally {
|
|
2038
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, previous);
|
|
2039
|
+
}
|
|
2040
|
+
},
|
|
2041
|
+
invalidate() {
|
|
2042
|
+
if (program) releaseParticleProgram(gl);
|
|
2043
|
+
program = null;
|
|
2044
|
+
if (quad) gl.deleteBuffer(quad);
|
|
2045
|
+
quad = null;
|
|
2046
|
+
if (ownsScratch) disposeParticleInstanceBuffer(gl, scratch.instances);
|
|
2047
|
+
discardTarget(target, true);
|
|
2048
|
+
},
|
|
2049
|
+
invalidateContextLoss() {
|
|
2050
|
+
if (program) releaseParticleProgram(gl, true);
|
|
2051
|
+
program = null;
|
|
2052
|
+
quad = null;
|
|
2053
|
+
invalidateParticleInstanceBuffer(gl, scratch.instances);
|
|
2054
|
+
discardTarget(target, false);
|
|
2055
|
+
},
|
|
2056
|
+
dispose() {
|
|
2057
|
+
if (!producerDisposed) {
|
|
2058
|
+
producerDisposed = true;
|
|
2059
|
+
producer.invalidate();
|
|
2060
|
+
targets.delete(target);
|
|
2061
|
+
invalidatables.delete(producer);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
};
|
|
2065
|
+
invalidatables.add(producer);
|
|
2066
|
+
return ok(producer);
|
|
2067
|
+
}
|
|
2068
|
+
function createGodotParticleDirectPass(pass) {
|
|
2069
|
+
const invalid = state();
|
|
2070
|
+
if (invalid) return invalid;
|
|
2071
|
+
if (!pass.config && !pass.scratch) return fail("UNSUPPORTED_PARTICLE_FEATURE", "A particle pass needs config or caller-owned scratch.", "config");
|
|
2072
|
+
if (pass.scratch && pass.config) return fail("UNSUPPORTED_PARTICLE_FEATURE", "When scratch is supplied, omit config: scratch is the semantic owner.", "scratch-config");
|
|
2073
|
+
const config = pass.scratch ? pass.scratch.config : normalizeParticleRenderConfig(pass.config);
|
|
2074
|
+
const sprite = pass.spriteTexture ? textureInputFor(pass.spriteTexture) : null;
|
|
2075
|
+
const lut = pass.lutTexture ? textureInputFor(pass.lutTexture) : null;
|
|
2076
|
+
const mask = pass.maskTexture ? textureInputFor(pass.maskTexture) : null;
|
|
2077
|
+
if (pass.spriteTexture && !sprite) return fail("FOREIGN_TARGET", "A textured particle system needs a live sprite target owned by this stage.", "sprite-texture");
|
|
2078
|
+
if (pass.lutTexture && !lut || pass.maskTexture && !mask) return fail("FOREIGN_TARGET", "Particle auxiliary textures must be live targets owned by this stage.", "auxiliary-texture");
|
|
2079
|
+
const unsupported = [
|
|
2080
|
+
"collision",
|
|
2081
|
+
"attractors",
|
|
2082
|
+
"trails",
|
|
2083
|
+
"customMaterial"
|
|
2084
|
+
].find((feature) => pass.features?.[feature]);
|
|
2085
|
+
if (unsupported) return fail("UNSUPPORTED_PARTICLE_FEATURE", `Headless Godot particles do not support ${unsupported}.`, unsupported);
|
|
2086
|
+
if (config.emissionShape === 4 || config.emissionShape === 5) return fail("UNSUPPORTED_PARTICLE_FEATURE", "Point-list and directed-point emission require their authored point data.", "emission-shape");
|
|
2087
|
+
const scratch = pass.scratch ?? createGodotParticleScratch(config);
|
|
2088
|
+
const ownsScratch = !pass.scratch;
|
|
2089
|
+
if (scratch.instances.data.length / 10 < scratch.state.count) return fail("UNSUPPORTED_PARTICLE_FEATURE", "Scratch cannot hold the configured particle count.", "scratch-capacity");
|
|
2090
|
+
const semanticConfig = scratch.config;
|
|
2091
|
+
const packing = {
|
|
2092
|
+
state: scratch.state,
|
|
2093
|
+
config: semanticConfig,
|
|
2094
|
+
instances: scratch.instances,
|
|
2095
|
+
textureWidth: 0,
|
|
2096
|
+
textureHeight: 0,
|
|
2097
|
+
origin: void 0,
|
|
2098
|
+
transform: void 0,
|
|
2099
|
+
modulate: void 0
|
|
2100
|
+
};
|
|
2101
|
+
let program = null;
|
|
2102
|
+
let quad = null;
|
|
2103
|
+
let disposedProducer = false;
|
|
2104
|
+
let preprocessed = false;
|
|
2105
|
+
let lastTime = null;
|
|
2106
|
+
const particleGl = {
|
|
2107
|
+
gl,
|
|
2108
|
+
quad: null
|
|
2109
|
+
};
|
|
2110
|
+
const drawOptions = {
|
|
2111
|
+
texture: sprite?.texture ?? null,
|
|
2112
|
+
textured: Boolean(sprite),
|
|
2113
|
+
lutTexture: lut?.texture ?? null,
|
|
2114
|
+
maskTexture: mask?.texture ?? null,
|
|
2115
|
+
hframes: semanticConfig.hframes,
|
|
2116
|
+
vframes: semanticConfig.vframes,
|
|
2117
|
+
blendMode: semanticConfig.blendMode,
|
|
2118
|
+
viewportW: 1,
|
|
2119
|
+
viewportH: 1,
|
|
2120
|
+
alphaFromRed: semanticConfig.alphaFromRed,
|
|
2121
|
+
erode: semanticConfig.alphaErode,
|
|
2122
|
+
uvPolar: semanticConfig.uvPolar,
|
|
2123
|
+
targetFramebuffer: null,
|
|
2124
|
+
additiveResolveIntoExisting: true
|
|
2125
|
+
};
|
|
2126
|
+
let readyResult = null;
|
|
2127
|
+
const ensure = () => {
|
|
2128
|
+
if (program) return readyResult;
|
|
2129
|
+
const compiled = acquireParticleProgram(gl);
|
|
2130
|
+
if (!compiled) return fail("SHADER_COMPILE_FAILED", "WebGL could not compile the shared Godot particle renderer.");
|
|
2131
|
+
quad = gl.createBuffer();
|
|
2132
|
+
if (!quad) {
|
|
2133
|
+
releaseParticleProgram(gl);
|
|
2134
|
+
return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not allocate the particle resolve quad.");
|
|
2135
|
+
}
|
|
2136
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, quad);
|
|
2137
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
|
2138
|
+
-1,
|
|
2139
|
+
-1,
|
|
2140
|
+
1,
|
|
2141
|
+
-1,
|
|
2142
|
+
-1,
|
|
2143
|
+
1,
|
|
2144
|
+
1,
|
|
2145
|
+
1
|
|
2146
|
+
]), gl.STATIC_DRAW);
|
|
2147
|
+
program = compiled;
|
|
2148
|
+
particleGl.quad = quad;
|
|
2149
|
+
readyResult = ok(compiled);
|
|
2150
|
+
return readyResult;
|
|
2151
|
+
};
|
|
2152
|
+
const dependencyLive = (value) => !(value && (value.owned ? !targets.has(value.owned) || !value.owned.texture || !value.owned.framebuffer : typeof gl.isTexture === "function" && !gl.isTexture(value.texture)));
|
|
2153
|
+
const reset = () => {
|
|
2154
|
+
const state = scratch.state;
|
|
2155
|
+
state.time = 0;
|
|
2156
|
+
state.cycle = 0;
|
|
2157
|
+
state.remainder = 0;
|
|
2158
|
+
state.emitting = true;
|
|
2159
|
+
for (const particle of state.particles) particle.active = false;
|
|
2160
|
+
};
|
|
2161
|
+
const preprocess = () => {
|
|
2162
|
+
if (preprocessed) return;
|
|
2163
|
+
preprocessParticles(scratch.state);
|
|
2164
|
+
preprocessed = true;
|
|
2165
|
+
};
|
|
2166
|
+
const producer = {
|
|
2167
|
+
scratch,
|
|
2168
|
+
warmUp() {
|
|
2169
|
+
const current = state();
|
|
2170
|
+
if (current) return current;
|
|
2171
|
+
if (disposedProducer) return fail("DISPOSED", "The Godot particle pass has been disposed.");
|
|
2172
|
+
const ready = ensure();
|
|
2173
|
+
if (!ready.ok) return ready;
|
|
2174
|
+
if (!prepareParticleDraw(gl, ready.value, drawOptions) || false) return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not prepare particle draw resources.");
|
|
2175
|
+
preprocess();
|
|
2176
|
+
return ok(void 0);
|
|
2177
|
+
},
|
|
2178
|
+
draw(input, context) {
|
|
2179
|
+
if (input.origin !== void 0 && (input.origin.length !== 2 || !input.origin.every(Number.isFinite)) || !Number.isFinite(input.time) || !Number.isFinite(input.delta) || input.time < 0 || input.delta < 0 || !Number.isFinite(context.width) || !Number.isFinite(context.height) || context.width <= 0 || context.height <= 0) return fail("INVALID_RENDER_INPUT", "Particle time, delta, and target dimensions must be finite and positive.", "render-input");
|
|
2180
|
+
const transform = directParticleTransform(input.transform);
|
|
2181
|
+
if (!transform) return fail("UNSUPPORTED_PARTICLE_FEATURE", "Direct particles require a finite uniform-scale rotation and translation transform.", "transform");
|
|
2182
|
+
const modulate = directParticleModulate(input.modulate);
|
|
2183
|
+
if (!modulate) return fail("INVALID_RENDER_INPUT", "Direct particle modulation components must be finite.", "modulate");
|
|
2184
|
+
if (!input.restart && lastTime !== null) {
|
|
2185
|
+
if (input.time < lastTime) return fail("INVALID_RENDER_INPUT", "Particle time must be monotonic unless restart is set.", "time");
|
|
2186
|
+
if (Math.abs(input.delta - (input.time - lastTime)) > 1e-6) return fail("INVALID_RENDER_INPUT", "Particle delta must reconcile with caller time.", "time-delta");
|
|
2187
|
+
}
|
|
2188
|
+
const current = state();
|
|
2189
|
+
if (current) return current;
|
|
2190
|
+
if (disposedProducer) return fail("DISPOSED", "The Godot particle pass has been disposed.");
|
|
2191
|
+
if (!dependencyLive(sprite)) return fail("FOREIGN_TARGET", "Particle sprite texture is no longer live.", "sprite-texture");
|
|
2192
|
+
if (!dependencyLive(lut)) return fail("FOREIGN_TARGET", "Particle LUT texture is no longer live.", "lut-texture");
|
|
2193
|
+
if (!dependencyLive(mask)) return fail("FOREIGN_TARGET", "Particle mask texture is no longer live.", "mask-texture");
|
|
2194
|
+
const ready = ensure();
|
|
2195
|
+
if (!ready.ok) return ready;
|
|
2196
|
+
drawOptions.viewportW = Math.floor(context.width);
|
|
2197
|
+
drawOptions.viewportH = Math.floor(context.height);
|
|
2198
|
+
drawOptions.targetFramebuffer = context.framebuffer;
|
|
2199
|
+
if (!prepareParticleDraw(gl, ready.value, drawOptions)) return fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not allocate the shared additive particle target.");
|
|
2200
|
+
if (input.restart) {
|
|
2201
|
+
reset();
|
|
2202
|
+
preprocessed = false;
|
|
2203
|
+
}
|
|
2204
|
+
preprocess();
|
|
2205
|
+
scratch.state.emitting = input.emitting;
|
|
2206
|
+
simulateParticles(scratch.state, input.delta);
|
|
2207
|
+
lastTime = input.time;
|
|
2208
|
+
const buffer = scratch.instances;
|
|
2209
|
+
packing.textureWidth = semanticConfig.textureWidth > 0 ? semanticConfig.textureWidth : sprite?.width ?? 16;
|
|
2210
|
+
packing.textureHeight = semanticConfig.textureHeight > 0 ? semanticConfig.textureHeight : sprite?.height ?? 16;
|
|
2211
|
+
packing.origin = input.origin;
|
|
2212
|
+
packing.transform = transform;
|
|
2213
|
+
packing.modulate = modulate;
|
|
2214
|
+
packParticleInstances(packing);
|
|
2215
|
+
if (buffer.count === 0) return ok(void 0);
|
|
2216
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, context.framebuffer);
|
|
2217
|
+
gl.viewport(0, 0, drawOptions.viewportW, drawOptions.viewportH);
|
|
2218
|
+
return drawParticles(particleGl, ready.value, buffer, drawOptions) ? ok(void 0) : fail("RESOURCE_ALLOCATION_FAILED", "WebGL could not draw the shared particle pass.");
|
|
2219
|
+
},
|
|
2220
|
+
invalidate() {
|
|
2221
|
+
if (program) releaseParticleProgram(gl);
|
|
2222
|
+
program = null;
|
|
2223
|
+
readyResult = null;
|
|
2224
|
+
if (quad) gl.deleteBuffer(quad);
|
|
2225
|
+
quad = null;
|
|
2226
|
+
particleGl.quad = null;
|
|
2227
|
+
if (ownsScratch) disposeParticleInstanceBuffer(gl, scratch.instances);
|
|
2228
|
+
},
|
|
2229
|
+
invalidateContextLoss() {
|
|
2230
|
+
if (program) releaseParticleProgram(gl, true);
|
|
2231
|
+
program = null;
|
|
2232
|
+
readyResult = null;
|
|
2233
|
+
quad = null;
|
|
2234
|
+
particleGl.quad = null;
|
|
2235
|
+
invalidateParticleInstanceBuffer(gl, scratch.instances);
|
|
2236
|
+
},
|
|
2237
|
+
dispose() {
|
|
2238
|
+
if (disposedProducer) return;
|
|
2239
|
+
disposedProducer = true;
|
|
2240
|
+
producer.invalidate();
|
|
2241
|
+
invalidatables.delete(producer);
|
|
2242
|
+
}
|
|
2243
|
+
};
|
|
2244
|
+
invalidatables.add(producer);
|
|
2245
|
+
return ok(producer);
|
|
2246
|
+
}
|
|
2247
|
+
return {
|
|
2248
|
+
gl,
|
|
2249
|
+
createShaderProducer,
|
|
2250
|
+
createGodotShaderProducer,
|
|
2251
|
+
createParticleProducer,
|
|
2252
|
+
createGodotParticleProducer,
|
|
2253
|
+
createGodotParticleDirectPass,
|
|
2254
|
+
executeScreenSample(command) {
|
|
2255
|
+
const current = state();
|
|
2256
|
+
if (current) return current;
|
|
2257
|
+
const source = targetFor(command.source);
|
|
2258
|
+
const destination = targetFor(command.destination);
|
|
2259
|
+
if (!source?.texture || !source.framebuffer || !destination?.texture || !destination.framebuffer) return fail("FOREIGN_TARGET", "Screen sample commands require live targets owned by this headless effects stage.");
|
|
2260
|
+
if (source === destination) {
|
|
2261
|
+
const captured = snapshotFor(source);
|
|
2262
|
+
if (!captured.ok) return captured;
|
|
2263
|
+
copy(captured.value, destination, command.filter ?? "nearest");
|
|
2264
|
+
} else copy(source, destination, command.filter ?? "nearest");
|
|
2265
|
+
return ok(void 0);
|
|
2266
|
+
},
|
|
2267
|
+
invalidate() {
|
|
2268
|
+
for (const item of [...invalidatables]) item.invalidate();
|
|
2269
|
+
if (snapshot) discardTarget(snapshot, true);
|
|
2270
|
+
if (opaqueWhite) discardTarget(opaqueWhite, true);
|
|
2271
|
+
opaqueWhiteFilled = false;
|
|
2272
|
+
opaqueWhiteResult = null;
|
|
2273
|
+
},
|
|
2274
|
+
invalidateContextLoss() {
|
|
2275
|
+
for (const item of [...invalidatables]) item.invalidateContextLoss();
|
|
2276
|
+
if (snapshot) discardTarget(snapshot, false);
|
|
2277
|
+
if (opaqueWhite) discardTarget(opaqueWhite, false);
|
|
2278
|
+
opaqueWhiteFilled = false;
|
|
2279
|
+
opaqueWhiteResult = null;
|
|
2280
|
+
},
|
|
2281
|
+
dispose() {
|
|
2282
|
+
if (disposed) return;
|
|
2283
|
+
disposed = true;
|
|
2284
|
+
for (const item of [...invalidatables]) item.dispose();
|
|
2285
|
+
if (snapshot) discardTarget(snapshot, true);
|
|
2286
|
+
if (opaqueWhite) discardTarget(opaqueWhite, true);
|
|
2287
|
+
snapshot = null;
|
|
2288
|
+
opaqueWhite = null;
|
|
2289
|
+
opaqueWhiteFilled = false;
|
|
2290
|
+
opaqueWhiteResult = null;
|
|
2291
|
+
targets.clear();
|
|
2292
|
+
}
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
//#endregion
|
|
2296
|
+
export { MAX_HEADLESS_PARTICLES, blendFactorsFor, clearWebglSurface, compileProgram, compileProgramAsync, createGodotParticleScratch, createHeadlessEffectsStage, createWebglFullscreenQuad, createWebglPlaceholderTexture, createWebglTexture, deleteWebglTexture, disposeParticleInstanceBuffer, drawGodotWebglShaderFrame, drawParticles, getParticleProgram, invalidateParticleInstanceBuffer, prepareParticleDraw, startProgram, uploadGodotShaderUniform, uploadWebglTexture };
|
|
2297
|
+
|
|
2298
|
+
//# sourceMappingURL=webgl.mjs.map
|