@displayxr/inline3d 1.44.0 → 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 +66 -0
- package/camera.d.ts +5 -1
- package/cursor-depth.d.ts +151 -0
- package/dist/call.js +18 -4
- package/js/camera/pair-cursor.js +66 -32
- package/js/inline3d-cursor-depth-playcanvas.js +369 -69
- package/js/inline3d-cursor-depth.js +91 -20
- package/js/inline3d-cursor-option.js +24 -5
- package/js/inline3d-cursor-shape.js +962 -0
- package/js/inline3d-playcanvas-engine.js +1 -0
- package/js/inline3d-splat-layer-rig.js +3 -0
- package/js/inline3d-three.js +117 -6
- package/model.d.ts +1 -1
- package/package.json +2 -1
- package/splat.d.ts +1 -1
- package/three.d.ts +11 -1
|
@@ -27,12 +27,17 @@ import {
|
|
|
27
27
|
CursorDepthPlacer,
|
|
28
28
|
CursorPointer,
|
|
29
29
|
CURSOR_DEFAULT_TUNING,
|
|
30
|
-
cursorArrowMesh,
|
|
31
30
|
cursorArrowFootprint,
|
|
32
31
|
cursorDefaultHeight,
|
|
33
32
|
cursorPlacementSoften,
|
|
33
|
+
cursorArrowMesh,
|
|
34
34
|
cursorSoftArrowBox,
|
|
35
35
|
cursorSoftArrowGLSL,
|
|
36
|
+
CursorShapeSelector,
|
|
37
|
+
cursorShapeTexture,
|
|
38
|
+
cursorSpriteBox,
|
|
39
|
+
cursorSpriteGLSL,
|
|
40
|
+
cursorSpriteRect,
|
|
36
41
|
cursorViewRay,
|
|
37
42
|
resolveCursorSoften,
|
|
38
43
|
CURSOR_FILL_COLOR,
|
|
@@ -50,9 +55,9 @@ export class PlayCanvasDepthCursor {
|
|
|
50
55
|
/**
|
|
51
56
|
* @param {object} pc the PlayCanvas namespace the adapter renders with.
|
|
52
57
|
* @param {object} viewer the PlayCanvasSplatViewer (app, rigNode, eye are read lazily).
|
|
53
|
-
* @param {{canvas: HTMLElement, height?: number, margin?: number, anchor?: string, pointerScope?: string, soften?: string|object}} opts
|
|
58
|
+
* @param {{canvas: HTMLElement, height?: number, margin?: number, anchor?: string, pointerScope?: string, soften?: string|object, shape?: string, shapeFor?: Function}} opts
|
|
54
59
|
*/
|
|
55
|
-
constructor(pc, viewer, { canvas, height, margin, anchor = 'hybrid', pointerScope = 'canvas', soften }) {
|
|
60
|
+
constructor(pc, viewer, { canvas, height, margin, anchor = 'hybrid', pointerScope = 'canvas', soften, shape = 'auto', shapeFor = null }) {
|
|
56
61
|
this.pc = pc;
|
|
57
62
|
this.viewer = viewer;
|
|
58
63
|
/** Sprite height (canvas heights) when set; null = the OS arrow's size, from the canvas's on-page height. */
|
|
@@ -70,12 +75,21 @@ export class PlayCanvasDepthCursor {
|
|
|
70
75
|
this.hit = null;
|
|
71
76
|
this._picker = null;
|
|
72
77
|
this._pickCam = null;
|
|
78
|
+
this._picker2 = null; // the display-rig pass (handle.setLayerRig), made on first use
|
|
79
|
+
this._pickCam2 = null;
|
|
73
80
|
this._inFlight = false;
|
|
74
81
|
this._pickId = 0;
|
|
75
82
|
this._pickStart = 0;
|
|
76
83
|
this._frame = 0;
|
|
77
84
|
this._disposed = false;
|
|
78
85
|
this._sprite = null; // { mi, node, mat } — built on first use
|
|
86
|
+
/** Which sprite (CSS mirror / fixed / shapeFor); `.current` = the one drawn last. */
|
|
87
|
+
this.shapes = new CursorShapeSelector(this.pointer, { shape, shapeFor });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The sprite drawn last: a CURSOR_SHAPES name, 'none' or 'image'. */
|
|
91
|
+
get shape() {
|
|
92
|
+
return this.shapes.current;
|
|
79
93
|
}
|
|
80
94
|
|
|
81
95
|
/**
|
|
@@ -116,7 +130,11 @@ export class PlayCanvasDepthCursor {
|
|
|
116
130
|
sprite.mi.visible = !!p.active;
|
|
117
131
|
if (!p.active) return;
|
|
118
132
|
const [pw, ph] = this.pointer.panelSize();
|
|
119
|
-
|
|
133
|
+
const s = (this.softening = cursorPlacementSoften(p, views, pw, ph, h, this.soften));
|
|
134
|
+
const chosen = this.shapes.pick({ u: uv[0], v: uv[1], point: this.hit }, t);
|
|
135
|
+
if (chosen.kind === 'none') sprite.mi.visible = false; // the page hides its cursor: so do we (the OS one stays hidden)
|
|
136
|
+
else if (chosen.kind === 'shape' && chosen.name === 'arrow') this._useSoft(app, sprite, s);
|
|
137
|
+
else this._useSprite(app, sprite, s, chosen);
|
|
120
138
|
// The unit arrow in the sprite's XY plane, tip at its origin: basis (rotation), height (scale), position = the hotspot.
|
|
121
139
|
const { x, y, z } = p.basis;
|
|
122
140
|
const q = quatFromMatrix([x[0], x[1], x[2], 0, y[0], y[1], y[2], 0, z[0], z[1], z[2], 0, 0, 0, 0, 1]);
|
|
@@ -177,7 +195,7 @@ export class PlayCanvasDepthCursor {
|
|
|
177
195
|
mi.pick = false;
|
|
178
196
|
mi.drawOrder = 1e9;
|
|
179
197
|
app.scene.layers.getLayerById(pc.LAYERID_UI).addMeshInstances([mi]);
|
|
180
|
-
this._sprite = { mi, node, mat, mesh, soft: null };
|
|
198
|
+
this._sprite = { mi, node, mat, mesh, soft: null, tex: null, textures: new Map() };
|
|
181
199
|
return this._sprite;
|
|
182
200
|
}
|
|
183
201
|
|
|
@@ -245,26 +263,130 @@ export class PlayCanvasDepthCursor {
|
|
|
245
263
|
return { mesh, mat };
|
|
246
264
|
}
|
|
247
265
|
|
|
266
|
+
// Every sprite but the arrow: a texture (a keyword shape rasterised once, or a url() image) on
|
|
267
|
+
// one soft quad — the same 13-tap blur and opacity, crisp at radius 0. One program for all.
|
|
268
|
+
_useSprite(app, sprite, s, chosen) {
|
|
269
|
+
const mi = sprite.mi;
|
|
270
|
+
const q = sprite.tex || (sprite.tex = this._makeTex(app));
|
|
271
|
+
const radius = s.soft ? s.radius : 0;
|
|
272
|
+
q.mat.setParameter('inline3dCursorTex', this._texture(app, sprite, chosen));
|
|
273
|
+
q.mat.setParameter('inline3dCursorBox', cursorSpriteBox(chosen, radius, s.aa));
|
|
274
|
+
q.mat.setParameter('inline3dCursorRect', cursorSpriteRect(chosen));
|
|
275
|
+
q.mat.setParameter('inline3dCursorSoft', [radius, s.soft ? s.alpha : 1, 0, 0]);
|
|
276
|
+
if (mi.mesh !== q.mesh) mi.mesh = q.mesh;
|
|
277
|
+
if (mi.material !== q.mat) mi.material = q.mat;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
_texture(app, sprite, chosen) {
|
|
281
|
+
let t = sprite.textures.get(chosen.key);
|
|
282
|
+
if (t) return t;
|
|
283
|
+
const pc = this.pc;
|
|
284
|
+
const opts = {
|
|
285
|
+
name: `inline3d-cursor-${chosen.kind}`,
|
|
286
|
+
format: pc.PIXELFORMAT_RGBA8,
|
|
287
|
+
mipmaps: false,
|
|
288
|
+
minFilter: pc.FILTER_LINEAR,
|
|
289
|
+
magFilter: pc.FILTER_LINEAR,
|
|
290
|
+
addressU: pc.ADDRESS_CLAMP_TO_EDGE,
|
|
291
|
+
addressV: pc.ADDRESS_CLAMP_TO_EDGE,
|
|
292
|
+
flipY: false, // row 0 = the sprite's top, as the shader reads it
|
|
293
|
+
};
|
|
294
|
+
if (chosen.kind === 'image') {
|
|
295
|
+
const img = chosen.entry.image;
|
|
296
|
+
t = new pc.Texture(app.graphicsDevice, { ...opts, width: img.naturalWidth || img.width, height: img.naturalHeight || img.height });
|
|
297
|
+
t.setSource(img);
|
|
298
|
+
} else {
|
|
299
|
+
const d = cursorShapeTexture(chosen.name);
|
|
300
|
+
t = new pc.Texture(app.graphicsDevice, { ...opts, width: d.width, height: d.height });
|
|
301
|
+
t.lock().set(d.data);
|
|
302
|
+
t.unlock();
|
|
303
|
+
}
|
|
304
|
+
if (sprite.textures.size > 32) {
|
|
305
|
+
for (const old of sprite.textures.values()) old.destroy?.();
|
|
306
|
+
sprite.textures.clear();
|
|
307
|
+
}
|
|
308
|
+
sprite.textures.set(chosen.key, t);
|
|
309
|
+
return t;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
_makeTex(app) {
|
|
313
|
+
const pc = this.pc;
|
|
314
|
+
const mesh = new pc.Mesh(app.graphicsDevice);
|
|
315
|
+
mesh.setPositions(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0]));
|
|
316
|
+
mesh.update();
|
|
317
|
+
const mat = new pc.ShaderMaterial({
|
|
318
|
+
uniqueName: 'inline3dDepthCursorSprite',
|
|
319
|
+
attributes: { vertex_position: pc.SEMANTIC_POSITION },
|
|
320
|
+
vertexGLSL: `
|
|
321
|
+
attribute vec3 vertex_position;
|
|
322
|
+
uniform mat4 matrix_model;
|
|
323
|
+
uniform mat4 matrix_viewProjection;
|
|
324
|
+
uniform vec4 inline3dCursorBox;
|
|
325
|
+
varying vec2 vSprite;
|
|
326
|
+
void main() {
|
|
327
|
+
vSprite = vec2(mix(inline3dCursorBox.x, inline3dCursorBox.z, vertex_position.x), mix(inline3dCursorBox.y, inline3dCursorBox.w, vertex_position.y));
|
|
328
|
+
gl_Position = matrix_viewProjection * matrix_model * vec4(vSprite.x, -vSprite.y, 0.0, 1.0);
|
|
329
|
+
}`,
|
|
330
|
+
fragmentGLSL: `
|
|
331
|
+
uniform vec4 inline3dCursorRect; // the area the texture covers (sprite units, y down)
|
|
332
|
+
uniform vec4 inline3dCursorSoft; // radius (sprite units), opacity
|
|
333
|
+
uniform sampler2D inline3dCursorTex;
|
|
334
|
+
varying vec2 vSprite;
|
|
335
|
+
${cursorSpriteGLSL()}
|
|
336
|
+
void main() {
|
|
337
|
+
vec4 c = inline3dSoftSprite(inline3dCursorTex, vSprite, inline3dCursorRect, inline3dCursorSoft.x);
|
|
338
|
+
gl_FragColor = vec4(c.rgb, c.a * inline3dCursorSoft.y);
|
|
339
|
+
}`,
|
|
340
|
+
});
|
|
341
|
+
mat.blendState = new pc.BlendState(
|
|
342
|
+
true,
|
|
343
|
+
pc.BLENDEQUATION_ADD,
|
|
344
|
+
pc.BLENDMODE_SRC_ALPHA,
|
|
345
|
+
pc.BLENDMODE_ONE_MINUS_SRC_ALPHA,
|
|
346
|
+
pc.BLENDEQUATION_ADD,
|
|
347
|
+
pc.BLENDMODE_ONE,
|
|
348
|
+
pc.BLENDMODE_ONE_MINUS_SRC_ALPHA,
|
|
349
|
+
);
|
|
350
|
+
mat.depthTest = false;
|
|
351
|
+
mat.depthWrite = false;
|
|
352
|
+
mat.cull = pc.CULLFACE_NONE;
|
|
353
|
+
mat.update();
|
|
354
|
+
return { mesh, mat };
|
|
355
|
+
}
|
|
356
|
+
|
|
248
357
|
dispose() {
|
|
249
358
|
this._disposed = true;
|
|
250
359
|
this.pointer.dispose();
|
|
360
|
+
this.shapes.dispose();
|
|
251
361
|
try {
|
|
252
362
|
if (this._sprite) {
|
|
253
363
|
this.viewer.app?.scene?.layers?.getLayerById(this.pc.LAYERID_UI)?.removeMeshInstances?.([this._sprite.mi]);
|
|
254
364
|
this._sprite.node.destroy?.();
|
|
255
365
|
this._sprite.soft?.mat.destroy?.();
|
|
256
366
|
this._sprite.soft?.mesh.destroy?.();
|
|
367
|
+
this._sprite.tex?.mat.destroy?.();
|
|
368
|
+
this._sprite.tex?.mesh.destroy?.();
|
|
369
|
+
for (const t of this._sprite.textures.values()) t.destroy?.();
|
|
257
370
|
}
|
|
258
371
|
this._picker?.destroy?.();
|
|
259
372
|
this._pickCam?.destroy?.();
|
|
373
|
+
this._picker2?.destroy?.();
|
|
374
|
+
this._pickCam2?.destroy?.();
|
|
260
375
|
} catch {
|
|
261
376
|
// the app may already be gone with its device
|
|
262
377
|
}
|
|
263
378
|
this._picker = null;
|
|
264
379
|
this._pickCam = null;
|
|
380
|
+
this._picker2 = null;
|
|
381
|
+
this._pickCam2 = null;
|
|
265
382
|
}
|
|
266
383
|
|
|
267
|
-
// Render the pick buffer from the first eye and read the footprint's world points.
|
|
384
|
+
// Render the pick buffer from the first eye and read the footprint's world points. Layers moved
|
|
385
|
+
// to the display rig (handle.setLayerRig) are drawn by the layer rig's own cameras, not the eye:
|
|
386
|
+
// with the rig ROUNDED (a camera rig, gain != 1) they take a second pass through the round
|
|
387
|
+
// views, and each of their hits becomes the world point the PHOTO views see at the same pixels
|
|
388
|
+
// in both eyes, so the placer (which solves against the photo views) puts the arrow where the
|
|
389
|
+
// stage appears. Otherwise they draw through the photo views and simply join the eye pass.
|
|
268
390
|
_pick(app, rig, e, uv, height) {
|
|
269
391
|
const pc = this.pc;
|
|
270
392
|
const aspect = e.width > 0 && e.height > 0 ? e.width / e.height : 1;
|
|
@@ -272,59 +394,27 @@ export class PlayCanvasDepthCursor {
|
|
|
272
394
|
const H = Math.max(1, Math.round(W / aspect));
|
|
273
395
|
if (!this._picker) {
|
|
274
396
|
this._picker = new pc.Picker(app, W, H, true);
|
|
275
|
-
|
|
276
|
-
// frustumCulling off: this camera never renders on its own (disabled — the picker drives
|
|
277
|
-
// it), so nothing keeps its culling frustum current, and a stale one culls everything.
|
|
278
|
-
cam.addComponent('camera', { clearColor: new pc.Color(0, 0, 0, 0), frustumCulling: false });
|
|
279
|
-
cam.camera.enabled = false;
|
|
280
|
-
cam._dxrProj = new Float64Array(16);
|
|
281
|
-
cam.camera.calculateProjection = (out) => out.set(cam._dxrProj);
|
|
282
|
-
rig.addChild(cam);
|
|
283
|
-
this._pickCam = cam;
|
|
397
|
+
this._pickCam = this._makePickCam(app, rig, 'inline3d-cursor-pick');
|
|
284
398
|
}
|
|
285
399
|
const cam = this._pickCam;
|
|
286
|
-
cam._dxrProj.set(e.proj);
|
|
287
400
|
// The picker linearises depth with the COMPONENT's near/far, and reconstructs through the
|
|
288
401
|
// projection: keep the two in agreement (GL-style projection: m10 = -(f+n)/(f-n), m14 = -2fn/(f-n)).
|
|
289
402
|
const m10 = e.proj[10], m14 = e.proj[14];
|
|
290
403
|
const near = m14 / (m10 - 1);
|
|
291
404
|
const far = m14 / (m10 + 1);
|
|
292
|
-
|
|
293
|
-
cam.camera.nearClip = near;
|
|
294
|
-
cam.camera.farClip = far;
|
|
295
|
-
}
|
|
296
|
-
if (cam.camera.camera) cam.camera.camera._projMatDirty = true;
|
|
297
|
-
const q = quatFromMatrix(e.pose);
|
|
298
|
-
cam.setLocalPosition(e.pose[12], e.pose[13], e.pose[14]);
|
|
299
|
-
cam.setLocalRotation(q[0], q[1], q[2], q[3]);
|
|
405
|
+
placePickCam(cam, e.proj, e.pose, near, far);
|
|
300
406
|
const eye = this.viewer.eye?.camera;
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
407
|
+
const plan = cursorPickPlan(
|
|
408
|
+
eye && Array.isArray(eye.layers) ? eye.layers : cam.camera.layers,
|
|
409
|
+
this.viewer.layerRigs,
|
|
410
|
+
[pc.LAYERID_UI, pc.LAYERID_IMMEDIATE, pc.LAYERID_SKYBOX],
|
|
411
|
+
);
|
|
412
|
+
const layers = plan.eye.map((id) => app.scene.layers.getLayerById(id)).filter(Boolean);
|
|
413
|
+
setCamLayers(cam, plan.eye);
|
|
304
414
|
|
|
305
415
|
const picker = this._picker;
|
|
306
416
|
picker.resize(W, H);
|
|
307
|
-
|
|
308
|
-
// gsplat shaders write both under DEPTH_PICK_PASS; an app's own pc.ShaderMaterial usually
|
|
309
|
-
// writes only the first, and WebGL rejects every such draw with GL_INVALID_OPERATION ("Active
|
|
310
|
-
// draw buffers with missing fragment shader outputs") — one per mesh per pick, so a page with
|
|
311
|
-
// a custom-shader backdrop logged hundreds a second. Those draws never reached the pick buffer
|
|
312
|
-
// anyway: leave them out of the pass, exactly as if they were not pickable.
|
|
313
|
-
const muted = muteDepthlessPicks(pc, layers, app.graphicsDevice);
|
|
314
|
-
try {
|
|
315
|
-
// Enabled only for the synchronous pick pass: a disabled camera is not in its layers'
|
|
316
|
-
// camera lists, so the picker would render nothing; enabled outside it, the camera would
|
|
317
|
-
// render a frame of its own on every app.tick.
|
|
318
|
-
cam.camera.enabled = true;
|
|
319
|
-
picker.prepare(cam.camera, app.scene, layers);
|
|
320
|
-
} catch (err) {
|
|
321
|
-
console.warn('[inline3d] cursor: depth: the PlayCanvas picker failed; cursor stays on the glass.', err);
|
|
322
|
-
this.hit = null;
|
|
323
|
-
return;
|
|
324
|
-
} finally {
|
|
325
|
-
cam.camera.enabled = false;
|
|
326
|
-
for (const mi of muted) mi.pick = true;
|
|
327
|
-
}
|
|
417
|
+
if (!this._prepare(picker, cam, app, layers)) return;
|
|
328
418
|
// The picker stores LINEAR depth: (viewDist - near) / (far - near), viewDist = 1/gl_FragCoord.w.
|
|
329
419
|
// Rebuild the point ourselves along the exact off-axis ray (getWorldPointAsync would unproject
|
|
330
420
|
// with the component's symmetric projection, not this one). PlayCanvas's view matrix is built
|
|
@@ -334,33 +424,31 @@ export class PlayCanvasDepthCursor {
|
|
|
334
424
|
const Mw = view.transformMatrix;
|
|
335
425
|
const f = [-Mw[8], -Mw[9], -Mw[10]];
|
|
336
426
|
const zNear = cam.camera.nearClip, zFar = cam.camera.farClip;
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
427
|
+
const footprint = cursorArrowFootprint(uv[0], uv[1], height, this.pointer.aspect()).filter(([u, v]) => u >= 0 && u <= 1 && v >= 0 && v <= 1);
|
|
428
|
+
const read = (pk, u, v, toWorld) =>
|
|
429
|
+
pk.getPointDepthAsync(Math.min(W - 1, Math.floor(u * W)), Math.min(H - 1, Math.floor(v * H))).then((lin) => {
|
|
430
|
+
if (lin === null || !(lin >= 0 && lin < 1)) return null;
|
|
431
|
+
return toWorld(u, v, zNear + lin * (zFar - zNear));
|
|
432
|
+
});
|
|
433
|
+
const samples = footprint.map(([u, v]) =>
|
|
434
|
+
read(picker, u, v, (uu, vv, z) => {
|
|
435
|
+
const ray = cursorViewRay(view, uu, vv);
|
|
436
|
+
return { x: ray.origin[0] + ray.direction[0] * z, y: ray.origin[1] + ray.direction[1] * z, z: ray.origin[2] + ray.direction[2] * z };
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
439
|
+
// The display run, rounded: a second pass, only while such a layer exists (zero cost otherwise).
|
|
440
|
+
if (plan.display.length) {
|
|
441
|
+
const lr = this.viewer.layerRigs;
|
|
442
|
+
const pass = this._pickDisplay(app, rig, e, lr, plan.display, near, far, W, H);
|
|
443
|
+
if (pass) for (const [u, v] of footprint) samples.push(read(pass.picker, u, v, pass.toWorld));
|
|
444
|
+
}
|
|
347
445
|
this._inFlight = true;
|
|
348
446
|
const id = ++this._pickId;
|
|
349
447
|
this._pickStart = globalThis.performance ? globalThis.performance.now() : Date.now();
|
|
350
448
|
Promise.all(samples)
|
|
351
449
|
.then((pts) => {
|
|
352
450
|
if (this._disposed || id !== this._pickId) return;
|
|
353
|
-
|
|
354
|
-
let nearest = null;
|
|
355
|
-
for (const p of pts) {
|
|
356
|
-
if (!p) continue;
|
|
357
|
-
const d = p.x * f[0] + p.y * f[1] + p.z * f[2];
|
|
358
|
-
if (d < best) {
|
|
359
|
-
best = d;
|
|
360
|
-
nearest = [p.x, p.y, p.z];
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
this.hit = nearest;
|
|
451
|
+
this.hit = nearestPickPoint(pts, f);
|
|
364
452
|
})
|
|
365
453
|
.catch(() => {
|
|
366
454
|
if (id === this._pickId) this.hit = null;
|
|
@@ -369,6 +457,218 @@ export class PlayCanvasDepthCursor {
|
|
|
369
457
|
if (id === this._pickId) this._inFlight = false;
|
|
370
458
|
});
|
|
371
459
|
}
|
|
460
|
+
|
|
461
|
+
// The pick camera: a child of the rig node, DISABLED except around prepare() (a disabled camera
|
|
462
|
+
// is in no layer's camera list, so the picker would render nothing; an enabled one would render
|
|
463
|
+
// a frame of its own on every app.tick), with a projection override.
|
|
464
|
+
_makePickCam(app, rig, name) {
|
|
465
|
+
const pc = this.pc;
|
|
466
|
+
const cam = new pc.Entity(name, app);
|
|
467
|
+
// frustumCulling off: this camera never renders on its own, so nothing keeps its culling
|
|
468
|
+
// frustum current, and a stale one culls everything.
|
|
469
|
+
cam.addComponent('camera', { clearColor: new pc.Color(0, 0, 0, 0), frustumCulling: false });
|
|
470
|
+
cam.camera.enabled = false;
|
|
471
|
+
cam._dxrProj = new Float64Array(16);
|
|
472
|
+
cam.camera.calculateProjection = (out) => out.set(cam._dxrProj);
|
|
473
|
+
rig.addChild(cam);
|
|
474
|
+
return cam;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// One synchronous pick pass. The depth picker draws into TWO colour targets (pick id + pick
|
|
478
|
+
// depth). The engine's lit and gsplat shaders write both under DEPTH_PICK_PASS; an app's own
|
|
479
|
+
// pc.ShaderMaterial usually writes only the first, and WebGL rejects every such draw with
|
|
480
|
+
// GL_INVALID_OPERATION ("Active draw buffers with missing fragment shader outputs") — one per
|
|
481
|
+
// mesh per pick, so a page with a custom-shader backdrop logged hundreds a second. Those draws
|
|
482
|
+
// never reached the pick buffer anyway: leave them out of the pass, exactly as if they were not
|
|
483
|
+
// pickable. (A ShaderMaterial opts in with CURSOR_DEPTH_PICK_GLSL / _WGSL.)
|
|
484
|
+
_prepare(picker, cam, app, layers) {
|
|
485
|
+
const muted = muteDepthlessPicks(this.pc, layers, app.graphicsDevice);
|
|
486
|
+
try {
|
|
487
|
+
cam.camera.enabled = true;
|
|
488
|
+
picker.prepare(cam.camera, app.scene, layers);
|
|
489
|
+
return true;
|
|
490
|
+
} catch (err) {
|
|
491
|
+
console.warn('[inline3d] cursor: depth: the PlayCanvas picker failed; cursor stays on the glass.', err);
|
|
492
|
+
this.hit = null;
|
|
493
|
+
return false;
|
|
494
|
+
} finally {
|
|
495
|
+
cam.camera.enabled = false;
|
|
496
|
+
for (const mi of muted) mi.pick = true;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// The display-rig pass: its own picker + camera, drawn with exactly what the layer rig's display
|
|
501
|
+
// camera draws, proj · view_round · rig⁻¹ (PlayCanvas composes RenderViews with the rig node's
|
|
502
|
+
// world transform, scale included). The camera's own view is undone in the projection override,
|
|
503
|
+
// as the layer rig's N-camera path does. Null (no pass) when the rig has no round views this frame.
|
|
504
|
+
_pickDisplay(app, rig, e, lr, ids, near, far, W, H) {
|
|
505
|
+
const round = lr?.lastRound;
|
|
506
|
+
const poses = lr?.lastEntries;
|
|
507
|
+
if (!round || !poses || round.length < 2 || poses.length !== round.length) return null;
|
|
508
|
+
const pc = this.pc;
|
|
509
|
+
if (!this._picker2) {
|
|
510
|
+
this._picker2 = new pc.Picker(app, W, H, true);
|
|
511
|
+
this._pickCam2 = this._makePickCam(app, rig, 'inline3d-cursor-pick-display');
|
|
512
|
+
}
|
|
513
|
+
const cam = this._pickCam2;
|
|
514
|
+
placePickCam(cam, e.proj, e.pose, near, far);
|
|
515
|
+
const R = rig.getWorldTransform().data;
|
|
516
|
+
const Rinv = invert4(R);
|
|
517
|
+
if (!Rinv) return null;
|
|
518
|
+
cam._dxrProj.set(mul4(e.proj, mul4(round[0].view, mul4(Rinv, unscaled(cam.getWorldTransform().data)))));
|
|
519
|
+
if (cam.camera.camera) cam.camera.camera._projMatDirty = true;
|
|
520
|
+
setCamLayers(cam, ids);
|
|
521
|
+
const layers = ids.map((id) => app.scene.layers.getLayerById(id)).filter(Boolean);
|
|
522
|
+
const picker = this._picker2;
|
|
523
|
+
picker.resize(W, H);
|
|
524
|
+
if (!this._prepare(picker, cam, app, layers)) return null;
|
|
525
|
+
const solve = displayRigPickSolver(e.proj, round, poses.map((p) => p.pose), R);
|
|
526
|
+
if (!solve) return null;
|
|
527
|
+
return { picker, toWorld: (u, v, dist) => solve(u, v, dist) };
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Which layers each pick pass draws. `eyeLayers` the eye camera's list; `layerRigs` the viewer's
|
|
533
|
+
* LayerRigCameras (or null); `skip` the ids never picked (UI, immediate, skybox).
|
|
534
|
+
* eye — the eye's layers, plus the layer rig's post run and (when the rig is not rounded this
|
|
535
|
+
* frame) its display run: all drawn through the photo views.
|
|
536
|
+
* display — the display run, when it is rounded: picked through the round views.
|
|
537
|
+
* @returns {{ eye: number[], display: number[] }}
|
|
538
|
+
*/
|
|
539
|
+
export function cursorPickPlan(eyeLayers, layerRigs, skip = []) {
|
|
540
|
+
const no = new Set(skip);
|
|
541
|
+
const eye = (eyeLayers || []).filter((id) => !no.has(id));
|
|
542
|
+
const moved = layerRigs && layerRigs.active !== false ? layerRigs.moved : null;
|
|
543
|
+
if (!moved || (!moved.display?.length && !moved.post?.length)) return { eye, display: [] };
|
|
544
|
+
const rounded = !!(layerRigs.lastRound && layerRigs.lastRound.length >= 2);
|
|
545
|
+
const add = (list, ids) => {
|
|
546
|
+
for (const id of ids || []) if (!no.has(id) && !list.includes(id)) list.push(id);
|
|
547
|
+
};
|
|
548
|
+
const display = [];
|
|
549
|
+
if (rounded) add(display, moved.display);
|
|
550
|
+
else add(eye, moved.display);
|
|
551
|
+
add(eye, moved.post);
|
|
552
|
+
return { eye, display: display.filter((id) => !eye.includes(id)) };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* The picked point nearest the viewer: the smallest distance along the eye's forward `f` (world),
|
|
557
|
+
* i.e. the most crossed disparity. `pts` are {x, y, z} or [x, y, z] (null = nothing there).
|
|
558
|
+
* @returns {number[]|null} [x, y, z], or null when nothing was hit.
|
|
559
|
+
*/
|
|
560
|
+
export function nearestPickPoint(pts, f) {
|
|
561
|
+
let best = Infinity;
|
|
562
|
+
let nearest = null;
|
|
563
|
+
for (const p of pts || []) {
|
|
564
|
+
if (!p) continue;
|
|
565
|
+
const x = Array.isArray(p) ? p[0] : p.x, y = Array.isArray(p) ? p[1] : p.y, z = Array.isArray(p) ? p[2] : p.z;
|
|
566
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) continue;
|
|
567
|
+
const d = x * f[0] + y * f[1] + z * f[2];
|
|
568
|
+
if (d < best) {
|
|
569
|
+
best = d;
|
|
570
|
+
nearest = [x, y, z];
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return nearest;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Turn a display-rig pick into the WORLD point the PHOTO views show at the same place.
|
|
578
|
+
*
|
|
579
|
+
* The pick was drawn from round eye 0 (`rounds[i].view` maps rig space to round eye i); a sample at
|
|
580
|
+
* canvas (u, v) with view distance `dist` is the round-space point X. Each round eye i shows X at
|
|
581
|
+
* the pixel where the PHOTO eye i (pose `poses[i]`, rig space) sees P_i · view_round_i · X, so the
|
|
582
|
+
* point the photo views put at both eyes' pixels is where the photo rays of the first and last eye
|
|
583
|
+
* through those points meet. Taken ON eye 0's ray (the pixel that was sampled stays exact) at its
|
|
584
|
+
* closest approach to the last eye's (exact whenever the rays meet: eyes level with each other).
|
|
585
|
+
* Mapped to world by the rig node's transform `R`. With no rounding (round view = photo view) it
|
|
586
|
+
* is X itself.
|
|
587
|
+
* @returns {((u: number, v: number, dist: number) => number[]|null)|null}
|
|
588
|
+
*/
|
|
589
|
+
export function displayRigPickSolver(proj, rounds, poses, R) {
|
|
590
|
+
const L = rounds.length - 1;
|
|
591
|
+
if (L < 1 || poses.length <= L) return null;
|
|
592
|
+
const T0 = mul4(poses[0], mul4(rounds[0].view, rounds[0].viewInv));
|
|
593
|
+
const TL = mul4(poses[L], mul4(rounds[L].view, rounds[0].viewInv));
|
|
594
|
+
const E0 = [poses[0][12], poses[0][13], poses[0][14]];
|
|
595
|
+
const EL = [poses[L][12], poses[L][13], poses[L][14]];
|
|
596
|
+
return (u, v, dist) => {
|
|
597
|
+
if (!(dist > 0)) return null;
|
|
598
|
+
// Round eye 0's view space: the direction through NDC (x, y) is ((x + c)/a, (y + d)/b, -1).
|
|
599
|
+
const x = 2 * u - 1, y = 1 - 2 * v;
|
|
600
|
+
const Xe = [((x + proj[8]) / proj[0]) * dist, ((y + proj[9]) / proj[5]) * dist, -dist];
|
|
601
|
+
const Q0 = apply(T0, Xe), QL = apply(TL, Xe);
|
|
602
|
+
const P = closestApproach(E0, sub3(Q0, E0), EL, sub3(QL, EL)) || Q0;
|
|
603
|
+
const w = apply(R, P);
|
|
604
|
+
return Number.isFinite(w[0]) && Number.isFinite(w[1]) && Number.isFinite(w[2]) ? w : null;
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function sub3(a, b) {
|
|
609
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** Column-major 4x4 × point (w = 1, affine). */
|
|
613
|
+
function apply(m, p) {
|
|
614
|
+
return [0, 1, 2].map((r) => m[r] * p[0] + m[4 + r] * p[1] + m[8 + r] * p[2] + m[12 + r]);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** The point of p1 + s·d1 closest to the line p2 + r·d2; null when (near) parallel. */
|
|
618
|
+
function closestApproach(p1, d1, p2, d2) {
|
|
619
|
+
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
620
|
+
const w0 = sub3(p1, p2);
|
|
621
|
+
const aa = dot(d1, d1), bb = dot(d1, d2), cc = dot(d2, d2), dd = dot(d1, w0), ee = dot(d2, w0);
|
|
622
|
+
const den = aa * cc - bb * bb;
|
|
623
|
+
if (!(den > 1e-12 * aa * cc)) return null;
|
|
624
|
+
const s = (bb * ee - cc * dd) / den;
|
|
625
|
+
return [0, 1, 2].map((i) => p1[i] + s * d1[i]);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** Point a pick camera along a view: its projection override, near/far, and rig-space pose. */
|
|
629
|
+
function placePickCam(cam, proj, pose, near, far) {
|
|
630
|
+
cam._dxrProj.set(proj);
|
|
631
|
+
if (near > 0 && far > near) {
|
|
632
|
+
cam.camera.nearClip = near;
|
|
633
|
+
cam.camera.farClip = far;
|
|
634
|
+
}
|
|
635
|
+
if (cam.camera.camera) cam.camera.camera._projMatDirty = true;
|
|
636
|
+
const q = quatFromMatrix(pose);
|
|
637
|
+
cam.setLocalPosition(pose[12], pose[13], pose[14]);
|
|
638
|
+
cam.setLocalRotation(q[0], q[1], q[2], q[3]);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** The picker draws only sublayers whose camera list holds the camera: give it these layers (on change only). */
|
|
642
|
+
function setCamLayers(cam, ids) {
|
|
643
|
+
const key = ids.join(',');
|
|
644
|
+
if (cam._dxrLayersKey === key) return;
|
|
645
|
+
cam._dxrLayersKey = key;
|
|
646
|
+
cam.camera.layers = ids.slice();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** General column-major 4x4 inverse (cofactors); null when singular. */
|
|
650
|
+
function invert4(m) {
|
|
651
|
+
const a = m, o = new Array(16);
|
|
652
|
+
o[0] = a[5]*a[10]*a[15]-a[5]*a[11]*a[14]-a[9]*a[6]*a[15]+a[9]*a[7]*a[14]+a[13]*a[6]*a[11]-a[13]*a[7]*a[10];
|
|
653
|
+
o[4] = -a[4]*a[10]*a[15]+a[4]*a[11]*a[14]+a[8]*a[6]*a[15]-a[8]*a[7]*a[14]-a[12]*a[6]*a[11]+a[12]*a[7]*a[10];
|
|
654
|
+
o[8] = a[4]*a[9]*a[15]-a[4]*a[11]*a[13]-a[8]*a[5]*a[15]+a[8]*a[7]*a[13]+a[12]*a[5]*a[11]-a[12]*a[7]*a[9];
|
|
655
|
+
o[12] = -a[4]*a[9]*a[14]+a[4]*a[10]*a[13]+a[8]*a[5]*a[14]-a[8]*a[6]*a[13]-a[12]*a[5]*a[10]+a[12]*a[6]*a[9];
|
|
656
|
+
o[1] = -a[1]*a[10]*a[15]+a[1]*a[11]*a[14]+a[9]*a[2]*a[15]-a[9]*a[3]*a[14]-a[13]*a[2]*a[11]+a[13]*a[3]*a[10];
|
|
657
|
+
o[5] = a[0]*a[10]*a[15]-a[0]*a[11]*a[14]-a[8]*a[2]*a[15]+a[8]*a[3]*a[14]+a[12]*a[2]*a[11]-a[12]*a[3]*a[10];
|
|
658
|
+
o[9] = -a[0]*a[9]*a[15]+a[0]*a[11]*a[13]+a[8]*a[1]*a[15]-a[8]*a[3]*a[13]-a[12]*a[1]*a[11]+a[12]*a[3]*a[9];
|
|
659
|
+
o[13] = a[0]*a[9]*a[14]-a[0]*a[10]*a[13]-a[8]*a[1]*a[14]+a[8]*a[2]*a[13]+a[12]*a[1]*a[10]-a[12]*a[2]*a[9];
|
|
660
|
+
o[2] = a[1]*a[6]*a[15]-a[1]*a[7]*a[14]-a[5]*a[2]*a[15]+a[5]*a[3]*a[14]+a[13]*a[2]*a[7]-a[13]*a[3]*a[6];
|
|
661
|
+
o[6] = -a[0]*a[6]*a[15]+a[0]*a[7]*a[14]+a[4]*a[2]*a[15]-a[4]*a[3]*a[14]-a[12]*a[2]*a[7]+a[12]*a[3]*a[6];
|
|
662
|
+
o[10] = a[0]*a[5]*a[15]-a[0]*a[7]*a[13]-a[4]*a[1]*a[15]+a[4]*a[3]*a[13]+a[12]*a[1]*a[7]-a[12]*a[3]*a[5];
|
|
663
|
+
o[14] = -a[0]*a[5]*a[14]+a[0]*a[6]*a[13]+a[4]*a[1]*a[14]-a[4]*a[2]*a[13]-a[12]*a[1]*a[6]+a[12]*a[2]*a[5];
|
|
664
|
+
o[3] = -a[1]*a[6]*a[11]+a[1]*a[7]*a[10]+a[5]*a[2]*a[11]-a[5]*a[3]*a[10]-a[9]*a[2]*a[7]+a[9]*a[3]*a[6];
|
|
665
|
+
o[7] = a[0]*a[6]*a[11]-a[0]*a[7]*a[10]-a[4]*a[2]*a[11]+a[4]*a[3]*a[10]+a[8]*a[2]*a[7]-a[8]*a[3]*a[6];
|
|
666
|
+
o[11] = -a[0]*a[5]*a[11]+a[0]*a[7]*a[9]+a[4]*a[1]*a[11]-a[4]*a[3]*a[9]-a[8]*a[1]*a[7]+a[8]*a[3]*a[5];
|
|
667
|
+
o[15] = a[0]*a[5]*a[10]-a[0]*a[6]*a[9]-a[4]*a[1]*a[10]+a[4]*a[2]*a[9]+a[8]*a[1]*a[6]-a[8]*a[2]*a[5];
|
|
668
|
+
const det = a[0]*o[0]+a[1]*o[4]+a[2]*o[8]+a[3]*o[12];
|
|
669
|
+
if (!det || !Number.isFinite(det)) return null;
|
|
670
|
+
for (let i = 0; i < 16; i++) o[i] /= det;
|
|
671
|
+
return o;
|
|
372
672
|
}
|
|
373
673
|
|
|
374
674
|
/** A copy of a column-major transform with unit rotation columns (scale removed). */
|