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.
- package/README.md +11 -9
- package/dist/bloom.js +4 -4
- package/dist/bloom.js.map +1 -1
- package/dist/charts.js +527 -510
- package/dist/charts.js.map +1 -1
- package/dist/core.js +1760 -1170
- package/dist/core.js.map +1 -1
- package/dist/debug.js +297 -0
- package/dist/debug.js.map +1 -0
- package/dist/edit.js +60 -60
- package/dist/edit.js.map +1 -1
- package/dist/explain.js +32 -13
- package/dist/explain.js.map +1 -1
- package/dist/gltf.js +155 -155
- package/dist/gltf.js.map +1 -1
- package/dist/keyboard.js +188 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/motion.js +35 -35
- package/dist/motion.js.map +1 -1
- package/dist/particles.js +187 -114
- 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 +95 -62
- 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 +27 -0
- package/dist/types/area.d.ts +18 -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/geometry/Geometry.d.ts +16 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/material/Material.d.ts +67 -0
- package/dist/types/material/Texture.d.ts +18 -2
- package/dist/types/particles/Emitter.d.ts +39 -2
- package/dist/types/scene/drawables.d.ts +23 -2
- 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/resources.d.ts +12 -2
- package/dist/types/webgl/shaders.d.ts +41 -5
- package/llms-full.txt +275 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/schema/scene.schema.json +75 -0
- package/src/View.js +121 -7
- package/src/area.js +131 -0
- package/src/charts/marks.js +29 -2
- package/src/debug.js +283 -0
- package/src/explain.js +19 -0
- package/src/frame.js +7 -2
- package/src/geometry/Geometry.js +16 -1
- package/src/index.js +1 -0
- package/src/material/Material.js +130 -4
- package/src/material/Texture.js +55 -2
- package/src/particles/Emitter.js +121 -10
- package/src/scene/drawables.js +89 -8
- package/src/watch.js +26 -3
- package/src/webgl/WebGLRenderer.js +55 -7
- package/src/webgl/hooks.js +121 -0
- package/src/webgl/passes.js +17 -2
- package/src/webgl/picking.js +45 -3
- package/src/webgl/programs.js +20 -9
- package/src/webgl/resources.js +169 -15
- 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
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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)'
|
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' */
|
|
@@ -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
|
|
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
|
|
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.
|
|
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)
|
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
|