phoria2 1.2.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.
Files changed (69) hide show
  1. package/README.md +11 -9
  2. package/dist/bloom.js +4 -4
  3. package/dist/bloom.js.map +1 -1
  4. package/dist/charts.js +527 -510
  5. package/dist/charts.js.map +1 -1
  6. package/dist/core.js +1760 -1170
  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 +60 -60
  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 +155 -155
  15. package/dist/gltf.js.map +1 -1
  16. package/dist/keyboard.js +188 -0
  17. package/dist/keyboard.js.map +1 -0
  18. package/dist/motion.js +35 -35
  19. package/dist/motion.js.map +1 -1
  20. package/dist/particles.js +187 -114
  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 +95 -62
  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 +27 -0
  30. package/dist/types/area.d.ts +18 -0
  31. package/dist/types/debug.d.ts +74 -0
  32. package/dist/types/explain.d.ts +4 -0
  33. package/dist/types/frame.d.ts +5 -0
  34. package/dist/types/geometry/Geometry.d.ts +16 -0
  35. package/dist/types/index.d.ts +2 -0
  36. package/dist/types/material/Material.d.ts +67 -0
  37. package/dist/types/material/Texture.d.ts +18 -2
  38. package/dist/types/particles/Emitter.d.ts +39 -2
  39. package/dist/types/scene/drawables.d.ts +23 -2
  40. package/dist/types/watch.d.ts +8 -1
  41. package/dist/types/webgl/hooks.d.ts +48 -0
  42. package/dist/types/webgl/passes.d.ts +4 -0
  43. package/dist/types/webgl/programs.d.ts +15 -5
  44. package/dist/types/webgl/resources.d.ts +12 -2
  45. package/dist/types/webgl/shaders.d.ts +41 -5
  46. package/llms-full.txt +275 -1
  47. package/llms.txt +1 -1
  48. package/package.json +1 -1
  49. package/schema/scene.schema.json +75 -0
  50. package/src/View.js +121 -7
  51. package/src/area.js +131 -0
  52. package/src/charts/marks.js +29 -2
  53. package/src/debug.js +283 -0
  54. package/src/explain.js +19 -0
  55. package/src/frame.js +7 -2
  56. package/src/geometry/Geometry.js +16 -1
  57. package/src/index.js +1 -0
  58. package/src/material/Material.js +130 -4
  59. package/src/material/Texture.js +55 -2
  60. package/src/particles/Emitter.js +121 -10
  61. package/src/scene/drawables.js +89 -8
  62. package/src/watch.js +26 -3
  63. package/src/webgl/WebGLRenderer.js +55 -7
  64. package/src/webgl/hooks.js +121 -0
  65. package/src/webgl/passes.js +17 -2
  66. package/src/webgl/picking.js +45 -3
  67. package/src/webgl/programs.js +20 -9
  68. package/src/webgl/resources.js +169 -15
  69. package/src/webgl/shaders.js +545 -38
@@ -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'
@@ -117,6 +118,8 @@ export class WebGLRenderer {
117
118
  /** This frame's prefiltered environment, when it is an HDR image. @type {Environment | null} */ #environment = null
118
119
  /** This frame's shadows - directional, spot, point - for the meshes that receive them. @type {import('./passes.js').ShadowMap[]} */ #shadows = []
119
120
  /** @type {Float32Array} */ #normalMatrix = new Float32Array(9)
121
+ /** Model-view for sorting points back to front. */
122
+ #modelView = new Float32Array(16)
120
123
  /** @type {Record<string, any>} */ #lights = {}
121
124
  /** This frame waits for shaders to compile rather than leaving things out. */
122
125
  #wait = false
@@ -221,6 +224,11 @@ export class WebGLRenderer {
221
224
  const env = frame.environment
222
225
  const bg = frame.background
223
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
+ }
224
232
  this.#environment = passes && typeof env === 'object' ? passes.environment(env) : null
225
233
  if (passes && typeof env === 'object' && env.image && !this.#environment) {
226
234
  warnOnce(env, env.isHDR ? "this device can't render to float textures, which HDR environments need - lighting without it"
@@ -407,6 +415,7 @@ export class WebGLRenderer {
407
415
 
408
416
  const clip = kind === 'mesh' ? item.clip : null
409
417
  const defines = this.#defines(kind, material, geometry.colors !== null, !!instances, geometry.uvs2 !== null)
418
+ if (kind === 'points' && /** @type {any} */ (node).drawsSprites) defines.push('SPRITES')
410
419
  for (const shadow of shadows) defines.push(shadow.define)
411
420
  const fog = material.fog ? frame.fog : null
412
421
  if (fog) {
@@ -416,15 +425,25 @@ export class WebGLRenderer {
416
425
  else if (material.blending === 'multiply') defines.push('FOG_MULTIPLY')
417
426
  }
418
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)
419
431
  const programs = this.#gpu.programs
420
- const program = programs.get(kind, defines)
432
+ const program = programs.get(kind, defines, undefined, hook?.source)
421
433
  if (!programs.isReady(program)) {
422
434
  if (!this.#wait) {
423
435
  // still compiling: drawn once it's done, rather than holding up everything else
424
436
  this.info.compiling++
425
437
  return
426
438
  }
427
- 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
428
447
  }
429
448
  gl.useProgram(program.program)
430
449
 
@@ -469,6 +488,7 @@ export class WebGLRenderer {
469
488
  }
470
489
  program.set('uColor', material.color)
471
490
  program.set('uOpacity', material.opacity)
491
+ if (hook) setHookUniforms(program, material, frame.time ?? 0, frame.camera.position, (t) => this.#gpu.resources.texture(t))
472
492
  if (fog) {
473
493
  program.set('uFogColor', fog.color)
474
494
  program.set('uFogRange', [fog.near, fog.far])
@@ -531,9 +551,18 @@ export class WebGLRenderer {
531
551
  if (kind === 'lines') {
532
552
  gl.disable(gl.CULL_FACE)
533
553
  program.set('uWidth', material.width * this.pixelRatio)
554
+ const arrows = /** @type {any} */ (node).arrowBits ?? 0
555
+ program.set('uArrows', arrows)
556
+ program.set('uArrowSize', /** @type {any} */ (node).arrowPixels?.(material.width) * this.pixelRatio || 0)
557
+ if (material.dash > 0) {
558
+ program.set('uDash', material.dash)
559
+ program.set('uGap', material.gap || material.dash)
560
+ program.set('uDashOffset', material.dashOffset)
561
+ }
534
562
  const buffers = this.#gpu.resources.lines(geometry)
535
563
  gl.bindVertexArray(buffers.vao)
536
- gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, Math.min(buffers.count, Math.floor(geometry.drawnCount / 2)))
564
+ // a quad a segment, and two arrowheads where there are any
565
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, arrows ? 12 : 6, Math.min(buffers.count, Math.floor(geometry.drawnCount / 2)))
537
566
  this.info.drawCalls++
538
567
  return
539
568
  }
@@ -541,9 +570,27 @@ export class WebGLRenderer {
541
570
  if (kind === 'points') {
542
571
  gl.disable(gl.CULL_FACE)
543
572
  program.set('uSize', material.sizeAttenuation ? material.size : material.size * this.pixelRatio)
544
- const buffers = this.#gpu.resources.mesh(geometry, null, false)
545
- gl.bindVertexArray(buffers.vao)
546
- gl.drawArrays(gl.POINTS, 0, geometry.drawnVertices)
573
+ // round points are solid, but their edges are soft: blended over what is behind even in the
574
+ // opaque pass, or the edge fades to black - a dark ring round every dot
575
+ const round = !transparent && !material.map
576
+ if (round) {
577
+ gl.enable(gl.BLEND)
578
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
579
+ }
580
+ if (/** @type {any} */ (node).drawsSprites) {
581
+ // each point a square, any size and turned - drawn back to front when the points ask, for
582
+ // smoke and other soft things that hide what is behind them
583
+ program.set('uResolution', [this.canvas.width, this.canvas.height])
584
+ const modelView = /** @type {any} */ (node).sort ? mat4.multiply(this.#modelView, frame.camera.view, worldMatrix) : null
585
+ const buffers = this.#gpu.resources.sprites(geometry, modelView)
586
+ gl.bindVertexArray(buffers.vao)
587
+ gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, buffers.count)
588
+ } else {
589
+ const buffers = this.#gpu.resources.mesh(geometry, null, false)
590
+ gl.bindVertexArray(buffers.vao)
591
+ gl.drawArrays(gl.POINTS, 0, geometry.drawnVertices)
592
+ }
593
+ if (round) gl.disable(gl.BLEND)
547
594
  this.info.drawCalls++
548
595
  return
549
596
  }
@@ -676,7 +723,8 @@ export class WebGLRenderer {
676
723
  } else if (kind === 'points') {
677
724
  if (material.map) d.push('MAP')
678
725
  if (material.sizeAttenuation) d.push('SIZE_ATTENUATION')
679
- }
726
+ } else if (material.dash > 0) d.push('DASHED')
727
+ // (points drawn as squares add SPRITES - see #draw, which knows the node)
680
728
  return d
681
729
  }
682
730
  }
@@ -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' */
@@ -18,9 +19,19 @@ const PICK_FRAGMENT = /* glsl */ `
18
19
  precision highp float;
19
20
  precision highp int;
20
21
  flat in uint vPickElement;
21
- #ifdef CLIP
22
+ #ifdef SPRITES
23
+ in vec2 vUv;
24
+ #endif
25
+ #if defined(CLIP) || defined(HOOK_FRAGMENT)
22
26
  in vec3 vWorldPosition;
23
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
24
35
  uniform uint uPickId;
25
36
  layout(location = 0) out uvec4 pickOut;
26
37
  ${COMMON}
@@ -29,8 +40,21 @@ void main() {
29
40
  #ifdef CLIP
30
41
  clipAway(vWorldPosition);
31
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
32
52
  #ifdef ROUND
53
+ #ifdef SPRITES
54
+ vec2 p = vUv * 2.0 - 1.0;
55
+ #else
33
56
  vec2 p = gl_PointCoord * 2.0 - 1.0;
57
+ #endif
34
58
  if (dot(p, p) > 1.0) discard;
35
59
  #endif
36
60
  pickOut = uvec4(uPickId, vPickElement, floatBitsToUint(gl_FragCoord.z), 1u);
@@ -219,12 +243,17 @@ export class Picker {
219
243
  const kind = mode === 'points' ? 'points' : mode === 'lines' ? 'lines' : 'mesh'
220
244
  const defines = ['PICK']
221
245
  if (instances) defines.push('INSTANCED')
246
+ const sprites = kind === 'points' && /** @type {any} */ (node).drawsSprites
222
247
  if (kind === 'points') {
223
248
  if (material.sizeAttenuation) defines.push('SIZE_ATTENUATION')
224
249
  if (!material.map) defines.push('ROUND')
250
+ if (sprites) defines.push('SPRITES')
225
251
  }
226
252
  if (kind === 'mesh' && item.clip) defines.push('CLIP')
227
- 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
228
257
  gl.useProgram(program.program)
229
258
  program.set('uView', frame.camera.view)
230
259
  program.set('uProjection', this.#projection)
@@ -233,6 +262,7 @@ export class Picker {
233
262
  program.set('uPickId', id)
234
263
  program.set('uHighlightIndex', -2)
235
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))
236
266
 
237
267
  if (kind === 'lines') {
238
268
  gl.disable(gl.CULL_FACE)
@@ -240,9 +270,13 @@ export class Picker {
240
270
  program.set('uResolution', [REGION, REGION])
241
271
  program.set('uWidth', material.width * pixelRatio)
242
272
  program.set('uNear', frame.camera.near)
273
+ // arrowheads pick as their segment; dashes' gaps do too, so a dashed link is as easy to hit
274
+ const arrows = /** @type {any} */ (node).arrowBits ?? 0
275
+ program.set('uArrows', arrows)
276
+ program.set('uArrowSize', /** @type {any} */ (node).arrowPixels?.(material.width) * pixelRatio || 0)
243
277
  const buffers = this.resources.lines(geometry)
244
278
  gl.bindVertexArray(buffers.vao)
245
- gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, Math.min(buffers.count, Math.floor(geometry.drawnCount / 2)))
279
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, arrows ? 12 : 6, Math.min(buffers.count, Math.floor(geometry.drawnCount / 2)))
246
280
  return
247
281
  }
248
282
  if (kind === 'points') {
@@ -250,6 +284,14 @@ export class Picker {
250
284
  // sizes as drawn: world sizes scale by the real projection, not the zoomed one
251
285
  program.set('uSize', material.sizeAttenuation ? material.size : material.size * pixelRatio)
252
286
  program.set('uScale', frame.camera.projection[5] * height / 2)
287
+ if (sprites) {
288
+ // squares, in whatever order they were last drawn - each says which point it is
289
+ program.set('uResolution', [REGION, REGION])
290
+ const buffers = this.resources.sprites(geometry)
291
+ gl.bindVertexArray(buffers.vao)
292
+ gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, buffers.count)
293
+ return
294
+ }
253
295
  const buffers = this.resources.mesh(geometry, null, false)
254
296
  gl.bindVertexArray(buffers.vao)
255
297
  gl.drawArrays(gl.POINTS, 0, geometry.drawnVertices)
@@ -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