phoria2 1.3.0 → 1.4.1

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.
Files changed (56) hide show
  1. package/README.md +9 -6
  2. package/dist/bloom.js +4 -4
  3. package/dist/bloom.js.map +1 -1
  4. package/dist/charts.js +436 -436
  5. package/dist/charts.js.map +1 -1
  6. package/dist/core.js +1240 -862
  7. package/dist/core.js.map +1 -1
  8. package/dist/debug.js +297 -0
  9. package/dist/debug.js.map +1 -0
  10. package/dist/edit.js +40 -40
  11. package/dist/edit.js.map +1 -1
  12. package/dist/explain.js +32 -13
  13. package/dist/explain.js.map +1 -1
  14. package/dist/gltf.js +128 -128
  15. package/dist/gltf.js.map +1 -1
  16. package/dist/keyboard.js +19 -19
  17. package/dist/keyboard.js.map +1 -1
  18. package/dist/motion.js +35 -35
  19. package/dist/motion.js.map +1 -1
  20. package/dist/particles.js +36 -36
  21. package/dist/particles.js.map +1 -1
  22. package/dist/passes.js +116 -104
  23. package/dist/passes.js.map +1 -1
  24. package/dist/phoria2.js +1 -1
  25. package/dist/picking.js +90 -71
  26. package/dist/picking.js.map +1 -1
  27. package/dist/snapshot.js +1 -1
  28. package/dist/tooltip.js +1 -1
  29. package/dist/types/View.d.ts +16 -0
  30. package/dist/types/debug.d.ts +74 -0
  31. package/dist/types/explain.d.ts +4 -0
  32. package/dist/types/frame.d.ts +5 -0
  33. package/dist/types/material/Material.d.ts +52 -0
  34. package/dist/types/watch.d.ts +8 -1
  35. package/dist/types/webgl/hooks.d.ts +48 -0
  36. package/dist/types/webgl/passes.d.ts +4 -0
  37. package/dist/types/webgl/programs.d.ts +15 -5
  38. package/dist/types/webgl/shaders.d.ts +36 -5
  39. package/llms-full.txt +227 -0
  40. package/llms.txt +1 -1
  41. package/package.json +1 -1
  42. package/schema/scene.schema.json +27 -0
  43. package/src/View.js +105 -7
  44. package/src/debug.js +283 -0
  45. package/src/explain.js +19 -0
  46. package/src/frame.js +7 -2
  47. package/src/material/Material.js +121 -2
  48. package/src/scene/drawables.js +21 -4
  49. package/src/watch.js +18 -2
  50. package/src/webgl/WebGLRenderer.js +19 -2
  51. package/src/webgl/hooks.js +121 -0
  52. package/src/webgl/passes.js +17 -2
  53. package/src/webgl/picking.js +23 -2
  54. package/src/webgl/programs.js +20 -9
  55. package/src/webgl/resources.js +1 -1
  56. 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)'
@@ -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
- const program = this.programs.ready('depth', defines, DEPTH)
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
- const program = smooth ? this.programs.ready('outline-cover', defines, OUTLINE_COVER) : this.programs.ready('outline-mask', defines, OUTLINE_MASK)
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])
@@ -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
- #ifdef CLIP
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 program = this.programs.ready(`pick-${kind}`, defines, SOURCES[kind])
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)
@@ -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 @returns {Program}
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
@@ -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.textures) keys.add(t)
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)