@displayxr/inline3d 1.43.1 → 1.45.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/CHANGELOG.md +129 -0
- package/THIRD_PARTY_NOTICES.md +26 -2
- package/camera.d.ts +7 -0
- package/cursor-depth.d.ts +275 -1
- package/dist/call.js +18 -4
- package/js/camera/pair-cursor.js +129 -26
- package/js/inline3d-cursor-bvh.js +147 -0
- package/js/inline3d-cursor-depth-playcanvas.js +507 -61
- package/js/inline3d-cursor-depth.js +497 -43
- package/js/inline3d-cursor-option.js +140 -6
- package/js/inline3d-cursor-perf.js +199 -0
- package/js/inline3d-cursor-shape.js +962 -0
- package/js/inline3d-model.js +30 -1
- package/js/inline3d-playcanvas-engine.js +2 -0
- package/js/inline3d-splat-layer-rig.js +3 -0
- package/js/inline3d-three.js +263 -30
- package/js/vendor/three-mesh-bvh.js +3 -0
- package/model.d.ts +1 -1
- package/package.json +7 -1
- package/splat.d.ts +1 -1
- package/three.d.ts +43 -4
|
@@ -33,12 +33,14 @@
|
|
|
33
33
|
// on a head-tracked display reads as "at the glass";
|
|
34
34
|
// 'world' — C = S - f (1 - t) eyeToCanvas, straight in front of the pointer's canvas point:
|
|
35
35
|
// parallaxes like content, drifts ~mm off the pointer from an off-axis viewer;
|
|
36
|
-
// 'hybrid' (default) — 'screen' while the pointer moves (exact aim), and
|
|
37
|
-
// is still (correct parallax): the line of sight at the last pointer move
|
|
38
|
-
// is kept,
|
|
36
|
+
// 'hybrid' (default) — 'screen' while the pointer moves (exact aim), and DISPLAY-fixed while
|
|
37
|
+
// it is still (correct parallax): the line of sight at the last pointer move
|
|
38
|
+
// (E0 → S0) is kept, IN THE DISPLAY'S FRAME (relative to the canvas, in canvas
|
|
39
|
+
// heights), and the sprite sits on it at the current depth — the point whose
|
|
39
40
|
// distance in front of the glass is (1 - t) eyeToCanvas. A head move leaves it
|
|
40
41
|
// put; a depth change slides it along that line, so it stays on the click point
|
|
41
|
-
// for the viewer who placed it
|
|
42
|
+
// for the viewer who placed it; a rig move (orbit, pan, zoom) carries it with the
|
|
43
|
+
// canvas, so it stays on the pointer. Continuous at the stop frame.
|
|
42
44
|
// The height is scaled by t either way, so the apparent size is constant.
|
|
43
45
|
//
|
|
44
46
|
// THE POLICY (ADR-046 Amendment 2, the same for every DisplayXR depth cursor — this module, ./three's
|
|
@@ -51,11 +53,61 @@
|
|
|
51
53
|
// CURSOR_DRAW_THRESHOLD (-0.005, ~0.3 mm) or under CURSOR_DRAW_MIN_PX on the panel
|
|
52
54
|
// (cursorShouldDraw), and the CSS cursor is left alone;
|
|
53
55
|
// - the look: the classic arrow (CURSOR_ARROW, tip = hotspot), at the OS cursor's size
|
|
54
|
-
// (CURSOR_ARROW_CSS_PX tall on the page, cursorDefaultHeight)
|
|
56
|
+
// (CURSOR_ARROW_CSS_PX tall on the page, cursorDefaultHeight) — or, mirroring the page's CSS
|
|
57
|
+
// cursor: every CSS keyword and url() images (./inline3d-cursor-shape.js);
|
|
58
|
+
// - crosstalk softening (Amendment 2 §B.7): a LIFTED arrow is blurred by k × its ghost offset and
|
|
59
|
+
// faded toward alphaMin with its lift, both exactly zero on the glass (cursorSoften and below).
|
|
55
60
|
|
|
56
61
|
import { CURSOR_ANCHOR_MODES } from './inline3d-cursor-option.js';
|
|
57
62
|
|
|
58
|
-
export {
|
|
63
|
+
export {
|
|
64
|
+
CURSOR_ANCHOR_MODES,
|
|
65
|
+
CURSOR_OPTION_KEYS,
|
|
66
|
+
CURSOR_SOFTEN_DEFAULTS,
|
|
67
|
+
CURSOR_SOFTEN_KEYS,
|
|
68
|
+
CURSOR_SOFTEN_PRESETS,
|
|
69
|
+
parseCursorSoften,
|
|
70
|
+
resolveCursorOption,
|
|
71
|
+
resolveCursorSoften,
|
|
72
|
+
} from './inline3d-cursor-option.js';
|
|
73
|
+
import { CURSOR_SOFTEN_DEFAULTS } from './inline3d-cursor-option.js';
|
|
74
|
+
import { CURSOR_HIDDEN_ATTR, ensureCursorHiddenRule } from './inline3d-cursor-shape.js';
|
|
75
|
+
|
|
76
|
+
// Which sprite: the page's CSS cursor mirrored (shapes, read-back) — its own module, re-exported.
|
|
77
|
+
export {
|
|
78
|
+
CURSOR_CSS_KEYWORDS,
|
|
79
|
+
CURSOR_CSS_READ_MS,
|
|
80
|
+
CURSOR_HIDDEN_ATTR,
|
|
81
|
+
CURSOR_HIDDEN_RULE,
|
|
82
|
+
CURSOR_IMAGE_MAX_PX,
|
|
83
|
+
CURSOR_SHAPES,
|
|
84
|
+
CURSOR_SHAPE_OPTIONS,
|
|
85
|
+
CURSOR_SHAPE_TEXTURE_PX,
|
|
86
|
+
CursorCssReader,
|
|
87
|
+
CursorImageCache,
|
|
88
|
+
CursorShapeSelector,
|
|
89
|
+
cssCursorToShape,
|
|
90
|
+
cssKeywordToShape,
|
|
91
|
+
curFileHotspot,
|
|
92
|
+
cursorImageCache,
|
|
93
|
+
cursorPickShape,
|
|
94
|
+
cursorShape,
|
|
95
|
+
cursorShapeBox,
|
|
96
|
+
cursorShapePolys,
|
|
97
|
+
cursorShapeRaster,
|
|
98
|
+
cursorShapeSample,
|
|
99
|
+
cursorShapeTexture,
|
|
100
|
+
cursorSoftenPixels,
|
|
101
|
+
cursorSpriteBox,
|
|
102
|
+
cursorSpriteGLSL,
|
|
103
|
+
cursorSpriteRect,
|
|
104
|
+
drawCursorShape2D,
|
|
105
|
+
ensureCursorHiddenRule,
|
|
106
|
+
parseCssCursor,
|
|
107
|
+
readPageCursor,
|
|
108
|
+
resolveCursorSprite,
|
|
109
|
+
spriteForName,
|
|
110
|
+
} from './inline3d-cursor-shape.js';
|
|
59
111
|
|
|
60
112
|
/**
|
|
61
113
|
* Fallback sprite height, as a fraction of the canvas height, when the canvas's on-page size is
|
|
@@ -183,7 +235,9 @@ export function solveCursorGeometry(viewA, viewB, u, v) {
|
|
|
183
235
|
const canvasHeight = dot(sub(canvasPoint, p1), forward) * (2 / viewA.projectionMatrix[5]);
|
|
184
236
|
if (!(eyeToCanvas > 0) || !(canvasHeight > 0) || !Number.isFinite(canvasHeight)) return null;
|
|
185
237
|
|
|
186
|
-
|
|
238
|
+
// Canvas aspect (width / height) from the same frustum: (2 / a) / (2 / b).
|
|
239
|
+
const aspect = viewA.projectionMatrix[5] / viewA.projectionMatrix[0];
|
|
240
|
+
return { eye, canvasPoint, forward, eyeToCanvas, canvasHeight, aspect, basis: orthonormalBasis(ma) };
|
|
187
241
|
}
|
|
188
242
|
|
|
189
243
|
/** The view's rotation as unit X/Y/Z columns (a transform with parent scale still works). */
|
|
@@ -249,6 +303,8 @@ export function placeCursor(g, disparity, heightFraction) {
|
|
|
249
303
|
|
|
250
304
|
/** A pointer moving less than this (canvas-normalised) between frames counts as still. */
|
|
251
305
|
const POINTER_STILL_EPS = 1e-4;
|
|
306
|
+
/** The runtime's U_CURSOR_DEPTH_ANCHOR_PLANE_EPS: a canvas whose shape moves more than this re-anchors. */
|
|
307
|
+
const CURSOR_ANCHOR_PLANE_EPS = 1e-3;
|
|
252
308
|
|
|
253
309
|
/**
|
|
254
310
|
* The whole pipeline with its state: one per cursor.
|
|
@@ -313,15 +369,42 @@ export class CursorDepthPlacer {
|
|
|
313
369
|
position = addScaled(g.canvasPoint, fw, out);
|
|
314
370
|
} else if (this.anchor === 'hybrid') {
|
|
315
371
|
const last = this._last;
|
|
316
|
-
|
|
372
|
+
let moved =
|
|
317
373
|
reprimed || !this._line || !last || Math.abs(hint.u - last[0]) > POINTER_STILL_EPS || Math.abs(hint.v - last[1]) > POINTER_STILL_EPS;
|
|
374
|
+
// The kept line lives in the DISPLAY's frame, never the scene's: origin S (the canvas point
|
|
375
|
+
// under the pointer — the same canvas point while the pointer is still), axes the canvas's
|
|
376
|
+
// (g.basis), unit the canvas height. A rig that orbits, pans or zooms the scene (./model's
|
|
377
|
+
// idle spin, damped drag, a scripted camera) moves the views and the canvas together, so in
|
|
378
|
+
// this frame nothing changed and the sprite stays on the pointer; a head move changes the
|
|
379
|
+
// eye's position relative to the canvas, so it still parallaxes like content. (The runtime's
|
|
380
|
+
// u_cursor_depth gets this for free: it works in display space.)
|
|
381
|
+
let k = 0;
|
|
382
|
+
let L = this._line;
|
|
383
|
+
if (!moved) {
|
|
384
|
+
// #1951's rule: the line means something only on the canvas it was taken against. In the
|
|
385
|
+
// display frame a rig move is not a change; a canvas whose SHAPE changed (resize) is.
|
|
386
|
+
moved = !(Math.abs(g.aspect / L.aspect - 1) <= CURSOR_ANCHOR_PLANE_EPS);
|
|
387
|
+
}
|
|
388
|
+
if (!moved) {
|
|
389
|
+
// On the kept line E0 → S0, the point (1 - t)·eyeToCanvas in front of the glass, all in
|
|
390
|
+
// canvas heights: s = 1 - lift / D0, C = S0 + (1 - s)(E0 - S0) = S0 + k E0.
|
|
391
|
+
const lift = ((1 - t) * g.eyeToCanvas) / g.canvasHeight;
|
|
392
|
+
k = lift / L.D;
|
|
393
|
+
// s <= 0 (the head moved back past E0): never place it at or behind the anchor eye.
|
|
394
|
+
if (!(k < 1) || !Number.isFinite(k)) moved = true;
|
|
395
|
+
}
|
|
318
396
|
if (moved) {
|
|
319
|
-
|
|
397
|
+
const e = sub(g.eye, g.canvasPoint);
|
|
398
|
+
const h = g.canvasHeight;
|
|
399
|
+
const B = g.basis;
|
|
400
|
+
L = this._line = { E: [dot(e, B.x) / h, dot(e, B.y) / h, dot(e, B.z) / h], D: g.eyeToCanvas / h, aspect: g.aspect };
|
|
320
401
|
} else {
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
position = addScaled(
|
|
402
|
+
const B = g.basis;
|
|
403
|
+
const h = g.canvasHeight * k;
|
|
404
|
+
position = g.canvasPoint;
|
|
405
|
+
position = addScaled(position, B.x, L.E[0] * h);
|
|
406
|
+
position = addScaled(position, B.y, L.E[1] * h);
|
|
407
|
+
position = addScaled(position, B.z, L.E[2] * h);
|
|
325
408
|
anchored = true;
|
|
326
409
|
}
|
|
327
410
|
}
|
|
@@ -348,33 +431,42 @@ function asView(v) {
|
|
|
348
431
|
export class CursorPointer {
|
|
349
432
|
/**
|
|
350
433
|
* @param {HTMLElement} canvas
|
|
351
|
-
* @param {{scope?: 'canvas'|'window'}} [opts] 'canvas' (default): the pointer counts only while
|
|
434
|
+
* @param {{scope?: 'canvas'|'window', keepCss?: boolean}} [opts] 'canvas' (default): the pointer counts only while
|
|
352
435
|
* it is over the canvas element itself. 'window': while it is inside the canvas's BOX,
|
|
353
436
|
* even over DOM layered on top of it (overlay buttons, labels) — and the CSS cursor is then
|
|
354
437
|
* hidden page-wide while the sprite shows, since the overlay has its own cursor.
|
|
438
|
+
* `keepCss`: never hide the CSS cursor (`cursor: { debug: 'both' }`, the latency check
|
|
439
|
+
* that films the sprite next to the OS cursor).
|
|
355
440
|
*/
|
|
356
|
-
constructor(canvas, { scope = 'canvas' } = {}) {
|
|
441
|
+
constructor(canvas, { scope = 'canvas', keepCss = false } = {}) {
|
|
357
442
|
if (scope !== 'canvas' && scope !== 'window') {
|
|
358
443
|
throw new Error(`@displayxr/inline3d: cursor pointerScope "${scope}" — expected 'canvas' or 'window'.`);
|
|
359
444
|
}
|
|
360
445
|
this.canvas = canvas;
|
|
361
446
|
this.scope = scope;
|
|
362
|
-
|
|
363
|
-
this.
|
|
447
|
+
this.keepCss = !!keepCss;
|
|
448
|
+
this._uv = null;
|
|
449
|
+
/** Time (event timeStamp, ms) of the newest pointer sample; 0 before the first. */
|
|
450
|
+
this.sampleTime = 0;
|
|
451
|
+
/** Pointer samples received so far (diagnostics). */
|
|
452
|
+
this.samples = 0;
|
|
364
453
|
this._hidden = false;
|
|
365
|
-
this.
|
|
366
|
-
this.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const u = (e.clientX - r.left) / r.width;
|
|
370
|
-
const v = (e.clientY - r.top) / r.height;
|
|
371
|
-
this.uv = u >= 0 && u <= 1 && v >= 0 && v <= 1 ? [u, v] : null;
|
|
372
|
-
};
|
|
454
|
+
this._cx = 0;
|
|
455
|
+
this._cy = 0;
|
|
456
|
+
this._pending = false; // a sample not yet resolved to uv (resolved once, on the next read)
|
|
457
|
+
this._onMove = (e) => this._sample(e);
|
|
373
458
|
this._onLeave = () => {
|
|
374
459
|
this.uv = null;
|
|
375
460
|
};
|
|
376
461
|
this._target = scope === 'window' ? globalThis : canvas;
|
|
462
|
+
// Freshest pointer: `pointerrawupdate` (Chromium, secure contexts) fires as each input arrives,
|
|
463
|
+
// not once per frame, so the frame reads the newest position instead of one up to a frame old.
|
|
464
|
+
// `pointermove` stays as the fallback (and for synthetic events) — a sample older than one
|
|
465
|
+
// already held is ignored, so the two never fight.
|
|
466
|
+
/** Which event feeds the position: 'pointerrawupdate' where supported, else 'pointermove'. */
|
|
467
|
+
this.source = supportsRawUpdate(this._target) ? 'pointerrawupdate' : 'pointermove';
|
|
377
468
|
this._target.addEventListener('pointermove', this._onMove);
|
|
469
|
+
if (this.source === 'pointerrawupdate') this._target.addEventListener('pointerrawupdate', this._onMove);
|
|
378
470
|
if (scope === 'window') {
|
|
379
471
|
// Leaving the window (relatedTarget null) or losing focus mid-hover: nothing to hover.
|
|
380
472
|
this._onOut = (e) => {
|
|
@@ -387,6 +479,41 @@ export class CursorPointer {
|
|
|
387
479
|
}
|
|
388
480
|
}
|
|
389
481
|
|
|
482
|
+
/** `[u, v]` while the pointer is over the canvas, else null. */
|
|
483
|
+
get uv() {
|
|
484
|
+
if (this._pending) {
|
|
485
|
+
this._pending = false;
|
|
486
|
+
const r = this.canvas.getBoundingClientRect();
|
|
487
|
+
if (!(r.width > 0 && r.height > 0)) {
|
|
488
|
+
this._uv = null;
|
|
489
|
+
} else {
|
|
490
|
+
const u = (this._cx - r.left) / r.width;
|
|
491
|
+
const v = (this._cy - r.top) / r.height;
|
|
492
|
+
this._uv = u >= 0 && u <= 1 && v >= 0 && v <= 1 ? [u, v] : null;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return this._uv;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
set uv(x) {
|
|
499
|
+
this._pending = false;
|
|
500
|
+
this._uv = x;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// One pointer event: keep its NEWEST coalesced sample (getCoalescedEvents(), last entry), unless
|
|
504
|
+
// a newer one is already held. No layout read here — uv resolves on the next read, once a frame.
|
|
505
|
+
_sample(e) {
|
|
506
|
+
const list = typeof e.getCoalescedEvents === 'function' ? e.getCoalescedEvents() : null;
|
|
507
|
+
const s = list && list.length ? list[list.length - 1] : e;
|
|
508
|
+
const t = s.timeStamp > 0 ? s.timeStamp : e.timeStamp > 0 ? e.timeStamp : globalThis.performance ? globalThis.performance.now() : Date.now();
|
|
509
|
+
if (t < this.sampleTime) return;
|
|
510
|
+
this.samples++;
|
|
511
|
+
this.sampleTime = t;
|
|
512
|
+
this._cx = s.clientX;
|
|
513
|
+
this._cy = s.clientY;
|
|
514
|
+
this._pending = true;
|
|
515
|
+
}
|
|
516
|
+
|
|
390
517
|
/** Set the pointer directly (canvas-normalised, v down), or null — for scripted input. */
|
|
391
518
|
set(u, v) {
|
|
392
519
|
this.uv = u === null || u === undefined ? null : [u, v];
|
|
@@ -409,28 +536,40 @@ export class CursorPointer {
|
|
|
409
536
|
return this.cssHeight() * (globalThis.devicePixelRatio > 0 ? globalThis.devicePixelRatio : 1);
|
|
410
537
|
}
|
|
411
538
|
|
|
539
|
+
/** The canvas's `[width, height]` in panel (device) pixels, `[0, 0]` when unknown. */
|
|
540
|
+
panelSize() {
|
|
541
|
+
const r = this.canvas.getBoundingClientRect();
|
|
542
|
+
const k = globalThis.devicePixelRatio > 0 ? globalThis.devicePixelRatio : 1;
|
|
543
|
+
return r.width > 0 && r.height > 0 ? [r.width * k, r.height * k] : [0, 0];
|
|
544
|
+
}
|
|
545
|
+
|
|
412
546
|
/**
|
|
413
547
|
* Hide (true) or restore (false) the CSS cursor; idempotent. Only ever while a sprite replaces
|
|
414
|
-
* it
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
548
|
+
* it. The hide is a data attribute (CURSOR_HIDDEN_ATTR) on the canvas — on <html> for
|
|
549
|
+
* pointerScope 'window' — plus one injected `cursor: none !important` rule for it: the page's
|
|
550
|
+
* own cursor (`pointer`, `grab`, a `url(…)` image, inline or from a stylesheet) is never
|
|
551
|
+
* written, so it is what shows the rest of the time, and what the sprite mirrors (CursorCssReader
|
|
552
|
+
* lifts the attribute for its read).
|
|
418
553
|
*/
|
|
419
554
|
hideCss(hide) {
|
|
555
|
+
if (this.keepCss) hide = false;
|
|
420
556
|
if (hide === this._hidden) return;
|
|
421
557
|
if (this.scope === 'window') {
|
|
422
558
|
pageCursorHidden(hide);
|
|
423
|
-
} else if (
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
559
|
+
} else if (this.canvas.setAttribute) {
|
|
560
|
+
if (hide) {
|
|
561
|
+
ensureCursorHiddenRule();
|
|
562
|
+
this.canvas.setAttribute(CURSOR_HIDDEN_ATTR, '');
|
|
563
|
+
} else {
|
|
564
|
+
this.canvas.removeAttribute(CURSOR_HIDDEN_ATTR);
|
|
565
|
+
}
|
|
428
566
|
}
|
|
429
567
|
this._hidden = hide;
|
|
430
568
|
}
|
|
431
569
|
|
|
432
570
|
dispose() {
|
|
433
571
|
this._target.removeEventListener('pointermove', this._onMove);
|
|
572
|
+
if (this.source === 'pointerrawupdate') this._target.removeEventListener('pointerrawupdate', this._onMove);
|
|
434
573
|
if (this.scope === 'window') {
|
|
435
574
|
globalThis.removeEventListener('pointerout', this._onOut);
|
|
436
575
|
globalThis.removeEventListener('blur', this._onLeave);
|
|
@@ -441,21 +580,23 @@ export class CursorPointer {
|
|
|
441
580
|
}
|
|
442
581
|
}
|
|
443
582
|
|
|
583
|
+
/** Does `target` dispatch `pointerrawupdate`? (Chromium; only in a secure context.) */
|
|
584
|
+
export function supportsRawUpdate(target) {
|
|
585
|
+
return !!target && 'onpointerrawupdate' in target && globalThis.isSecureContext !== false;
|
|
586
|
+
}
|
|
587
|
+
|
|
444
588
|
// pointerScope 'window': the overlay DOM has its own cursor, so hiding it on the canvas alone is
|
|
445
|
-
// not enough.
|
|
446
|
-
// shows; a count keeps two tiles from un-hiding each other.
|
|
589
|
+
// not enough. The attribute on <html> (the injected rule covers its descendants) hides it
|
|
590
|
+
// everywhere while any depth cursor shows; a count keeps two tiles from un-hiding each other.
|
|
447
591
|
let _pageHides = 0;
|
|
448
592
|
function pageCursorHidden(hide) {
|
|
449
593
|
const doc = globalThis.document;
|
|
450
594
|
if (!doc || !doc.documentElement) return;
|
|
451
|
-
if (hide
|
|
452
|
-
const st = doc.createElement('style');
|
|
453
|
-
st.id = 'inline3d-cursor-style';
|
|
454
|
-
st.textContent = 'html.inline3d-cursor-hidden, html.inline3d-cursor-hidden * { cursor: none !important; }';
|
|
455
|
-
(doc.head || doc.documentElement).appendChild(st);
|
|
456
|
-
}
|
|
595
|
+
if (hide) ensureCursorHiddenRule(doc);
|
|
457
596
|
_pageHides = Math.max(0, _pageHides + (hide ? 1 : -1));
|
|
458
|
-
doc.documentElement
|
|
597
|
+
const el = doc.documentElement;
|
|
598
|
+
if (_pageHides > 0) el.setAttribute?.(CURSOR_HIDDEN_ATTR, '');
|
|
599
|
+
else el.removeAttribute?.(CURSOR_HIDDEN_ATTR);
|
|
459
600
|
}
|
|
460
601
|
|
|
461
602
|
/**
|
|
@@ -592,3 +733,316 @@ export function cursorModelMatrix(placement, out = new Array(16)) {
|
|
|
592
733
|
out[12] = e[0]; out[13] = e[1]; out[14] = e[2]; out[15] = 1;
|
|
593
734
|
return out;
|
|
594
735
|
}
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
// ── Crosstalk softening of the lifted cursor (runtime ADR-046 Amendment 2 §B.7) ─────────────
|
|
739
|
+
//
|
|
740
|
+
// A port of the runtime's u_cursor_draw_soften_sized / u_cursor_draw_ghost_px and its 13-tap
|
|
741
|
+
// kernel (comp_metal_compositor.m `cursor_fragment_soft`, the same on every backend). The blur
|
|
742
|
+
// and the fade live in the CURSOR'S OWN material: a quad whose fragment shader evaluates the arrow
|
|
743
|
+
// analytically at the kernel's 13 taps (cursorSoftArrowGLSL), or, on a 2D pair, the same function
|
|
744
|
+
// rasterised in JS (cursorSoftArrowRaster). Never a CSS filter or CSS opacity. A frame whose
|
|
745
|
+
// softening is (0 px, 1.0) draws the crisp arrow mesh exactly as before.
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* One frame's softening (u_cursor_draw_soften_sized): the blur radius in panel px and the opacity,
|
|
749
|
+
* for a cursor drawn at filtered disparity `d` (eye baselines) whose neighbouring views sit
|
|
750
|
+
* `ghostPx` panel px apart. Blur = blurK × |ghost|, capped at blurMaxPx and — when `spritePx`, the
|
|
751
|
+
* drawn arrow's height in panel px, is known — at blurMaxFrac × spritePx. Opacity eases 1 → alphaMin
|
|
752
|
+
* by a smoothstep over |d| up to fullD. Both are (0, 1) on the glass; non-finite input = none.
|
|
753
|
+
* @param {number} d
|
|
754
|
+
* @param {number} ghostPx
|
|
755
|
+
* @param {number} [spritePx]
|
|
756
|
+
* @param {object} [params=CURSOR_SOFTEN_DEFAULTS] resolveCursorSoften's result.
|
|
757
|
+
* @returns {{blurPx: number, alpha: number}}
|
|
758
|
+
*/
|
|
759
|
+
export function cursorSoften(d, ghostPx, spritePx, params = CURSOR_SOFTEN_DEFAULTS) {
|
|
760
|
+
const s = { blurPx: 0, alpha: 1 };
|
|
761
|
+
const p = params;
|
|
762
|
+
if (!p) return s;
|
|
763
|
+
if (p.blur && Number.isFinite(ghostPx) && p.blurK > 0 && p.blurMaxPx > 0) {
|
|
764
|
+
let cap = p.blurMaxPx;
|
|
765
|
+
if (p.blurMaxFrac > 0 && Number.isFinite(spritePx) && spritePx > 0) {
|
|
766
|
+
const rel = p.blurMaxFrac * spritePx; // proportional to the drawn cursor
|
|
767
|
+
cap = rel < cap ? rel : cap;
|
|
768
|
+
}
|
|
769
|
+
const b = p.blurK * Math.abs(ghostPx);
|
|
770
|
+
s.blurPx = b < cap ? b : cap;
|
|
771
|
+
}
|
|
772
|
+
if (p.alpha && Number.isFinite(d) && p.fullD > 0) {
|
|
773
|
+
let x = Math.abs(d) / p.fullD;
|
|
774
|
+
x = x < 1 ? x : 1;
|
|
775
|
+
const ease = x * x * (3 - 2 * x); // smoothstep(0, fullD, |d|)
|
|
776
|
+
const amin = p.alphaMin < 0 ? 0 : p.alphaMin > 1 ? 1 : p.alphaMin;
|
|
777
|
+
s.alpha = 1 - (1 - amin) * ease;
|
|
778
|
+
}
|
|
779
|
+
return s;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* The ghost offset (u_cursor_draw_ghost_px): the largest distance, in panel px, between the
|
|
784
|
+
* hotspot's positions in two NEIGHBOURING views (i, i + 1) — what crosstalk leaks.
|
|
785
|
+
* @param {ArrayLike<ArrayLike<number>|null>} origins the hotspot per view, canvas-normalised [u, v]
|
|
786
|
+
* @param {number} pxW panel px that 0..1 of u spans (the canvas width)
|
|
787
|
+
* @param {number} pxH panel px that 0..1 of v spans
|
|
788
|
+
*/
|
|
789
|
+
export function cursorGhostPx(origins, pxW, pxH) {
|
|
790
|
+
if (!origins || !(pxW > 0) || !(pxH > 0)) return 0;
|
|
791
|
+
let ghost = 0;
|
|
792
|
+
for (let i = 0; i + 1 < origins.length; i++) {
|
|
793
|
+
const a = origins[i], b = origins[i + 1];
|
|
794
|
+
if (!a || !b) continue;
|
|
795
|
+
const g = Math.hypot((b[0] - a[0]) * pxW, (b[1] - a[1]) * pxH);
|
|
796
|
+
if (Number.isFinite(g) && g > ghost) ghost = g;
|
|
797
|
+
}
|
|
798
|
+
return ghost;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Where a world point lands on the canvas in one view: canvas-normalised [u, v] (v down), or null
|
|
803
|
+
* when it is not in front of the view. The view's transform may carry a parent scale.
|
|
804
|
+
*/
|
|
805
|
+
export function cursorProjectToCanvas(view, point) {
|
|
806
|
+
const v = asView(view);
|
|
807
|
+
const m = v.transformMatrix, p = v.projectionMatrix;
|
|
808
|
+
// Inverse of the transform's 3×3 (cofactors) applied to (point − origin).
|
|
809
|
+
const a = m[0], b = m[4], c = m[8], d = m[1], e = m[5], f = m[9], g = m[2], h = m[6], k = m[10];
|
|
810
|
+
const A = e * k - f * h, B = -(d * k - f * g), C = d * h - e * g;
|
|
811
|
+
const det = a * A + b * B + c * C;
|
|
812
|
+
if (!(Math.abs(det) > 1e-30)) return null;
|
|
813
|
+
const x = point[0] - m[12], y = point[1] - m[13], z = point[2] - m[14];
|
|
814
|
+
const lx = (A * x + (c * h - b * k) * y + (b * f - c * e) * z) / det;
|
|
815
|
+
const ly = (B * x + (a * k - c * g) * y + (c * d - a * f) * z) / det;
|
|
816
|
+
const lz = (C * x + (b * g - a * h) * y + (a * e - b * d) * z) / det;
|
|
817
|
+
const cx = p[0] * lx + p[4] * ly + p[8] * lz + p[12];
|
|
818
|
+
const cy = p[1] * lx + p[5] * ly + p[9] * lz + p[13];
|
|
819
|
+
const cw = p[3] * lx + p[7] * ly + p[11] * lz + p[15];
|
|
820
|
+
if (!(cw > 0)) return null;
|
|
821
|
+
const out = [(cx / cw + 1) / 2, (1 - cy / cw) / 2];
|
|
822
|
+
return Number.isFinite(out[0]) && Number.isFinite(out[1]) ? out : null;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* The softening for a placement (CursorDepthPlacer.update's result) drawn with `views` on a canvas
|
|
827
|
+
* `panelW` × `panelH` panel px, the arrow `heightFraction` of the canvas tall. The ghost offset is
|
|
828
|
+
* measured from where the placement's hotspot lands in each view — right for every rig and view
|
|
829
|
+
* count. Inactive placements are never softened.
|
|
830
|
+
* @returns {{blurPx: number, alpha: number, ghostPx: number, spritePx: number, radius: number, aa: number, soft: boolean}}
|
|
831
|
+
* `radius` / `aa`: the blur radius and one panel px in ARROW units (its height = 1), for the
|
|
832
|
+
* material; `soft`: false = draw the crisp arrow (bit-identical to the unsoftened draw).
|
|
833
|
+
*/
|
|
834
|
+
export function cursorPlacementSoften(placement, views, panelW, panelH, heightFraction, params = CURSOR_SOFTEN_DEFAULTS) {
|
|
835
|
+
const none = { blurPx: 0, alpha: 1, ghostPx: 0, spritePx: 0, radius: 0, aa: 0, soft: false };
|
|
836
|
+
if (!placement || !placement.active || !placement.position || !views) return none;
|
|
837
|
+
const origins = Array.from(views, (v) => cursorProjectToCanvas(v, placement.position));
|
|
838
|
+
const ghostPx = cursorGhostPx(origins, panelW, panelH);
|
|
839
|
+
const spritePx = heightFraction > 0 && panelH > 0 ? heightFraction * panelH : 0;
|
|
840
|
+
const s = cursorSoften(placement.disparity, ghostPx, spritePx, params);
|
|
841
|
+
const soft = s.blurPx > 0 || s.alpha < 1;
|
|
842
|
+
return {
|
|
843
|
+
blurPx: s.blurPx,
|
|
844
|
+
alpha: s.alpha,
|
|
845
|
+
ghostPx,
|
|
846
|
+
spritePx,
|
|
847
|
+
radius: spritePx > 0 ? s.blurPx / spritePx : 0,
|
|
848
|
+
aa: spritePx > 0 ? 1 / spritePx : 0,
|
|
849
|
+
soft,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* The runtime's blur kernel, in units of the radius: `[dx, dy, weight]` — the centre, 4 taps at
|
|
855
|
+
* r/2 and 8 at r (a Gaussian, sigma ≈ 0.6 r). Taps outside the arrow read as transparent.
|
|
856
|
+
*/
|
|
857
|
+
export const CURSOR_SOFTEN_KERNEL = Object.freeze([
|
|
858
|
+
Object.freeze([0, 0, 1]),
|
|
859
|
+
...[[0.5, 0], [-0.5, 0], [0, 0.5], [0, -0.5]].map(([x, y]) => Object.freeze([x, y, 0.7066])),
|
|
860
|
+
...Array.from({ length: 8 }, (_, k) => {
|
|
861
|
+
const a = 0.3927 + 0.7854 * k;
|
|
862
|
+
return Object.freeze([Math.cos(a), Math.sin(a), 0.2494]);
|
|
863
|
+
}),
|
|
864
|
+
]);
|
|
865
|
+
|
|
866
|
+
/** The arrow's extent in its own units (height = 1, x right, y down from the tip). */
|
|
867
|
+
export const CURSOR_ARROW_SIZE = Object.freeze([
|
|
868
|
+
Math.max(...CURSOR_ARROW.map((p) => p[0])),
|
|
869
|
+
Math.max(...CURSOR_ARROW.map((p) => p[1])),
|
|
870
|
+
]);
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* The softened arrow's quad in arrow units (y down, the tip = the hotspot at the origin): the
|
|
874
|
+
* arrow's box padded by the blur radius (plus one AA width) on every side, so the soft edge is not
|
|
875
|
+
* clipped. The hotspot does not move — the box only grows around it.
|
|
876
|
+
* @returns {[number, number, number, number]} [x0, y0, x1, y1]
|
|
877
|
+
*/
|
|
878
|
+
export function cursorSoftArrowBox(radius, aa = 0) {
|
|
879
|
+
const pad = (radius > 0 && Number.isFinite(radius) ? radius : 0) + (aa > 0 && Number.isFinite(aa) ? aa : 0);
|
|
880
|
+
return [-pad, -pad, CURSOR_ARROW_SIZE[0] + pad, CURSOR_ARROW_SIZE[1] + pad];
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/** Signed distance to a polygon (negative inside), any winding — the crossing-number form. */
|
|
884
|
+
export function cursorPolygonSd(poly, x, y) {
|
|
885
|
+
let d = (x - poly[0][0]) ** 2 + (y - poly[0][1]) ** 2;
|
|
886
|
+
let s = 1;
|
|
887
|
+
for (let i = 0, j = poly.length - 1; i < poly.length; j = i, i++) {
|
|
888
|
+
const vi = poly[i], vj = poly[j];
|
|
889
|
+
const ex = vj[0] - vi[0], ey = vj[1] - vi[1];
|
|
890
|
+
const wx = x - vi[0], wy = y - vi[1];
|
|
891
|
+
const t = Math.min(1, Math.max(0, (wx * ex + wy * ey) / (ex * ex + ey * ey)));
|
|
892
|
+
const bx = wx - ex * t, by = wy - ey * t;
|
|
893
|
+
d = Math.min(d, bx * bx + by * by);
|
|
894
|
+
const c1 = y >= vi[1], c2 = y < vj[1], c3 = ex * wy > ey * wx;
|
|
895
|
+
if ((c1 && c2 && c3) || (!c1 && !c2 && !c3)) s = -s;
|
|
896
|
+
}
|
|
897
|
+
return s * Math.sqrt(d);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const coverage = (sd, aa) => (aa > 0 ? Math.min(1, Math.max(0, 0.5 - sd / aa)) : sd < 0 ? 1 : 0);
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* The softened arrow at point (x, y) of its own units — the reference the shader mirrors: the
|
|
904
|
+
* kernel's 13 taps of the crisp arrow (dark outline, the fill over it, each edge anti-aliased over
|
|
905
|
+
* `aa`), accumulated PREMULTIPLIED, so clear taps add no colour. `radius` 0 = the centre tap only.
|
|
906
|
+
* @returns {[number, number, number, number]} straight RGB and alpha (before the opacity)
|
|
907
|
+
*/
|
|
908
|
+
export function cursorSoftArrowSample(x, y, radius, aa, fill = CURSOR_FILL_COLOR, outline = CURSOR_OUTLINE_COLOR, polys = [CURSOR_ARROW, CURSOR_ARROW_FILL]) {
|
|
909
|
+
let r = 0, g = 0, b = 0, a = 0, ws = 0;
|
|
910
|
+
const taps = radius > 0 ? CURSOR_SOFTEN_KERNEL : CURSOR_SOFTEN_KERNEL.slice(0, 1);
|
|
911
|
+
for (const [dx, dy, w] of taps) {
|
|
912
|
+
const px = x + dx * radius, py = y + dy * radius;
|
|
913
|
+
const co = coverage(cursorPolygonSd(polys[0], px, py), aa);
|
|
914
|
+
const cf = co > 0 ? coverage(cursorPolygonSd(polys[1], px, py), aa) : 0;
|
|
915
|
+
r += w * (outline[0] * co + (fill[0] - outline[0]) * cf);
|
|
916
|
+
g += w * (outline[1] * co + (fill[1] - outline[1]) * cf);
|
|
917
|
+
b += w * (outline[2] * co + (fill[2] - outline[2]) * cf);
|
|
918
|
+
a += w * co;
|
|
919
|
+
ws += w;
|
|
920
|
+
}
|
|
921
|
+
a /= ws;
|
|
922
|
+
if (!(a > 1e-5)) return [0, 0, 0, 0];
|
|
923
|
+
return [r / ws / a, g / ws / a, b / ws / a, a];
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const glf = (v) => {
|
|
927
|
+
const s = (+v).toPrecision(9);
|
|
928
|
+
return /[.eE]/.test(s) ? s : `${s}.0`;
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* GLSL (ES 1.00 and 3.00 alike: no arrays, no loops) for the softened arrow, generated from the
|
|
933
|
+
* same polygons and kernel as cursorSoftArrowSample:
|
|
934
|
+
*
|
|
935
|
+
* vec4 inline3dSoftArrow(vec2 p, float radius, float aa, vec3 fill, vec3 outline)
|
|
936
|
+
*
|
|
937
|
+
* `p` in arrow units (height 1, y down, tip at the origin). Returns straight RGB and alpha; the
|
|
938
|
+
* caller multiplies the alpha by the opacity.
|
|
939
|
+
*/
|
|
940
|
+
export function cursorSoftArrowGLSL(polys = [CURSOR_ARROW, CURSOR_ARROW_FILL]) {
|
|
941
|
+
const v2 = (q) => `vec2(${glf(q[0])}, ${glf(q[1])})`;
|
|
942
|
+
const sdFn = (name, poly) => {
|
|
943
|
+
const lines = [`float ${name}(vec2 p) {`, ` float d = dot(p - ${v2(poly[0])}, p - ${v2(poly[0])});`, ' float s = 1.0;'];
|
|
944
|
+
for (let i = 0, j = poly.length - 1; i < poly.length; j = i, i++) lines.push(` i3dCursorEdge(p, ${v2(poly[i])}, ${v2(poly[j])}, d, s);`);
|
|
945
|
+
lines.push(' return s * sqrt(d);', '}');
|
|
946
|
+
return lines.join('\n');
|
|
947
|
+
};
|
|
948
|
+
const taps = CURSOR_SOFTEN_KERNEL.slice(1)
|
|
949
|
+
.map(([dx, dy, w]) => ` acc += ${glf(w)} * i3dCursorTap(p + vec2(${glf(dx)}, ${glf(dy)}) * radius, aa, fill, outline);`)
|
|
950
|
+
.join('\n');
|
|
951
|
+
const wsum = CURSOR_SOFTEN_KERNEL.slice(1).reduce((t, k) => t + k[2], 0);
|
|
952
|
+
return `
|
|
953
|
+
void i3dCursorEdge(vec2 p, vec2 vi, vec2 vj, inout float d, inout float s) {
|
|
954
|
+
vec2 e = vj - vi;
|
|
955
|
+
vec2 w = p - vi;
|
|
956
|
+
vec2 b = w - e * clamp(dot(w, e) / dot(e, e), 0.0, 1.0);
|
|
957
|
+
d = min(d, dot(b, b));
|
|
958
|
+
bool c1 = p.y >= vi.y;
|
|
959
|
+
bool c2 = p.y < vj.y;
|
|
960
|
+
bool c3 = e.x * w.y > e.y * w.x;
|
|
961
|
+
if ((c1 && c2 && c3) || (!c1 && !c2 && !c3)) s = -s;
|
|
962
|
+
}
|
|
963
|
+
${sdFn('i3dCursorSdOutline', polys[0])}
|
|
964
|
+
${sdFn('i3dCursorSdFill', polys[1])}
|
|
965
|
+
float i3dCursorCov(float sd, float aa) {
|
|
966
|
+
return aa > 0.0 ? clamp(0.5 - sd / aa, 0.0, 1.0) : (sd < 0.0 ? 1.0 : 0.0);
|
|
967
|
+
}
|
|
968
|
+
vec4 i3dCursorTap(vec2 p, float aa, vec3 fill, vec3 outline) {
|
|
969
|
+
float co = i3dCursorCov(i3dCursorSdOutline(p), aa);
|
|
970
|
+
float cf = i3dCursorCov(i3dCursorSdFill(p), aa);
|
|
971
|
+
return vec4(outline * co + (fill - outline) * cf, co);
|
|
972
|
+
}
|
|
973
|
+
vec4 inline3dSoftArrow(vec2 p, float radius, float aa, vec3 fill, vec3 outline) {
|
|
974
|
+
vec4 acc = i3dCursorTap(p, aa, fill, outline);
|
|
975
|
+
float wsum = 1.0;
|
|
976
|
+
if (radius > 0.0) {
|
|
977
|
+
${taps}
|
|
978
|
+
wsum += ${glf(wsum)};
|
|
979
|
+
}
|
|
980
|
+
acc /= wsum;
|
|
981
|
+
return vec4(acc.a > 1e-5 ? acc.rgb / acc.a : vec3(0.0), acc.a);
|
|
982
|
+
}
|
|
983
|
+
`;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* The softened arrow rasterised for a 2D canvas (the stereo-pair cursor): pixel (i, j) of the
|
|
988
|
+
* result covers canvas px (x0 + i, y0 + j), sampled at its centre, with the arrow `sizePx` tall and
|
|
989
|
+
* its tip at canvas point (cx, cy). Straight-alpha RGBA bytes (ImageData layout), the opacity
|
|
990
|
+
* applied. One canvas px is the AA width.
|
|
991
|
+
* @returns {{x0: number, y0: number, width: number, height: number, data: Uint8ClampedArray}}
|
|
992
|
+
*/
|
|
993
|
+
export function cursorSoftArrowRaster(cx, cy, sizePx, radius, alpha, fill = CURSOR_FILL_COLOR, outline = CURSOR_OUTLINE_COLOR, polys = [CURSOR_ARROW, CURSOR_ARROW_FILL]) {
|
|
994
|
+
const size = sizePx > 0 && Number.isFinite(sizePx) ? sizePx : 1;
|
|
995
|
+
const aa = 1 / size;
|
|
996
|
+
const [bx0, by0, bx1, by1] = cursorSoftArrowBox(radius, aa);
|
|
997
|
+
const x0 = Math.floor(cx + bx0 * size), y0 = Math.floor(cy + by0 * size);
|
|
998
|
+
const width = Math.max(1, Math.ceil(cx + bx1 * size) - x0), height = Math.max(1, Math.ceil(cy + by1 * size) - y0);
|
|
999
|
+
const data = new Uint8ClampedArray(width * height * 4);
|
|
1000
|
+
const op = alpha >= 0 && alpha <= 1 ? alpha : 1;
|
|
1001
|
+
for (let j = 0; j < height; j++) {
|
|
1002
|
+
const y = (y0 + j + 0.5 - cy) / size;
|
|
1003
|
+
for (let i = 0; i < width; i++) {
|
|
1004
|
+
const c = cursorSoftArrowSample((x0 + i + 0.5 - cx) / size, y, radius, aa, fill, outline, polys);
|
|
1005
|
+
if (!(c[3] > 0)) continue;
|
|
1006
|
+
const o = (j * width + i) * 4;
|
|
1007
|
+
data[o] = Math.round(c[0] * 255);
|
|
1008
|
+
data[o + 1] = Math.round(c[1] * 255);
|
|
1009
|
+
data[o + 2] = Math.round(c[2] * 255);
|
|
1010
|
+
data[o + 3] = Math.round(c[3] * op * 255);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return { x0, y0, width, height, data };
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* PlayCanvas `pc.ShaderMaterial` opt-in to the depth cursor's pick (`cursor: 'depth'` on ./model
|
|
1018
|
+
* and ./splat). The pick pass draws into TWO colour targets — the pick id, then the pick depth — and
|
|
1019
|
+
* leaves out any ShaderMaterial whose fragment code does not write the second under
|
|
1020
|
+
* DEPTH_PICK_PASS (WebGL would reject the draw), so such a mesh is never under the cursor. Paste
|
|
1021
|
+
* `declarations` above `main` and `output` inside `#ifdef PICK_PASS … #else <your colour> #endif`:
|
|
1022
|
+
*
|
|
1023
|
+
* fragmentGLSL: `${CURSOR_DEPTH_PICK_GLSL.declarations}
|
|
1024
|
+
* void main() {
|
|
1025
|
+
* #ifdef PICK_PASS
|
|
1026
|
+
* ${CURSOR_DEPTH_PICK_GLSL.output}
|
|
1027
|
+
* #else
|
|
1028
|
+
* gl_FragColor = vec4(0.2, 0.3, 0.4, 1.0);
|
|
1029
|
+
* #endif
|
|
1030
|
+
* }`
|
|
1031
|
+
*
|
|
1032
|
+
* The engine's own `pickPS` chunk does the work (getPickOutput / getPickDepth), so the id and the
|
|
1033
|
+
* linear depth match its lit and gsplat materials exactly. Discard where your shader discards.
|
|
1034
|
+
*/
|
|
1035
|
+
export const CURSOR_DEPTH_PICK_GLSL = Object.freeze({
|
|
1036
|
+
declarations: '#ifdef PICK_PASS\n#include "pickPS"\n#endif\n',
|
|
1037
|
+
output: 'pcFragColor0 = getPickOutput();\n#ifdef DEPTH_PICK_PASS\npcFragColor1 = getPickDepth();\n#endif\n',
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* The WGSL twin of CURSOR_DEPTH_PICK_GLSL (a ShaderMaterial with `fragmentWGSL`, WebGPU): `output`
|
|
1042
|
+
* writes `output.color` / `output.color1` of the engine's `FragmentOutput`, inside
|
|
1043
|
+
* `#ifdef PICK_PASS … #else … #endif` in `fragmentMain`.
|
|
1044
|
+
*/
|
|
1045
|
+
export const CURSOR_DEPTH_PICK_WGSL = Object.freeze({
|
|
1046
|
+
declarations: '#ifdef PICK_PASS\n#include "pickPS"\n#endif\n',
|
|
1047
|
+
output: 'output.color = getPickOutput();\n#ifdef DEPTH_PICK_PASS\noutput.color1 = getPickDepth();\n#endif\n',
|
|
1048
|
+
});
|