phoria2 1.2.0 → 1.3.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 (50) hide show
  1. package/README.md +2 -3
  2. package/dist/charts.js +476 -459
  3. package/dist/charts.js.map +1 -1
  4. package/dist/core.js +1192 -963
  5. package/dist/core.js.map +1 -1
  6. package/dist/edit.js +75 -75
  7. package/dist/edit.js.map +1 -1
  8. package/dist/explain.js +1 -1
  9. package/dist/gltf.js +84 -84
  10. package/dist/gltf.js.map +1 -1
  11. package/dist/keyboard.js +188 -0
  12. package/dist/keyboard.js.map +1 -0
  13. package/dist/motion.js +35 -35
  14. package/dist/motion.js.map +1 -1
  15. package/dist/particles.js +191 -118
  16. package/dist/particles.js.map +1 -1
  17. package/dist/passes.js +16 -16
  18. package/dist/passes.js.map +1 -1
  19. package/dist/phoria2.js +1 -1
  20. package/dist/picking.js +30 -14
  21. package/dist/picking.js.map +1 -1
  22. package/dist/snapshot.js +1 -1
  23. package/dist/tooltip.js +1 -1
  24. package/dist/types/View.d.ts +11 -0
  25. package/dist/types/area.d.ts +18 -0
  26. package/dist/types/geometry/Geometry.d.ts +16 -0
  27. package/dist/types/index.d.ts +2 -0
  28. package/dist/types/material/Material.d.ts +15 -0
  29. package/dist/types/material/Texture.d.ts +18 -2
  30. package/dist/types/particles/Emitter.d.ts +39 -2
  31. package/dist/types/scene/drawables.d.ts +23 -2
  32. package/dist/types/webgl/resources.d.ts +12 -2
  33. package/dist/types/webgl/shaders.d.ts +7 -2
  34. package/llms-full.txt +48 -1
  35. package/package.json +1 -1
  36. package/schema/scene.schema.json +48 -0
  37. package/src/View.js +16 -0
  38. package/src/area.js +131 -0
  39. package/src/charts/marks.js +29 -2
  40. package/src/geometry/Geometry.js +16 -1
  41. package/src/index.js +1 -0
  42. package/src/material/Material.js +9 -2
  43. package/src/material/Texture.js +55 -2
  44. package/src/particles/Emitter.js +121 -10
  45. package/src/scene/drawables.js +68 -4
  46. package/src/watch.js +8 -1
  47. package/src/webgl/WebGLRenderer.js +36 -5
  48. package/src/webgl/picking.js +22 -1
  49. package/src/webgl/resources.js +168 -14
  50. package/src/webgl/shaders.js +110 -11
@@ -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)
@@ -415,7 +461,7 @@ export class Resources {
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
+ }
@@ -12,12 +12,18 @@ export const ATTRIBUTES = {
12
12
  instanceColor: 8,
13
13
  tangent: 9, // brushed metal's direction: along the UVs' u, and which way their v runs
14
14
  uv2: 10, // a second UV set, for baked occlusion and light maps
15
+ // points drawn as squares (sprites): each one's size, opacity and angle, and which point it is
16
+ pointSize: 11,
17
+ pointOpacity: 12,
18
+ pointAngle: 13,
19
+ pointIndex: 14,
15
20
  // lines: per-segment endpoints and colours, and the corner of the quad being drawn
16
21
  segmentStart: 0,
17
22
  segmentEnd: 1,
18
23
  segmentColorStart: 2,
19
24
  segmentColorEnd: 3,
20
25
  corner: 4,
26
+ segmentInfo: 5, // how far along its chain the segment starts, and whether it starts or ends one
21
27
  }
22
28
 
23
29
  // a section plane: fragments on its far side are cut away
@@ -957,6 +963,17 @@ void main() {
957
963
  export const POINTS_VERTEX = /* glsl */ `
958
964
  layout(location = 0) in vec3 position;
959
965
  layout(location = 3) in vec3 color;
966
+ #ifdef SPRITES
967
+ // each point a square: one instance a point, drawn as a strip over the corners (along 0-1, across -1-1)
968
+ layout(location = 4) in vec2 corner;
969
+ layout(location = 11) in float pointSize;
970
+ layout(location = 12) in float pointOpacity;
971
+ layout(location = 13) in float pointAngle;
972
+ layout(location = 14) in float pointIndex;
973
+ uniform vec2 uResolution; // drawing buffer size in pixels
974
+ out vec2 vUv;
975
+ out float vOpacity;
976
+ #endif
960
977
 
961
978
  uniform mat4 uModel;
962
979
  uniform mat4 uView;
@@ -978,11 +995,34 @@ ${COMMON}
978
995
 
979
996
  void main() {
980
997
  vec4 viewPosition = uView * (uModel * vec4(position, 1.0));
998
+ #ifdef SPRITES
999
+ // the square's corner, turned by the point's angle: the texture turns with it
1000
+ vec2 c = vec2(corner.x * 2.0 - 1.0, corner.y);
1001
+ float s = sin(pointAngle), k = cos(pointAngle);
1002
+ vec2 turned = vec2(c.x * k - c.y * s, c.x * s + c.y * k);
1003
+ float size = uSize * pointSize;
1004
+ #ifdef SIZE_ATTENUATION
1005
+ // world units: the square sits in the view, facing the camera
1006
+ viewPosition.xy += turned * size * 0.5;
1007
+ gl_Position = uProjection * viewPosition;
1008
+ #else
1009
+ // pixels: the square is laid out on the screen
981
1010
  gl_Position = uProjection * viewPosition;
1011
+ gl_Position.xy += turned * size / uResolution * gl_Position.w;
1012
+ #endif
1013
+ // the image upright on the square, its top at the top, as a point's is
1014
+ vUv = vec2(c.x * 0.5 + 0.5, 0.5 - c.y * 0.5);
1015
+ vOpacity = pointOpacity;
1016
+ int self = int(pointIndex + 0.5);
1017
+ #else
1018
+ gl_Position = uProjection * viewPosition;
1019
+ int self = gl_VertexID;
1020
+ #endif
982
1021
  #ifdef FOG
983
1022
  vFogDepth = -viewPosition.z;
984
1023
  #endif
985
- #ifdef SIZE_ATTENUATION
1024
+ #ifdef SPRITES
1025
+ #elif defined(SIZE_ATTENUATION)
986
1026
  // perspective shrinks with distance; an orthographic projection (w row 0, 0, 0, 1) doesn't
987
1027
  float depth = uProjection[3][3] == 1.0 ? 1.0 : max(-viewPosition.z, 1e-4);
988
1028
  gl_PointSize = uSize * uScale / depth;
@@ -993,9 +1033,9 @@ void main() {
993
1033
  #ifdef VERTEX_COLORS
994
1034
  vColor = srgbToLinear(color);
995
1035
  #endif
996
- vHighlight = uHighlightIndex == -1 || gl_VertexID == uHighlightIndex ? 1.0 : 0.0;
1036
+ vHighlight = uHighlightIndex == -1 || self == uHighlightIndex ? 1.0 : 0.0;
997
1037
  #ifdef PICK
998
- vPickElement = uint(gl_VertexID);
1038
+ vPickElement = uint(self);
999
1039
  #endif
1000
1040
  }
1001
1041
  `
@@ -1004,6 +1044,10 @@ const POINTS_FRAGMENT = /* glsl */ `
1004
1044
  precision highp float;
1005
1045
  in vec3 vColor;
1006
1046
  flat in float vHighlight;
1047
+ #ifdef SPRITES
1048
+ in vec2 vUv;
1049
+ in float vOpacity;
1050
+ #endif
1007
1051
  uniform vec4 uHighlight;
1008
1052
  uniform vec3 uColor;
1009
1053
  uniform float uOpacity;
@@ -1016,12 +1060,18 @@ ${COMMON}
1016
1060
  ${FOG}
1017
1061
 
1018
1062
  void main() {
1063
+ #ifdef SPRITES
1064
+ vec2 at = vUv;
1065
+ vec4 base = vec4(uColor * vColor, uOpacity * vOpacity);
1066
+ #else
1067
+ vec2 at = gl_PointCoord;
1019
1068
  vec4 base = vec4(uColor * vColor, uOpacity);
1069
+ #endif
1020
1070
  #ifdef MAP
1021
- base *= texture(uMap, vec2(gl_PointCoord.x, gl_PointCoord.y));
1071
+ base *= texture(uMap, at);
1022
1072
  #else
1023
1073
  // round points with a one pixel soft edge
1024
- vec2 p = gl_PointCoord * 2.0 - 1.0;
1074
+ vec2 p = at * 2.0 - 1.0;
1025
1075
  float r = dot(p, p);
1026
1076
  float edge = fwidth(r);
1027
1077
  base.a *= 1.0 - smoothstep(1.0 - edge, 1.0, r);
@@ -1037,7 +1087,9 @@ layout(location = 0) in vec3 segmentStart;
1037
1087
  layout(location = 1) in vec3 segmentEnd;
1038
1088
  layout(location = 2) in vec3 segmentColorStart;
1039
1089
  layout(location = 3) in vec3 segmentColorEnd;
1040
- layout(location = 4) in vec2 corner; // x: 0 at start, 1 at end; y: -1 or 1 across
1090
+ // the distance along its chain (segments joined end to start) where it starts, in the model's
1091
+ // units; 1 if it starts a chain; 1 if it ends one
1092
+ layout(location = 5) in vec3 segmentInfo;
1041
1093
 
1042
1094
  uniform mat4 uModel;
1043
1095
  uniform mat4 uView;
@@ -1045,8 +1097,18 @@ uniform mat4 uProjection;
1045
1097
  uniform vec2 uResolution; // drawing buffer size in pixels
1046
1098
  uniform float uWidth; // pixels, already scaled by the pixel ratio
1047
1099
  uniform float uNear;
1100
+ uniform int uArrows; // arrowheads: 1 at the start of each chain, 2 at its end, 3 at both
1101
+ uniform float uArrowSize; // their length in pixels, scaled by the pixel ratio
1102
+
1103
+ // six vertices an instance make the segment's quad, as two triangles - x: 0 at its start, 1 at its
1104
+ // end; y: -1 or 1 across - and six more its two arrowheads, drawn only when asked for
1105
+ const vec2 QUAD[6] = vec2[6](vec2(0.0, -1.0), vec2(0.0, 1.0), vec2(1.0, -1.0), vec2(1.0, -1.0), vec2(0.0, 1.0), vec2(1.0, 1.0));
1048
1106
 
1049
1107
  out vec3 vColor;
1108
+ #ifdef DASHED
1109
+ out float vDistance; // along the chain, in the model's units
1110
+ flat out float vArrow; // 1 on an arrowhead, which is never dashed
1111
+ #endif
1050
1112
  #ifdef PICK
1051
1113
  flat out uint vPickElement; // which instance, point or segment, for GPU picking
1052
1114
  #endif
@@ -1077,21 +1139,48 @@ void main() {
1077
1139
  dir = len > 0.0 ? dir / len : vec2(1.0, 0.0);
1078
1140
  vec2 normal = vec2(-dir.y, dir.x);
1079
1141
 
1080
- vec4 clip = mix(ca, cb, corner.x);
1142
+ bool arrowStart = (uArrows & 1) != 0 && segmentInfo.y > 0.5;
1143
+ bool arrowEnd = (uArrows & 2) != 0 && segmentInfo.z > 0.5;
1144
+ float along, arrow = 0.0;
1145
+ vec2 offset;
1146
+ if (gl_VertexID < 6) {
1147
+ vec2 c = QUAD[gl_VertexID];
1148
+ along = c.x;
1149
+ // past each end by half the width so joins look continuous - or short of it, under an arrowhead
1150
+ float startOut = arrowStart ? -uArrowSize * 0.8 : uWidth * 0.5;
1151
+ float endOut = arrowEnd ? -uArrowSize * 0.8 : uWidth * 0.5;
1152
+ offset = normal * c.y * uWidth * 0.5 + dir * (along > 0.5 ? endOut : -startOut);
1153
+ } else {
1154
+ // an arrowhead: its tip on the chain's end, pointing out along the segment
1155
+ int which = (gl_VertexID - 6) / 3, k = gl_VertexID - 6 - which * 3;
1156
+ if (which == 0 ? !arrowStart : !arrowEnd) {
1157
+ gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
1158
+ return;
1159
+ }
1160
+ along = float(which);
1161
+ vec2 d = which == 1 ? dir : -dir;
1162
+ float wing = uArrowSize * 0.45;
1163
+ offset = k == 0 ? vec2(0.0) : -d * uArrowSize + normal * (k == 1 ? wing : -wing);
1164
+ arrow = 1.0;
1165
+ }
1166
+
1167
+ vec4 clip = mix(ca, cb, along);
1081
1168
  #ifdef FOG
1082
- vFogDepth = -mix(a.z, b.z, corner.x);
1169
+ vFogDepth = -mix(a.z, b.z, along);
1083
1170
  #endif
1084
- // extend past each end by half the width so joins look continuous
1085
- vec2 offset = normal * corner.y * uWidth * 0.5 + dir * (corner.x * 2.0 - 1.0) * uWidth * 0.5;
1086
1171
  clip.xy += offset / (uResolution * 0.5) * clip.w;
1087
1172
  gl_Position = clip;
1173
+ #ifdef DASHED
1174
+ vDistance = segmentInfo.x + length(segmentEnd - segmentStart) * along;
1175
+ vArrow = arrow;
1176
+ #endif
1088
1177
  #ifdef PICK
1089
1178
  vPickElement = uint(gl_InstanceID);
1090
1179
  #endif
1091
1180
 
1092
1181
  vColor = vec3(1.0);
1093
1182
  #ifdef VERTEX_COLORS
1094
- vColor = srgbToLinear(mix(segmentColorStart, segmentColorEnd, corner.x));
1183
+ vColor = srgbToLinear(mix(segmentColorStart, segmentColorEnd, along));
1095
1184
  #endif
1096
1185
  }
1097
1186
  `
@@ -1101,12 +1190,22 @@ precision highp float;
1101
1190
  in vec3 vColor;
1102
1191
  uniform vec3 uColor;
1103
1192
  uniform float uOpacity;
1193
+ #ifdef DASHED
1194
+ in float vDistance;
1195
+ flat in float vArrow;
1196
+ uniform float uDash; // dash and gap lengths, in the model's units
1197
+ uniform float uGap;
1198
+ uniform float uDashOffset; // moves the dashes along: animate it for flow
1199
+ #endif
1104
1200
  out vec4 fragColor;
1105
1201
 
1106
1202
  ${COMMON}
1107
1203
  ${FOG}
1108
1204
 
1109
1205
  void main() {
1206
+ #ifdef DASHED
1207
+ if (vArrow < 0.5 && mod(vDistance - uDashOffset, uDash + uGap) >= uDash) discard;
1208
+ #endif
1110
1209
  vec3 color = fogged(uColor * vColor);
1111
1210
  fragColor = vec4(linearToSrgb(color) * uOpacity, uOpacity);
1112
1211
  }