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
@@ -10,6 +10,9 @@ import { isVideo } from '../material/Texture.js'
10
10
 
11
11
  // uploads bind textures here, out of the way of the units programs sample from
12
12
  const UPLOAD_UNIT = 15
13
+ // past this many changed regions since the last upload, those in each cell of a GRID x GRID split of
14
+ // the image go as one rectangle round them - nearby changes together, far ones still apart
15
+ const MOST_REGIONS = 16, GRID = 4
13
16
 
14
17
  /**
15
18
  * @typedef {object} MeshBuffers
@@ -109,6 +112,11 @@ export function uvMatrix(t) {
109
112
 
110
113
  // a quad as a triangle strip of [along, across] corners, shared by every Lines draw
111
114
  const LINE_CORNERS = new Float32Array([0, -1, 0, 1, 1, -1, 1, 1])
115
+ // a point drawn as a square: position, colour, size, opacity, angle and index, as [location, floats,
116
+ // offset in floats]
117
+ const SPRITE_LAYOUT = [[ATTRIBUTES.position, 3, 0], [ATTRIBUTES.color, 3, 3], [ATTRIBUTES.pointSize, 1, 6],
118
+ [ATTRIBUTES.pointOpacity, 1, 7], [ATTRIBUTES.pointAngle, 1, 8], [ATTRIBUTES.pointIndex, 1, 9]]
119
+ const SPRITE_STRIDE = 10
112
120
 
113
121
  export class Resources {
114
122
  /** @type {WebGL2RenderingContext} */ gl
@@ -116,7 +124,8 @@ export class Resources {
116
124
  /** @type {WeakMap<object, MeshBuffers>} */ #triangles = new WeakMap()
117
125
  /** @type {WeakMap<object, MeshBuffers>} */ #points = new WeakMap()
118
126
  /** @type {WeakMap<object, MeshBuffers>} */ #lines = new WeakMap()
119
- /** @type {WeakMap<Texture, { texture: WebGLTexture, version: number }>} */ #textures = new WeakMap()
127
+ /** Points drawn as squares: an instance a point. @type {WeakMap<object, MeshBuffers & { capacity: number, sorted: boolean, data: Float32Array }>} */ #sprites = new WeakMap()
128
+ /** The GPU copy of each texture: the version it holds, and the storage it was made with. @type {WeakMap<Texture, { texture: WebGLTexture, version: number, width: number, height: number, internal: number }>} */ #textures = new WeakMap()
120
129
  /** @type {WebGLTexture | null} */ #white = null
121
130
  /** @type {WebGLBuffer | null} */ #corners = null
122
131
  /** Counts for renderer.info. */
@@ -284,8 +293,8 @@ export class Resources {
284
293
  }
285
294
 
286
295
  /**
287
- * Vertex array for Lines: one instance per segment with its two endpoints (and colours),
288
- * expanded to a screen-space quad in the vertex shader.
296
+ * Vertex array for Lines: one instance per segment with its two endpoints (and colours) and where
297
+ * it is along its chain, expanded to a screen-space quad - and arrowheads - in the vertex shader.
289
298
  * @param {Geometry} geometry
290
299
  * @returns {MeshBuffers}
291
300
  */
@@ -301,6 +310,9 @@ export class Resources {
301
310
  const segments = geometry.elementCount / 2
302
311
  const ends = new Float32Array(segments * 6)
303
312
  const colors = new Float32Array(segments * 6).fill(1)
313
+ // chains: runs of segments each starting where the one before ended - a polyline. Dashes run
314
+ // on along a chain, and arrowheads go at its ends
315
+ const info = new Float32Array(segments * 3)
304
316
  for (let s = 0; s < segments; s++) {
305
317
  const i0 = idx ? idx[s * 2] : s * 2, i1 = idx ? idx[s * 2 + 1] : s * 2 + 1
306
318
  for (let k = 0; k < 3; k++) {
@@ -311,6 +323,12 @@ export class Resources {
311
323
  colors[s * 6 + 3 + k] = c[i1 * 3 + k]
312
324
  }
313
325
  }
326
+ const o = s * 6, joined = s > 0 && ends[o - 3] === ends[o] && ends[o - 2] === ends[o + 1] && ends[o - 1] === ends[o + 2]
327
+ if (joined) {
328
+ info[s * 3] = info[s * 3 - 3] + Math.hypot(ends[o - 3] - ends[o - 6], ends[o - 2] - ends[o - 5], ends[o - 1] - ends[o - 4])
329
+ info[s * 3 - 1] = 0
330
+ } else info[s * 3 + 1] = 1
331
+ info[s * 3 + 2] = 1
314
332
  }
315
333
 
316
334
  const vao = /** @type {WebGLVertexArrayObject} */ (gl.createVertexArray())
@@ -330,6 +348,13 @@ export class Resources {
330
348
  }
331
349
  interleavedPair(ends, ATTRIBUTES.segmentStart, ATTRIBUTES.segmentEnd)
332
350
  interleavedPair(colors, ATTRIBUTES.segmentColorStart, ATTRIBUTES.segmentColorEnd)
351
+ const chains = /** @type {WebGLBuffer} */ (gl.createBuffer())
352
+ gl.bindBuffer(gl.ARRAY_BUFFER, chains)
353
+ gl.bufferData(gl.ARRAY_BUFFER, info, gl.STATIC_DRAW)
354
+ gl.enableVertexAttribArray(ATTRIBUTES.segmentInfo)
355
+ gl.vertexAttribPointer(ATTRIBUTES.segmentInfo, 3, gl.FLOAT, false, 0, 0)
356
+ gl.vertexAttribDivisor(ATTRIBUTES.segmentInfo, 1)
357
+ buffers.push(chains)
333
358
  gl.bindBuffer(gl.ARRAY_BUFFER, this.#cornerBuffer())
334
359
  gl.enableVertexAttribArray(ATTRIBUTES.corner)
335
360
  gl.vertexAttribPointer(ATTRIBUTES.corner, 2, gl.FLOAT, false, 0, 0)
@@ -354,7 +379,7 @@ export class Resources {
354
379
  // a video with no frame yet: what it showed before, or white - and try again next frame
355
380
  if (isVideo(texture.image) && /** @type {HTMLVideoElement} */ (texture.image).readyState < 2) return entry?.texture ?? this.#whiteTexture()
356
381
  if (!entry) {
357
- entry = { texture: /** @type {WebGLTexture} */ (gl.createTexture()), version: -1 }
382
+ entry = { texture: /** @type {WebGLTexture} */ (gl.createTexture()), version: -1, width: 0, height: 0, internal: 0 }
358
383
  this.#textures.set(texture, entry)
359
384
  this.counts.textures++
360
385
  texture.onDispose(() => this.#freeTexture(texture))
@@ -364,16 +389,37 @@ export class Resources {
364
389
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, texture.flipY)
365
390
  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false)
366
391
  const image = texture.image
367
- if ('data' in image) {
368
- // raw pixels: bytes as usual, floats (HDR) as half floats, which filter everywhere
369
- const d = /** @type {DataImage} */ (image)
370
- const float = d.data instanceof Float32Array
371
- const internal = float ? gl.RGBA16F : texture.colorSpace === 'srgb' ? gl.SRGB8_ALPHA8 : gl.RGBA8
372
- gl.texImage2D(gl.TEXTURE_2D, 0, internal, d.width, d.height, 0, gl.RGBA, float ? gl.FLOAT : gl.UNSIGNED_BYTE, d.data)
392
+ const data = 'data' in image ? /** @type {DataImage} */ (image) : null
393
+ // raw pixels: bytes as usual, floats (HDR) as half floats, which filter everywhere; sRGB textures
394
+ // are stored as sRGB so sampling returns linear values, filtered correctly
395
+ const float = !!data && data.data instanceof Float32Array
396
+ const internal = float ? gl.RGBA16F : texture.colorSpace === 'srgb' ? gl.SRGB8_ALPHA8 : gl.RGBA8
397
+ const type = float ? gl.FLOAT : gl.UNSIGNED_BYTE
398
+ const [width, height] = sizeOf(image)
399
+ if (entry.version < 0 || entry.width !== width || entry.height !== height || entry.internal !== internal) {
400
+ // new storage: the first upload, or the image changed size or kind
401
+ if (data) gl.texImage2D(gl.TEXTURE_2D, 0, internal, width, height, 0, gl.RGBA, type, data.data)
402
+ else gl.texImage2D(gl.TEXTURE_2D, 0, internal, gl.RGBA, type, /** @type {TexImageSource} */ (image))
403
+ Object.assign(entry, { width, height, internal })
373
404
  } else {
374
- // sRGB textures are stored as sRGB so sampling returns linear values, filtered correctly
375
- const internal = texture.colorSpace === 'srgb' ? gl.SRGB8_ALPHA8 : gl.RGBA8
376
- gl.texImage2D(gl.TEXTURE_2D, 0, internal, gl.RGBA, gl.UNSIGNED_BYTE, /** @type {TexImageSource} */ (image))
405
+ // the same storage, refilled: just the parts that changed, when the updates since said - a
406
+ // canvas drawn on a little at a time - or the whole image (a video's next frame). A flipped
407
+ // image's rows run the other way, so it goes whole
408
+ let regions = texture.flipY ? null : texture.changedSince(entry.version)
409
+ if (regions && regions.length > MOST_REGIONS) regions = byCell(regions, width, height)
410
+ for (const r of regions ?? [{ x: 0, y: 0, width, height }]) {
411
+ // inside the image: a region over its edge is cut there, one off it sends nothing
412
+ const x = Math.max(0, r.x), y = Math.max(0, r.y)
413
+ const w = Math.min(width, r.x + r.width) - x, h = Math.min(height, r.y + r.height) - y
414
+ if (!(w > 0 && h > 0)) continue
415
+ gl.pixelStorei(gl.UNPACK_SKIP_PIXELS, x)
416
+ gl.pixelStorei(gl.UNPACK_SKIP_ROWS, y)
417
+ if (data) gl.pixelStorei(gl.UNPACK_ROW_LENGTH, width)
418
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, w, h, gl.RGBA, type, /** @type {any} */ (data ? data.data : image))
419
+ }
420
+ gl.pixelStorei(gl.UNPACK_SKIP_PIXELS, 0)
421
+ gl.pixelStorei(gl.UNPACK_SKIP_ROWS, 0)
422
+ gl.pixelStorei(gl.UNPACK_ROW_LENGTH, 0)
377
423
  }
378
424
  const wrap = { repeat: gl.REPEAT, clamp: gl.CLAMP_TO_EDGE, mirror: gl.MIRRORED_REPEAT }[texture.wrap]
379
425
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap)
@@ -409,13 +455,13 @@ export class Resources {
409
455
  if (!d.isDrawable) return
410
456
  keys.add(d.geometry)
411
457
  if ('instanceMatrices' in d) keys.add(d)
412
- for (const [, t] of d.material.textures) keys.add(t)
458
+ for (const [, t] of d.material.allTextures) keys.add(t)
413
459
  })
414
460
  for (const key of keys) {
415
461
  const users = this.#usedBy.get(key)
416
462
  if (!users?.delete(owner) || users.size) continue
417
463
  this.#usedBy.delete(key)
418
- for (const cache of [this.#triangles, this.#points, this.#lines]) {
464
+ for (const cache of [this.#triangles, this.#points, this.#lines, this.#sprites]) {
419
465
  const entry = cache.get(key)
420
466
  if (!entry) continue
421
467
  this.#free(entry)
@@ -434,6 +480,64 @@ export class Resources {
434
480
  users.add(this.owner)
435
481
  }
436
482
 
483
+ /**
484
+ * Vertex array for points drawn as squares: one instance a point - its position, colour, size,
485
+ * opacity, angle and index - over the corner strip lines use. Given a model-view matrix, the points
486
+ * go back to front (furthest first) - re-sorted every call, as the camera may have moved; without
487
+ * one, in their own order; with undefined, in whatever order they were last sent (picking, which
488
+ * reads each point's index and doesn't mind).
489
+ * @param {Geometry} geometry @param {ArrayLike<number> | null} [modelView]
490
+ * @returns {MeshBuffers}
491
+ */
492
+ sprites(geometry, modelView) {
493
+ this.#mark(geometry)
494
+ const gl = this.gl
495
+ let entry = this.#sprites.get(geometry)
496
+ const sorting = !!modelView
497
+ if (entry && entry.version === geometry.version && !sorting && (modelView === undefined || !entry.sorted)) return entry
498
+ const n = geometry.drawnVertices, count = geometry.vertexCount
499
+ if (!entry || entry.capacity < count) {
500
+ if (entry) this.#free(entry)
501
+ else this.#watch(geometry, geometry, this.#sprites)
502
+ const vao = /** @type {WebGLVertexArrayObject} */ (gl.createVertexArray())
503
+ gl.bindVertexArray(vao)
504
+ const buffer = /** @type {WebGLBuffer} */ (gl.createBuffer())
505
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
506
+ gl.bufferData(gl.ARRAY_BUFFER, count * SPRITE_STRIDE * 4, gl.DYNAMIC_DRAW)
507
+ for (const [location, size, offset] of SPRITE_LAYOUT) {
508
+ gl.enableVertexAttribArray(location)
509
+ gl.vertexAttribPointer(location, size, gl.FLOAT, false, SPRITE_STRIDE * 4, offset * 4)
510
+ gl.vertexAttribDivisor(location, 1)
511
+ }
512
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.#cornerBuffer())
513
+ gl.enableVertexAttribArray(ATTRIBUTES.corner)
514
+ gl.vertexAttribPointer(ATTRIBUTES.corner, 2, gl.FLOAT, false, 0, 0)
515
+ gl.bindVertexArray(null)
516
+ entry = { vao, buffers: [buffer], count: 0, indexType: 0, geometry, version: -1, instanceVersion: 0, capacity: count, sorted: false, data: new Float32Array(count * SPRITE_STRIDE) }
517
+ this.#sprites.set(geometry, entry)
518
+ }
519
+ const order = sorting ? backToFront(geometry.positions, n, /** @type {ArrayLike<number>} */ (modelView)) : null
520
+ const p = geometry.positions, c = geometry.colors, sizes = geometry.sizes, opacities = geometry.opacities, angles = geometry.angles
521
+ const data = entry.data
522
+ for (let j = 0; j < n; j++) {
523
+ const i = order ? order[j] : j, o = j * SPRITE_STRIDE
524
+ data[o] = p[i * 3]
525
+ data[o + 1] = p[i * 3 + 1]
526
+ data[o + 2] = p[i * 3 + 2]
527
+ data[o + 3] = c ? c[i * 3] : 1
528
+ data[o + 4] = c ? c[i * 3 + 1] : 1
529
+ data[o + 5] = c ? c[i * 3 + 2] : 1
530
+ data[o + 6] = sizes ? sizes[i] : 1
531
+ data[o + 7] = opacities ? opacities[i] : 1
532
+ data[o + 8] = angles ? angles[i] : 0
533
+ data[o + 9] = i
534
+ }
535
+ gl.bindBuffer(gl.ARRAY_BUFFER, entry.buffers[0])
536
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, data, 0, n * SPRITE_STRIDE)
537
+ Object.assign(entry, { count: n, version: geometry.version, sorted: sorting })
538
+ return entry
539
+ }
540
+
437
541
  /** @param {Texture} texture */
438
542
  #freeTexture(texture) {
439
543
  const entry = this.#textures.get(texture)
@@ -448,6 +552,7 @@ export class Resources {
448
552
  this.#triangles = new WeakMap()
449
553
  this.#points = new WeakMap()
450
554
  this.#lines = new WeakMap()
555
+ this.#sprites = new WeakMap()
451
556
  this.#textures = new WeakMap()
452
557
  this.#usedBy = new WeakMap()
453
558
  this.#white = null
@@ -497,3 +602,52 @@ export class Resources {
497
602
  return this.#corners
498
603
  }
499
604
  }
605
+
606
+ /**
607
+ * An image's size in pixels - a video's frames, an <img>'s own pixels rather than its size on the page.
608
+ * @param {any} image @returns {[number, number]}
609
+ */
610
+ function sizeOf(image) {
611
+ if (isVideo(image)) return [image.videoWidth, image.videoHeight]
612
+ if ('naturalWidth' in image) return [image.naturalWidth, image.naturalHeight]
613
+ return [image.width, image.height]
614
+ }
615
+
616
+ /**
617
+ * Many regions as a few: those whose middles fall in the same cell of a GRID x GRID split of the
618
+ * image, as one rectangle round them.
619
+ * @param {import('../material/Texture.js').TextureRegion[]} regions @param {number} width @param {number} height
620
+ */
621
+ function byCell(regions, width, height) {
622
+ /** @type {Map<number, number[]>} x0, y0, x1, y1 round each cell's regions */
623
+ const cells = new Map()
624
+ for (const r of regions) {
625
+ const cx = Math.min(GRID - 1, Math.max(0, Math.floor((r.x + r.width / 2) / width * GRID)))
626
+ const cy = Math.min(GRID - 1, Math.max(0, Math.floor((r.y + r.height / 2) / height * GRID)))
627
+ const key = cy * GRID + cx, box = cells.get(key)
628
+ if (!box) cells.set(key, [r.x, r.y, r.x + r.width, r.y + r.height])
629
+ else {
630
+ box[0] = Math.min(box[0], r.x)
631
+ box[1] = Math.min(box[1], r.y)
632
+ box[2] = Math.max(box[2], r.x + r.width)
633
+ box[3] = Math.max(box[3], r.y + r.height)
634
+ }
635
+ }
636
+ return [...cells.values()].map(([x0, y0, x1, y1]) => ({ x: x0, y: y0, width: x1 - x0, height: y1 - y0 }))
637
+ }
638
+
639
+ /**
640
+ * The first n points' order furthest from the camera first: by their depth in view, given the
641
+ * model-view matrix.
642
+ * @param {Float32Array} p @param {number} n @param {ArrayLike<number>} m
643
+ * @returns {Uint32Array}
644
+ */
645
+ function backToFront(p, n, m) {
646
+ const depth = new Float32Array(n), order = new Uint32Array(n)
647
+ for (let i = 0; i < n; i++) {
648
+ // view space z: more negative is further away
649
+ depth[i] = m[2] * p[i * 3] + m[6] * p[i * 3 + 1] + m[10] * p[i * 3 + 2] + m[14]
650
+ order[i] = i
651
+ }
652
+ return order.sort((a, b) => depth[a] - depth[b])
653
+ }