phoria2 1.3.0 → 1.4.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/README.md +9 -6
- package/dist/bloom.js +4 -4
- package/dist/bloom.js.map +1 -1
- package/dist/charts.js +436 -436
- package/dist/charts.js.map +1 -1
- package/dist/core.js +1223 -862
- package/dist/core.js.map +1 -1
- package/dist/debug.js +297 -0
- package/dist/debug.js.map +1 -0
- package/dist/edit.js +40 -40
- package/dist/edit.js.map +1 -1
- package/dist/explain.js +32 -13
- package/dist/explain.js.map +1 -1
- package/dist/gltf.js +128 -128
- package/dist/gltf.js.map +1 -1
- package/dist/keyboard.js +19 -19
- package/dist/keyboard.js.map +1 -1
- package/dist/motion.js +35 -35
- package/dist/motion.js.map +1 -1
- package/dist/particles.js +36 -36
- package/dist/particles.js.map +1 -1
- package/dist/passes.js +116 -104
- package/dist/passes.js.map +1 -1
- package/dist/phoria2.js +1 -1
- package/dist/picking.js +88 -71
- package/dist/picking.js.map +1 -1
- package/dist/snapshot.js +1 -1
- package/dist/tooltip.js +1 -1
- package/dist/types/View.d.ts +16 -0
- package/dist/types/debug.d.ts +74 -0
- package/dist/types/explain.d.ts +4 -0
- package/dist/types/frame.d.ts +5 -0
- package/dist/types/material/Material.d.ts +52 -0
- package/dist/types/watch.d.ts +8 -1
- package/dist/types/webgl/hooks.d.ts +48 -0
- package/dist/types/webgl/passes.d.ts +4 -0
- package/dist/types/webgl/programs.d.ts +15 -5
- package/dist/types/webgl/shaders.d.ts +36 -5
- package/llms-full.txt +227 -0
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/schema/scene.schema.json +27 -0
- package/src/View.js +105 -7
- package/src/debug.js +283 -0
- package/src/explain.js +19 -0
- package/src/frame.js +7 -2
- package/src/material/Material.js +121 -2
- package/src/scene/drawables.js +21 -4
- package/src/watch.js +18 -2
- package/src/webgl/WebGLRenderer.js +19 -2
- package/src/webgl/hooks.js +121 -0
- package/src/webgl/passes.js +17 -2
- package/src/webgl/picking.js +23 -2
- package/src/webgl/programs.js +20 -9
- package/src/webgl/resources.js +1 -1
- package/src/webgl/shaders.js +443 -35
|
@@ -3,6 +3,7 @@ import * as mat4 from '../math/mat4.js'
|
|
|
3
3
|
import { Geometry } from '../geometry/Geometry.js'
|
|
4
4
|
import { Material } from '../material/Material.js'
|
|
5
5
|
import { CLIP_ONLY_FRAGMENT, MESH_VERTEX } from './shaders.js'
|
|
6
|
+
import { hookFailed, hookFor, setHookUniforms } from './hooks.js'
|
|
6
7
|
import { collect } from '../frame.js'
|
|
7
8
|
import { checkNumber, checkOneOf, checkOptions, checkRange, describe, fail, warnOnce } from '../validate.js'
|
|
8
9
|
import { GLContext } from './context.js'
|
|
@@ -223,6 +224,11 @@ export class WebGLRenderer {
|
|
|
223
224
|
const env = frame.environment
|
|
224
225
|
const bg = frame.background
|
|
225
226
|
const passes = gpu.loadPasses(!!frame.shadow || frame.spotShadows.length > 0 || !!frame.pointShadow || frame.outlines.length > 0 || (typeof env === 'object' && !!env.image) || (!!bg && !('getHex' in bg) && !!bg.image))
|
|
227
|
+
// shader hooks that read the clock read it in the shadow maps and outlines too
|
|
228
|
+
if (passes) {
|
|
229
|
+
passes.time = frame.time ?? 0
|
|
230
|
+
passes.cameraPosition = frame.camera.position
|
|
231
|
+
}
|
|
226
232
|
this.#environment = passes && typeof env === 'object' ? passes.environment(env) : null
|
|
227
233
|
if (passes && typeof env === 'object' && env.image && !this.#environment) {
|
|
228
234
|
warnOnce(env, env.isHDR ? "this device can't render to float textures, which HDR environments need - lighting without it"
|
|
@@ -419,15 +425,25 @@ export class WebGLRenderer {
|
|
|
419
425
|
else if (material.blending === 'multiply') defines.push('FOG_MULTIPLY')
|
|
420
426
|
}
|
|
421
427
|
if (clip) defines.push('CLIP')
|
|
428
|
+
// a material's own GLSL, spliced into this program
|
|
429
|
+
const hook = hookFor(material)
|
|
430
|
+
if (hook) defines.push(...hook.defines)
|
|
422
431
|
const programs = this.#gpu.programs
|
|
423
|
-
const program = programs.get(kind, defines)
|
|
432
|
+
const program = programs.get(kind, defines, undefined, hook?.source)
|
|
424
433
|
if (!programs.isReady(program)) {
|
|
425
434
|
if (!this.#wait) {
|
|
426
435
|
// still compiling: drawn once it's done, rather than holding up everything else
|
|
427
436
|
this.info.compiling++
|
|
428
437
|
return
|
|
429
438
|
}
|
|
430
|
-
programs.ready(kind, defines)
|
|
439
|
+
programs.ready(kind, defines, undefined, hook?.source)
|
|
440
|
+
}
|
|
441
|
+
if (program.failed) {
|
|
442
|
+
// reported once; drawn without the hooks from the next frame
|
|
443
|
+
hookFailed(material, /** @type {import('./hooks.js').Hook} */ (hook), program.failed)
|
|
444
|
+
// counted as still compiling, so the View draws again - with the material's plain program
|
|
445
|
+
this.info.compiling++
|
|
446
|
+
return
|
|
431
447
|
}
|
|
432
448
|
gl.useProgram(program.program)
|
|
433
449
|
|
|
@@ -472,6 +488,7 @@ export class WebGLRenderer {
|
|
|
472
488
|
}
|
|
473
489
|
program.set('uColor', material.color)
|
|
474
490
|
program.set('uOpacity', material.opacity)
|
|
491
|
+
if (hook) setHookUniforms(program, material, frame.time ?? 0, frame.camera.position, (t) => this.#gpu.resources.texture(t))
|
|
475
492
|
if (fog) {
|
|
476
493
|
program.set('uFogColor', fog.color)
|
|
477
494
|
program.set('uFogRange', [fog.near, fog.far])
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Shader hooks for the renderer: a material's GLSL turned into what the shaders splice in - uniform
|
|
2
|
+
// declarations and the code - and the defines that switch it on, one program per distinct hook. A
|
|
3
|
+
// hook that fails to compile is reported once, in terms of its own lines, and the material is drawn
|
|
4
|
+
// without it from then on, rather than stopping the frame.
|
|
5
|
+
import { Texture } from '../material/Texture.js'
|
|
6
|
+
import { warnOnce } from '../validate.js'
|
|
7
|
+
import { CLIP_ONLY_FRAGMENT, HOOK_HELPERS, HOOK_SOURCE_NUMBERS, LINES_VERTEX, MESH_VERTEX, POINTS_VERTEX, shaderSource } from './shaders.js'
|
|
8
|
+
|
|
9
|
+
/** @import { Material, ShaderUniform } from '../material/Material.js' */
|
|
10
|
+
/** @import { HookSource } from './shaders.js' */
|
|
11
|
+
/** @import { Program } from './programs.js' */
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A material's hooks, ready to compile.
|
|
15
|
+
* @typedef {object} Hook
|
|
16
|
+
* @property {string} signature its code and uniform types - one program per signature
|
|
17
|
+
* @property {string[]} defines HOOK_VERTEX, HOOK_FRAGMENT, HOOK_LIT for the parts it has, and its id
|
|
18
|
+
* @property {HookSource} source what the shaders splice in
|
|
19
|
+
* @property {boolean} moves it has a vertex hook
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** @type {Map<string, Hook>} */
|
|
23
|
+
const hooks = new Map()
|
|
24
|
+
/** Signatures that failed to compile: those materials draw without their hooks. @type {Set<string>} */
|
|
25
|
+
const failed = new Set()
|
|
26
|
+
/** @type {WeakMap<object, { types: string, hook: Hook | null }>} */
|
|
27
|
+
const recent = new WeakMap()
|
|
28
|
+
|
|
29
|
+
/** The GLSL type of a uniform's value. @param {ShaderUniform} v */
|
|
30
|
+
function glslType(v) {
|
|
31
|
+
if (typeof v === 'number') return 'float'
|
|
32
|
+
if (v instanceof Texture) return 'sampler2D'
|
|
33
|
+
return `vec${/** @type {ArrayLike<number>} */ (v).length}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// the uniforms phoria2's shaders declare, which a hook's own can't reuse
|
|
37
|
+
let own = /** @type {Set<string> | null} */ (null)
|
|
38
|
+
const ownUniforms = () => own ??= new Set([MESH_VERTEX, POINTS_VERTEX, LINES_VERTEX, CLIP_ONLY_FRAGMENT, ...['mesh', 'points', 'lines'].flatMap((k) => shaderSource(/** @type {any} */ (k), []))]
|
|
39
|
+
.flatMap((s) => [...s.matchAll(/uniform\s+\w+\s+(\w+)/g)].map((m) => m[1])))
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The hooks a material draws with, or null: none, or ones that failed to compile.
|
|
43
|
+
* @param {Material} material
|
|
44
|
+
* @returns {Hook | null}
|
|
45
|
+
*/
|
|
46
|
+
export function hookFor(material) {
|
|
47
|
+
const s = material.shader
|
|
48
|
+
if (!s) return null
|
|
49
|
+
const types = Object.entries(s.uniforms).map(([k, v]) => `${glslType(v)} ${k}`).join(';')
|
|
50
|
+
const last = recent.get(s)
|
|
51
|
+
if (last && last.types === types) return last.hook
|
|
52
|
+
const signature = JSON.stringify([s.vertex, s.fragment, s.lit, types])
|
|
53
|
+
let hook = failed.has(signature) ? null : hooks.get(signature) ?? null
|
|
54
|
+
if (!hook && !failed.has(signature)) {
|
|
55
|
+
const clash = Object.keys(s.uniforms).find((k) => ownUniforms().has(k))
|
|
56
|
+
if (clash) {
|
|
57
|
+
failed.add(signature)
|
|
58
|
+
warnOnce(material, `shader uniform '${clash}' is one of phoria2's own - give it another name; the material is drawn without its shader hooks`)
|
|
59
|
+
} else {
|
|
60
|
+
hook = {
|
|
61
|
+
signature,
|
|
62
|
+
defines: [...(s.vertex ? ['HOOK_VERTEX'] : []), ...(s.fragment ? ['HOOK_FRAGMENT'] : []), ...(s.lit ? ['HOOK_LIT'] : []), `HOOKS ${hooks.size + 1}`],
|
|
63
|
+
source: {
|
|
64
|
+
uniforms: ['uniform float uTime;', 'uniform vec3 cameraPosition;', ...types.split(';').filter(Boolean).map((t) => `uniform ${t};`)].join('\n'),
|
|
65
|
+
helpers: HOOK_HELPERS,
|
|
66
|
+
vertex: s.vertex, fragment: s.fragment, lit: s.lit,
|
|
67
|
+
},
|
|
68
|
+
moves: !!s.vertex,
|
|
69
|
+
}
|
|
70
|
+
hooks.set(signature, hook)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
recent.set(s, { types, hook })
|
|
74
|
+
return hook
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Defines for a pass that isn't the colour pass: the vertex hook always - so a shadow, an outline or
|
|
79
|
+
* a pick is of the moved shape - and the fragment hook only where its discards should cut (meshes'
|
|
80
|
+
* shadows and picks); never the lit hook.
|
|
81
|
+
* @param {Hook} hook @param {boolean} cuts
|
|
82
|
+
*/
|
|
83
|
+
export function passDefines(hook, cuts) {
|
|
84
|
+
return hook.defines.filter((d) => d !== 'HOOK_LIT' && (cuts || d !== 'HOOK_FRAGMENT'))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Set the hook's uniforms: the clock, where the camera is, and the material's own values.
|
|
89
|
+
* @param {Program} program @param {Material} material @param {number} time
|
|
90
|
+
* @param {ArrayLike<number>} camera the camera's world position
|
|
91
|
+
* @param {(t: Texture) => WebGLTexture} texture
|
|
92
|
+
*/
|
|
93
|
+
export function setHookUniforms(program, material, time, camera, texture) {
|
|
94
|
+
program.set('uTime', time)
|
|
95
|
+
program.set('cameraPosition', camera)
|
|
96
|
+
const u = /** @type {NonNullable<Material['shader']>} */ (material.shader).uniforms
|
|
97
|
+
for (const name in u) {
|
|
98
|
+
const v = u[name]
|
|
99
|
+
program.set(name, v instanceof Texture ? texture(v) : v)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A program built with a material's hooks failed: say why, once, in the hook's own terms, and draw
|
|
105
|
+
* the material without them from now on.
|
|
106
|
+
* @param {Material} material @param {Hook} hook @param {string} log the compiler's
|
|
107
|
+
*/
|
|
108
|
+
export function hookFailed(material, hook, log) {
|
|
109
|
+
failed.add(hook.signature)
|
|
110
|
+
recent.delete(/** @type {object} */ (material.shader))
|
|
111
|
+
const names = Object.fromEntries(Object.entries(HOOK_SOURCE_NUMBERS).map(([part, n]) => [n, part]))
|
|
112
|
+
const lines = log.split('\n').map((line) => /ERROR:\s*(\d+):(\d+):\s*(.*)/.exec(line)).filter((m) => !!m)
|
|
113
|
+
const own = lines.filter((m) => names[m[1]])
|
|
114
|
+
const where = own.length
|
|
115
|
+
? own.map((m) => `shader.${names[m[1]]} line ${m[2]}: ${m[3]}`).join('; ')
|
|
116
|
+
: `in phoria2's shader around the hook: ${lines[0]?.[3] ?? log.trim().split('\n')[0]}`
|
|
117
|
+
const uniforms = Object.keys(/** @type {NonNullable<Material['shader']>} */ (material.shader).uniforms)
|
|
118
|
+
warnOnce(material, `a shader hook doesn't compile - ${where}. Hooks can use ${HOOK_NAMES}, uTime${uniforms.length ? `, ${uniforms.join(', ')}` : ''}, cameraPosition, and call hash, vnoise, snoise, snoised, vfbm, sfbm, sfbmd, voronoi, srgb, hsv2rgb, palette, fresnel, rotate2, rotate3, remap and saturate. The material is drawn without its hooks`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const HOOK_NAMES = 'local, n, uv, index, size, along (vertex); base, emissive, normal, vUv, vWorldPosition, vNormal, vColor, pointUv, distance (fragment); color, base (lit)'
|
package/src/webgl/passes.js
CHANGED
|
@@ -5,6 +5,7 @@ import * as mat4 from '../math/mat4.js'
|
|
|
5
5
|
import { irradianceSH } from '../irradiance.js'
|
|
6
6
|
import { POINT_SHADOW_SIZE, SHADOW_MAP_SIZE, SPOT_ATLAS_SIZE } from '../shadow.js'
|
|
7
7
|
import { CLIP, CLIP_ONLY_FRAGMENT, COMMON, EQUIRECT, FULLSCREEN_VERTEX, MESH_VERTEX, TONE_MAP } from './shaders.js'
|
|
8
|
+
import { hookFor, passDefines, setHookUniforms } from './hooks.js'
|
|
8
9
|
import { uvMatrix } from './resources.js'
|
|
9
10
|
|
|
10
11
|
/** @import { Programs } from './programs.js' */
|
|
@@ -281,6 +282,10 @@ const ENV_LEVELS = 6
|
|
|
281
282
|
*/
|
|
282
283
|
|
|
283
284
|
export class Passes {
|
|
285
|
+
/** The View's clock, for shader hooks that read uTime - set by the renderer each frame. */
|
|
286
|
+
time = 0
|
|
287
|
+
/** The camera's world position, for shader hooks' fresnel and cameraPosition - set each frame too. @type {ArrayLike<number>} */
|
|
288
|
+
cameraPosition = [0, 0, 0]
|
|
284
289
|
/** @type {WeakMap<Texture, Environment>} */ #environments = new WeakMap()
|
|
285
290
|
/** @type {{ texture: WebGLTexture, framebuffer: WebGLFramebuffer } | null} */ #shadow = null
|
|
286
291
|
/** Spot lights' shadows, a tile each. @type {{ texture: WebGLTexture, framebuffer: WebGLFramebuffer } | null} */ #spotShadow = null
|
|
@@ -512,8 +517,13 @@ export class Passes {
|
|
|
512
517
|
if (cutout) defines.push('ALPHA_TEST', ...(material.map ? ['MAP'] : []), ...(triplanar ? ['TRIPLANAR'] : []))
|
|
513
518
|
// a map on the second UV set reads it here too
|
|
514
519
|
if (cutout && material.map?.uvSet === 1 && item.geometry.uvs2) defines.push('UV2', 'uMapSet vUv2')
|
|
515
|
-
|
|
520
|
+
// shader hooks: the shadow of the moved shape, with a fragment hook's holes in it
|
|
521
|
+
const hook = hookFor(material)
|
|
522
|
+
if (hook) defines.push(...passDefines(hook, true))
|
|
523
|
+
const program = this.programs.ready('depth', defines, DEPTH, hook?.source)
|
|
524
|
+
if (program.failed) continue
|
|
516
525
|
gl.useProgram(program.program)
|
|
526
|
+
if (hook) setHookUniforms(program, material, this.time, this.cameraPosition, (t) => this.resources.texture(t))
|
|
517
527
|
program.set('uViewProjection', viewProjection)
|
|
518
528
|
program.set('uModel', item.worldMatrix)
|
|
519
529
|
if (item.clip) program.set('uClipPlane', item.clip.plane)
|
|
@@ -562,8 +572,13 @@ export class Passes {
|
|
|
562
572
|
for (const item of outline.items) {
|
|
563
573
|
const defines = item.instances ? ['INSTANCED'] : []
|
|
564
574
|
if (item.clip) defines.push('CLIP')
|
|
565
|
-
|
|
575
|
+
// round the moved shape
|
|
576
|
+
const hook = hookFor(item.material)
|
|
577
|
+
if (hook) defines.push(...passDefines(hook, false))
|
|
578
|
+
const program = smooth ? this.programs.ready('outline-cover', defines, OUTLINE_COVER, hook?.source) : this.programs.ready('outline-mask', defines, OUTLINE_MASK, hook?.source)
|
|
579
|
+
if (program.failed) continue
|
|
566
580
|
gl.useProgram(program.program)
|
|
581
|
+
if (hook) setHookUniforms(program, item.material, frame.time ?? 0, frame.camera.position, (t) => this.resources.texture(t))
|
|
567
582
|
program.set('uViewProjection', frame.camera.viewProjection)
|
|
568
583
|
program.set('uModel', item.worldMatrix)
|
|
569
584
|
program.set('uMask', [outline.color[0], outline.color[1], outline.color[2], px / OUTLINE_MAX])
|
package/src/webgl/picking.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// instances there are, where testing rays against 100,000 instances on the CPU takes milliseconds.
|
|
4
4
|
// Loaded the first time a View picks.
|
|
5
5
|
import { CLIP, COMMON, LINES_VERTEX, MESH_VERTEX, POINTS_VERTEX } from './shaders.js'
|
|
6
|
+
import { hookFor, passDefines, setHookUniforms } from './hooks.js'
|
|
6
7
|
import * as mat4 from '../math/mat4.js'
|
|
7
8
|
|
|
8
9
|
/** @import { Programs } from './programs.js' */
|
|
@@ -21,9 +22,16 @@ flat in uint vPickElement;
|
|
|
21
22
|
#ifdef SPRITES
|
|
22
23
|
in vec2 vUv;
|
|
23
24
|
#endif
|
|
24
|
-
#
|
|
25
|
+
#if defined(CLIP) || defined(HOOK_FRAGMENT)
|
|
25
26
|
in vec3 vWorldPosition;
|
|
26
27
|
#endif
|
|
28
|
+
#ifdef HOOK_FRAGMENT
|
|
29
|
+
// a mesh's fragment hook: what it cuts away can't be picked
|
|
30
|
+
in vec3 vNormal;
|
|
31
|
+
in vec2 vUv;
|
|
32
|
+
in vec3 vColor;
|
|
33
|
+
#endif
|
|
34
|
+
//@hook uniforms
|
|
27
35
|
uniform uint uPickId;
|
|
28
36
|
layout(location = 0) out uvec4 pickOut;
|
|
29
37
|
${COMMON}
|
|
@@ -32,6 +40,15 @@ void main() {
|
|
|
32
40
|
#ifdef CLIP
|
|
33
41
|
clipAway(vWorldPosition);
|
|
34
42
|
#endif
|
|
43
|
+
#ifdef HOOK_FRAGMENT
|
|
44
|
+
{
|
|
45
|
+
vec4 base = vec4(vColor, 1.0);
|
|
46
|
+
vec3 emissive = vec3(0.0);
|
|
47
|
+
vec3 normal = normalize(vNormal);
|
|
48
|
+
//@hook fragment
|
|
49
|
+
if (base.a <= 0.0) discard;
|
|
50
|
+
}
|
|
51
|
+
#endif
|
|
35
52
|
#ifdef ROUND
|
|
36
53
|
#ifdef SPRITES
|
|
37
54
|
vec2 p = vUv * 2.0 - 1.0;
|
|
@@ -233,7 +250,10 @@ export class Picker {
|
|
|
233
250
|
if (sprites) defines.push('SPRITES')
|
|
234
251
|
}
|
|
235
252
|
if (kind === 'mesh' && item.clip) defines.push('CLIP')
|
|
236
|
-
const
|
|
253
|
+
const hook = hookFor(material)
|
|
254
|
+
if (hook) defines.push(...passDefines(hook, kind === 'mesh'))
|
|
255
|
+
const program = this.programs.ready(`pick-${kind}`, defines, SOURCES[kind], hook?.source)
|
|
256
|
+
if (program.failed) return
|
|
237
257
|
gl.useProgram(program.program)
|
|
238
258
|
program.set('uView', frame.camera.view)
|
|
239
259
|
program.set('uProjection', this.#projection)
|
|
@@ -242,6 +262,7 @@ export class Picker {
|
|
|
242
262
|
program.set('uPickId', id)
|
|
243
263
|
program.set('uHighlightIndex', -2)
|
|
244
264
|
if (item.clip) program.set('uClipPlane', item.clip.plane)
|
|
265
|
+
if (hook) setHookUniforms(program, material, frame.time ?? 0, frame.camera.position, (t) => this.resources.texture(t))
|
|
245
266
|
|
|
246
267
|
if (kind === 'lines') {
|
|
247
268
|
gl.disable(gl.CULL_FACE)
|
package/src/webgl/programs.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// draws again when they are done. Nothing waits on a status check until a program is used.
|
|
5
5
|
import { shaderSource } from './shaders.js'
|
|
6
6
|
|
|
7
|
-
/** @import { ShaderKind } from './shaders.js' */
|
|
7
|
+
/** @import { HookSource, ShaderKind } from './shaders.js' */
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* @typedef {object} Program
|
|
@@ -13,10 +13,12 @@ import { shaderSource } from './shaders.js'
|
|
|
13
13
|
* @property {(name: string, value: any) => void} set set a uniform by name (ignored if the shader doesn't use it)
|
|
14
14
|
* @property {number} frame the frame its per-frame uniforms were last set for
|
|
15
15
|
* @property {boolean} ready compiled, linked and its uniforms found
|
|
16
|
+
* @property {string} [failed] the compiler's log, for a program with a material's shader hooks that
|
|
17
|
+
* failed - those are reported and drawn without, rather than thrown
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
|
-
* @typedef {Program & { shaders: [WebGLShader, WebGLShader], sources: [string, string] }} Building
|
|
21
|
+
* @typedef {Program & { shaders: [WebGLShader, WebGLShader], sources: [string, string], hooked: boolean }} Building
|
|
20
22
|
*/
|
|
21
23
|
|
|
22
24
|
export class Programs {
|
|
@@ -49,13 +51,14 @@ export class Programs {
|
|
|
49
51
|
* @param {ShaderKind | string} kind a drawable kind, or a pass's name when it brings its sources
|
|
50
52
|
* @param {string[]} defines
|
|
51
53
|
* @param {[string, string]} [sources] a pass's vertex and fragment shaders
|
|
54
|
+
* @param {HookSource | null} [hooks] a material's shader hooks, spliced in (its id is in the defines)
|
|
52
55
|
* @returns {Program}
|
|
53
56
|
*/
|
|
54
|
-
get(kind, defines, sources) {
|
|
57
|
+
get(kind, defines, sources, hooks = null) {
|
|
55
58
|
const key = `${kind}|${defines.join(',')}`
|
|
56
59
|
let p = this.#cache.get(key)
|
|
57
60
|
if (!p) {
|
|
58
|
-
p = this.#start(key, shaderSource(sources ?? /** @type {ShaderKind} */ (kind), defines))
|
|
61
|
+
p = this.#start(key, shaderSource(sources ?? /** @type {ShaderKind} */ (kind), defines, hooks), !!hooks)
|
|
59
62
|
this.#cache.set(key, p)
|
|
60
63
|
}
|
|
61
64
|
return p
|
|
@@ -65,10 +68,11 @@ export class Programs {
|
|
|
65
68
|
* A program, waiting for it to finish compiling if it hasn't - for passes and picking, which
|
|
66
69
|
* need their programs now.
|
|
67
70
|
* @param {ShaderKind | string} kind @param {string[]} defines @param {[string, string]} [sources]
|
|
71
|
+
* @param {HookSource | null} [hooks]
|
|
68
72
|
* @returns {Program}
|
|
69
73
|
*/
|
|
70
|
-
ready(kind, defines, sources) {
|
|
71
|
-
const p = this.get(kind, defines, sources)
|
|
74
|
+
ready(kind, defines, sources, hooks = null) {
|
|
75
|
+
const p = this.get(kind, defines, sources, hooks)
|
|
72
76
|
if (!p.ready) this.#finish(/** @type {Building} */ (p))
|
|
73
77
|
return p
|
|
74
78
|
}
|
|
@@ -115,9 +119,10 @@ export class Programs {
|
|
|
115
119
|
|
|
116
120
|
/**
|
|
117
121
|
* Start compiling and linking, without asking how it went - that would wait for it.
|
|
118
|
-
* @param {string} key @param {[string, string]} sources @
|
|
122
|
+
* @param {string} key @param {[string, string]} sources @param {boolean} [hooked] has a material's own code
|
|
123
|
+
* @returns {Program}
|
|
119
124
|
*/
|
|
120
|
-
#start(key, sources) {
|
|
125
|
+
#start(key, sources, hooked = false) {
|
|
121
126
|
const gl = this.gl
|
|
122
127
|
const shader = (type, source) => {
|
|
123
128
|
const s = /** @type {WebGLShader} */ (gl.createShader(type))
|
|
@@ -132,7 +137,7 @@ export class Programs {
|
|
|
132
137
|
gl.attachShader(program, fs)
|
|
133
138
|
gl.linkProgram(program)
|
|
134
139
|
/** @type {Building} */
|
|
135
|
-
const p = { program, key, frame: -1, ready: false, set() {}, shaders: [vs, fs], sources }
|
|
140
|
+
const p = { program, key, frame: -1, ready: false, set() {}, shaders: [vs, fs], sources, hooked }
|
|
136
141
|
// without parallel compiling there is nothing to gain by waiting until it's used
|
|
137
142
|
if (!this.#parallel) this.#finish(p)
|
|
138
143
|
return p
|
|
@@ -143,6 +148,12 @@ export class Programs {
|
|
|
143
148
|
const gl = this.gl
|
|
144
149
|
const { program, key, shaders, sources } = p
|
|
145
150
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS) && !gl.isContextLost()) {
|
|
151
|
+
// a material's own code failing is its problem, not the frame's: kept to be reported
|
|
152
|
+
if (p.hooked) {
|
|
153
|
+
p.failed = shaders.map((s) => gl.getShaderParameter(s, gl.COMPILE_STATUS) ? '' : gl.getShaderInfoLog(s) ?? '').join('\n') || (gl.getProgramInfoLog(program) ?? 'failed to link')
|
|
154
|
+
p.ready = true
|
|
155
|
+
return
|
|
156
|
+
}
|
|
146
157
|
// the shader that failed says why, with its source numbered to match the log
|
|
147
158
|
shaders.forEach((s, i) => {
|
|
148
159
|
if (gl.getShaderParameter(s, gl.COMPILE_STATUS)) return
|
package/src/webgl/resources.js
CHANGED
|
@@ -455,7 +455,7 @@ export class Resources {
|
|
|
455
455
|
if (!d.isDrawable) return
|
|
456
456
|
keys.add(d.geometry)
|
|
457
457
|
if ('instanceMatrices' in d) keys.add(d)
|
|
458
|
-
for (const [, t] of d.material.
|
|
458
|
+
for (const [, t] of d.material.allTextures) keys.add(t)
|
|
459
459
|
})
|
|
460
460
|
for (const key of keys) {
|
|
461
461
|
const users = this.#usedBy.get(key)
|