Skip to content

Commit bcfa2fa

Browse files
authored
Dynamic hotspots (#3981)
* switch to three.js reduceVertices * cleanup and refactor of fromPoint methods * implementation complete * updated editor to dynamic hotspots * position working * added normals * fixed editor tests * added test * added docs * added example * update
1 parent 02e22e6 commit bcfa2fa

File tree

24 files changed

+468
-405
lines changed

24 files changed

+468
-405
lines changed

packages/model-viewer/src/features/annotation.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import {Matrix3, Matrix4, Vector3} from 'three';
17+
import {Matrix4, Vector3} from 'three';
1818

1919
import ModelViewerElementBase, {$needsRender, $scene, $tick, toVector2D, toVector3D, Vector2D, Vector3D} from '../model-viewer-base.js';
2020
import {Hotspot, HotspotConfiguration} from '../three-components/Hotspot.js';
@@ -27,7 +27,6 @@ const $addHotspot = Symbol('addHotspot');
2727
const $removeHotspot = Symbol('removeHotspot');
2828

2929
const worldToModel = new Matrix4();
30-
const worldToModelNormal = new Matrix3();
3130

3231
export declare type HotspotData = {
3332
position: Vector3D,
@@ -40,7 +39,8 @@ export declare interface AnnotationInterface {
4039
updateHotspot(config: HotspotConfiguration): void;
4140
queryHotspot(name: string): HotspotData|null;
4241
positionAndNormalFromPoint(pixelX: number, pixelY: number):
43-
{position: Vector3D, normal: Vector3D, uv: Vector2D|null}|null
42+
{position: Vector3D, normal: Vector3D, uv: Vector2D|null}|null;
43+
surfaceFromPoint(pixelX: number, pixelY: number): string|null;
4444
}
4545

4646
/**
@@ -108,7 +108,8 @@ export const AnnotationMixin = <T extends Constructor<ModelViewerElementBase>>(
108108
const camera = scene.getCamera();
109109

110110
if (scene.shouldRender()) {
111-
scene.updateHotspots(camera.position);
111+
scene.updateSurfaceHotspots();
112+
scene.updateHotspotsVisibility(camera.position);
112113
annotationRenderer.domElement.style.display = '';
113114
annotationRenderer.render(scene, camera);
114115
}
@@ -190,10 +191,7 @@ export const AnnotationMixin = <T extends Constructor<ModelViewerElementBase>>(
190191

191192
worldToModel.copy(scene.target.matrixWorld).invert();
192193
const position = toVector3D(hit.position.applyMatrix4(worldToModel));
193-
194-
worldToModelNormal.getNormalMatrix(worldToModel);
195-
const normal =
196-
toVector3D(hit.normal.applyNormalMatrix(worldToModelNormal));
194+
const normal = toVector3D(hit.normal.transformDirection(worldToModel));
197195

198196
let uv = null;
199197
if (hit.uv != null) {
@@ -203,6 +201,20 @@ export const AnnotationMixin = <T extends Constructor<ModelViewerElementBase>>(
203201
return {position: position, normal: normal, uv: uv};
204202
}
205203

204+
/**
205+
* This method returns a dynamic hotspot ID string of the point on the mesh
206+
* corresponding to the input pixel coordinates given relative to the
207+
* model-viewer element. The ID string can be used in the data-surface
208+
* attribute of the hotspot to make it follow this point on the surface even
209+
* as the model animates. If the mesh is not hit, the result is null.
210+
*/
211+
surfaceFromPoint(pixelX: number, pixelY: number): string|null {
212+
const scene = this[$scene];
213+
const ndcPosition = scene.getNDC(pixelX, pixelY);
214+
215+
return scene.surfaceFromPoint(ndcPosition);
216+
}
217+
206218
private[$addHotspot](node: Node) {
207219
if (!(node instanceof HTMLElement &&
208220
node.slot.indexOf('hotspot') === 0)) {
@@ -218,6 +230,7 @@ export const AnnotationMixin = <T extends Constructor<ModelViewerElementBase>>(
218230
name: node.slot,
219231
position: node.dataset.position,
220232
normal: node.dataset.normal,
233+
surface: node.dataset.surface,
221234
});
222235
this[$hotspotMap].set(node.slot, hotspot);
223236
this[$scene].addHotspot(hotspot);

packages/model-viewer/src/features/scene-graph.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,11 +276,18 @@ export const SceneGraphMixin = <T extends Constructor<ModelViewerElementBase>>(
276276
}
277277

278278
materialFromPoint(pixelX: number, pixelY: number): Material|null {
279+
const model = this[$model];
280+
if (model == null) {
281+
return null;
282+
}
279283
const scene = this[$scene];
280284
const ndcCoords = scene.getNDC(pixelX, pixelY);
281-
scene.raycaster.setFromCamera(ndcCoords, scene.getCamera());
285+
const hit = scene.hitFromPoint(ndcCoords);
286+
if (hit == null || hit.face == null) {
287+
return null;
288+
}
282289

283-
return this[$model]![$materialFromPoint](scene.raycaster);
290+
return model[$materialFromPoint](hit);
284291
}
285292
}
286293

packages/model-viewer/src/features/scene-graph/model.ts

Lines changed: 27 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@
1313
* limitations under the License.
1414
*/
1515

16-
import {Group, Intersection, Material as ThreeMaterial, Mesh, MeshStandardMaterial, Object3D, Raycaster} from 'three';
16+
import {Intersection, Material as ThreeMaterial, Mesh, MeshStandardMaterial, Object3D} from 'three';
1717

1818
import {CorrelatedSceneGraph, GLTFElementToThreeObjectMap, ThreeObjectSet} from '../../three-components/gltf-instance/correlated-scene-graph.js';
1919
import {GLTF, GLTFElement, Material as GLTFMaterial} from '../../three-components/gltf-instance/gltf-2.0.js';
2020

2121
import {Model as ModelInterface} from './api.js';
2222
import {$setActive, $variantSet, Material} from './material.js';
23-
import {$children, Node, PrimitiveNode} from './nodes/primitive-node.js';
23+
import {Node, PrimitiveNode} from './nodes/primitive-node.js';
2424
import {$correlatedObjects, $sourceObject} from './three-dom-element.js';
2525

2626

@@ -33,9 +33,9 @@ export const $loadVariant = Symbol('loadVariant');
3333
export const $correlatedSceneGraph = Symbol('correlatedSceneGraph');
3434
export const $prepareVariantsForExport = Symbol('prepareVariantsForExport');
3535
export const $switchVariant = Symbol('switchVariant');
36-
export const $threeScene = Symbol('threeScene');
37-
export const $materialsFromPoint = Symbol('materialsFromPoint');
3836
export const $materialFromPoint = Symbol('materialFromPoint');
37+
export const $nodeFromPoint = Symbol('nodeFromPoint');
38+
export const $nodeFromIndex = Symbol('nodeFromIndex');
3939
export const $variantData = Symbol('variantData');
4040
export const $availableVariants = Symbol('availableVariants');
4141
const $modelOnUpdate = Symbol('modelOnUpdate');
@@ -78,7 +78,6 @@ export class Model implements ModelInterface {
7878
private[$hierarchy] = new Array<Node>();
7979
private[$roots] = new Array<Node>();
8080
private[$primitivesList] = new Array<PrimitiveNode>();
81-
private[$threeScene]: Object3D|Group;
8281
private[$modelOnUpdate]: () => void = () => {};
8382
private[$correlatedSceneGraph]: CorrelatedSceneGraph;
8483
private[$variantData] = new Map<string, VariantData>();
@@ -89,7 +88,6 @@ export class Model implements ModelInterface {
8988
this[$modelOnUpdate] = onUpdate;
9089
this[$correlatedSceneGraph] = correlatedSceneGraph;
9190
const {gltf, threeGLTF, gltfElementMap} = correlatedSceneGraph;
92-
this[$threeScene] = threeGLTF.scene;
9391

9492
for (const [i, material] of gltf.materials!.entries()) {
9593
const correlatedMaterial =
@@ -168,7 +166,7 @@ export class Model implements ModelInterface {
168166

169167
const parent: Node|undefined = parentMap.get(object);
170168
if (parent != null) {
171-
parent[$children].push(node);
169+
parent.children.push(node);
172170
} else {
173171
this[$roots].push(node);
174172
}
@@ -213,46 +211,37 @@ export class Model implements ModelInterface {
213211
return null;
214212
}
215213

216-
217-
/**
218-
* Intersects a ray with the Model and returns a list of materials whose
219-
* objects were intersected.
220-
*/
221-
[$materialsFromPoint](raycaster: Raycaster): Material[] {
222-
const hits = raycaster.intersectObject(this[$threeScene], true);
223-
224-
// Map the object hits to primitives and then to the active material of
225-
// the primitive.
226-
return hits.map((hit: Intersection<Object3D>) => {
227-
const found = this[$hierarchy].find((node: Node) => {
228-
if (node instanceof PrimitiveNode) {
229-
const primitive = node as PrimitiveNode;
230-
if (primitive.mesh === hit.object) {
231-
return true;
232-
}
214+
[$nodeFromIndex](mesh: number, primitive: number): PrimitiveNode|null {
215+
const found = this[$hierarchy].find((node: Node) => {
216+
if (node instanceof PrimitiveNode) {
217+
const {meshes, primitives} = node.mesh.userData.associations;
218+
if (meshes == mesh && primitives == primitive) {
219+
return true;
233220
}
234-
return false;
235-
}) as PrimitiveNode;
221+
}
222+
return false;
223+
});
224+
return found == null ? null : found as PrimitiveNode;
225+
}
236226

237-
if (found != null) {
238-
return found.getActiveMaterial();
227+
[$nodeFromPoint](hit: Intersection<Object3D>): PrimitiveNode {
228+
return this[$hierarchy].find((node: Node) => {
229+
if (node instanceof PrimitiveNode) {
230+
const primitive = node as PrimitiveNode;
231+
if (primitive.mesh === hit.object) {
232+
return true;
233+
}
239234
}
240-
return null;
241-
}) as Material[];
235+
return false;
236+
}) as PrimitiveNode;
242237
}
243238

244239
/**
245240
* Intersects a ray with the Model and returns the first material whose
246241
* object was intersected.
247242
*/
248-
[$materialFromPoint](raycaster: Raycaster): Material|null {
249-
const materials = this[$materialsFromPoint](raycaster);
250-
251-
if (materials.length > 0) {
252-
return materials[0];
253-
}
254-
255-
return null;
243+
[$materialFromPoint](hit: Intersection<Object3D>): Material {
244+
return this[$nodeFromPoint](hit).getActiveMaterial();
256245
}
257246

258247
/**

0 commit comments

Comments
 (0)