UNPKG

cione-comp-lib

Version:

`$ yarn add cione-comp-lib` 或者 `$ npm i cione-comp-lib -S`

691 lines (690 loc) 23 kB
import { Vector3, Raycaster, Vector2, Scene, PerspectiveCamera, Object3D, Box3, Sphere, AnimationMixer, Euler, LoopPingPong, LoopRepeat, Matrix3, Triangle } from "../../../../_three@0.150.1@three/build/three.module.mjs"; import { CSS2DRenderer } from "../../../../_three@0.150.1@three/examples/jsm/renderers/CSS2DRenderer.mjs"; import { reduceVertices } from "../../../../_three@0.150.1@three/examples/jsm/utils/SceneUtils.mjs"; import { $currentGLTF, $originalGltfJson, $model } from "../features/scene-graph.mjs"; import { $nodeFromPoint, $nodeFromIndex } from "../features/scene-graph/model.mjs"; import { $renderer } from "../model-viewer-base.mjs"; import { normalizeUnit } from "../styles/conversions.mjs"; import { parseExpressions } from "../styles/parsers.mjs"; import { resolveDpr } from "../utilities.mjs"; import { Damper, SETTLING_TIME } from "./Damper.mjs"; import { Hotspot } from "./Hotspot.mjs"; import { Shadow } from "./Shadow.mjs"; /* @license * Copyright 2019 Google LLC. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an 'AS IS' BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const MIN_SHADOW_RATIO = 100; const view = new Vector3(); const target = new Vector3(); const normalWorld = new Vector3(); const raycaster = new Raycaster(); const vector3 = new Vector3(); const ndc = new Vector2(); class ModelScene extends Scene { constructor({ canvas, element, width, height }) { super(); this.annotationRenderer = new CSS2DRenderer(); this.schemaElement = document.createElement("script"); this.width = 1; this.height = 1; this.aspect = 1; this.scaleStep = 0; this.renderCount = 0; this.externalRenderer = null; this.camera = new PerspectiveCamera(45, 1, 0.1, 100); this.xrCamera = null; this.url = null; this.target = new Object3D(); this.animationNames = []; this.boundingBox = new Box3(); this.boundingSphere = new Sphere(); this.size = new Vector3(); this.idealAspect = 0; this.framedFoVDeg = 0; this.shadow = null; this.shadowIntensity = 0; this.shadowSoftness = 1; this.bakedShadows = /* @__PURE__ */ new Set(); this.exposure = 1; this.canScale = true; this.isDirty = false; this.goalTarget = new Vector3(); this.targetDamperX = new Damper(); this.targetDamperY = new Damper(); this.targetDamperZ = new Damper(); this._currentGLTF = null; this._model = null; this.cancelPendingSourceChange = null; this.animationsByName = /* @__PURE__ */ new Map(); this.currentAnimationAction = null; this.name = "ModelScene"; this.element = element; this.canvas = canvas; this.camera = new PerspectiveCamera(45, 1, 0.1, 100); this.camera.name = "MainCamera"; this.add(this.target); this.setSize(width, height); this.target.name = "Target"; this.mixer = new AnimationMixer(this.target); const { domElement } = this.annotationRenderer; const { style } = domElement; style.display = "none"; style.pointerEvents = "none"; style.position = "absolute"; style.top = "0"; this.element.shadowRoot.querySelector(".default").appendChild(domElement); this.schemaElement.setAttribute("type", "application/ld+json"); } get context() { return this.canvas.getContext("2d"); } getCamera() { return this.xrCamera != null ? this.xrCamera : this.camera; } queueRender() { this.isDirty = true; } shouldRender() { return this.isDirty; } hasRendered() { this.isDirty = false; } forceRescale() { this.scaleStep = -1; this.queueRender(); } async setObject(model) { this.reset(); this._model = model; this.target.add(model); await this.setupScene(); } async setSource(url, progressCallback = () => { }) { if (!url || url === this.url) { progressCallback(1); return; } this.reset(); this.url = url; if (this.externalRenderer != null) { const framingInfo = await this.externalRenderer.load(progressCallback); this.boundingSphere.radius = framingInfo.framedRadius; this.idealAspect = framingInfo.fieldOfViewAspect; return; } if (this.cancelPendingSourceChange != null) { this.cancelPendingSourceChange(); this.cancelPendingSourceChange = null; } let gltf; try { gltf = await new Promise(async (resolve, reject) => { this.cancelPendingSourceChange = () => reject(); try { const result = await this.element[$renderer].loader.load(url, this.element, progressCallback); resolve(result); } catch (error) { reject(error); } }); } catch (error) { if (error == null) { return; } throw error; } this.cancelPendingSourceChange = null; this.reset(); this.url = url; this._currentGLTF = gltf; if (gltf != null) { this._model = gltf.scene; this.target.add(gltf.scene); } const { animations } = gltf; const animationsByName = /* @__PURE__ */ new Map(); const animationNames = []; for (const animation of animations) { animationsByName.set(animation.name, animation); animationNames.push(animation.name); } this.animations = animations; this.animationsByName = animationsByName; this.animationNames = animationNames; await this.setupScene(); } async setupScene() { this.applyTransform(); this.updateBoundingBox(); await this.updateFraming(); this.updateShadow(); this.setShadowIntensity(this.shadowIntensity); } reset() { this.url = null; this.renderCount = 0; this.queueRender(); if (this.shadow != null) { this.shadow.setIntensity(0); } this.bakedShadows.clear(); const { _model } = this; if (_model != null) { _model.removeFromParent(); this._model = null; } const gltf = this._currentGLTF; if (gltf != null) { gltf.dispose(); this._currentGLTF = null; } if (this.currentAnimationAction != null) { this.currentAnimationAction.stop(); this.currentAnimationAction = null; } this.mixer.stopAllAction(); this.mixer.uncacheRoot(this); } dispose() { this.reset(); if (this.shadow != null) { this.shadow.dispose(); this.shadow = null; } this.element[$currentGLTF] = null; this.element[$originalGltfJson] = null; this.element[$model] = null; } get currentGLTF() { return this._currentGLTF; } setSize(width, height) { if (this.width === width && this.height === height) { return; } this.width = Math.max(width, 1); this.height = Math.max(height, 1); this.annotationRenderer.setSize(width, height); this.aspect = this.width / this.height; if (this.externalRenderer != null) { const dpr = resolveDpr(); this.externalRenderer.resize(width * dpr, height * dpr); } this.queueRender(); } markBakedShadow(mesh) { mesh.userData.shadow = true; this.bakedShadows.add(mesh); } unmarkBakedShadow(mesh) { mesh.userData.shadow = false; mesh.visible = true; this.bakedShadows.delete(mesh); this.boundingBox.expandByObject(mesh); } findBakedShadows(group) { const boundingBox = new Box3(); group.traverse((object) => { const mesh = object; if (!mesh.isMesh) { return; } const material = mesh.material; if (!material.transparent) { return; } boundingBox.setFromObject(mesh); const size = boundingBox.getSize(vector3); const minDim = Math.min(size.x, size.y, size.z); const maxDim = Math.max(size.x, size.y, size.z); if (maxDim < MIN_SHADOW_RATIO * minDim) { return; } this.markBakedShadow(mesh); }); } checkBakedShadows() { const { min, max } = this.boundingBox; const shadowBox = new Box3(); this.boundingBox.getSize(this.size); for (const mesh of this.bakedShadows) { shadowBox.setFromObject(mesh); if (shadowBox.min.y < min.y + this.size.y / MIN_SHADOW_RATIO && shadowBox.min.x <= min.x && shadowBox.max.x >= max.x && shadowBox.min.z <= min.z && shadowBox.max.z >= max.z) { continue; } if (shadowBox.min.z < min.z + this.size.z / MIN_SHADOW_RATIO && shadowBox.min.x <= min.x && shadowBox.max.x >= max.x && shadowBox.min.y <= min.y && shadowBox.max.y >= max.y) { continue; } this.unmarkBakedShadow(mesh); } } applyTransform() { const { model } = this; if (model == null) { return; } const orientation = parseExpressions(this.element.orientation)[0].terms; const roll = normalizeUnit(orientation[0]).number; const pitch = normalizeUnit(orientation[1]).number; const yaw = normalizeUnit(orientation[2]).number; model.quaternion.setFromEuler(new Euler(pitch, yaw, roll, "YXZ")); const scale = parseExpressions(this.element.scale)[0].terms; model.scale.set(scale[0].number, scale[1].number, scale[2].number); } updateBoundingBox() { const { model } = this; if (model == null) { return; } this.target.remove(model); this.findBakedShadows(model); const bound = (box, vertex) => { return box.expandByPoint(vertex); }; this.setBakedShadowVisibility(false); this.boundingBox = reduceVertices(model, bound, new Box3()); if (this.boundingBox.isEmpty()) { this.setBakedShadowVisibility(true); this.bakedShadows.forEach((mesh) => this.unmarkBakedShadow(mesh)); this.boundingBox = reduceVertices(model, bound, new Box3()); } this.checkBakedShadows(); this.setBakedShadowVisibility(); this.boundingBox.getSize(this.size); this.target.add(model); } async updateFraming() { const { model } = this; if (model == null) { return; } this.target.remove(model); this.setBakedShadowVisibility(false); const { center } = this.boundingSphere; this.element.requestUpdate("cameraTarget"); await this.element.updateComplete; center.copy(this.getTarget()); const radiusSquared = (value, vertex) => { return Math.max(value, center.distanceToSquared(vertex)); }; this.boundingSphere.radius = Math.sqrt(reduceVertices(model, radiusSquared, 0)); const horizontalTanFov = (value, vertex) => { vertex.sub(center); const radiusXZ = Math.sqrt(vertex.x * vertex.x + vertex.z * vertex.z); return Math.max(value, radiusXZ / (this.idealCameraDistance() - Math.abs(vertex.y))); }; this.idealAspect = reduceVertices(model, horizontalTanFov, 0) / Math.tan(this.framedFoVDeg / 2 * Math.PI / 180); this.setBakedShadowVisibility(); this.target.add(model); } setBakedShadowVisibility(visible = this.shadowIntensity <= 0) { for (const shadow of this.bakedShadows) { shadow.visible = visible; } } idealCameraDistance() { const halfFovRad = this.framedFoVDeg / 2 * Math.PI / 180; return this.boundingSphere.radius / Math.sin(halfFovRad); } adjustedFoV(fovDeg) { const vertical = Math.tan(fovDeg / 2 * Math.PI / 180) * Math.max(1, this.idealAspect / this.aspect); return 2 * Math.atan(vertical) * 180 / Math.PI; } getNDC(clientX, clientY) { if (this.xrCamera != null) { ndc.set(clientX / window.screen.width, clientY / window.screen.height); } else { const rect = this.element.getBoundingClientRect(); ndc.set((clientX - rect.x) / this.width, (clientY - rect.y) / this.height); } ndc.multiplyScalar(2).subScalar(1); ndc.y *= -1; return ndc; } getSize() { return { width: this.width, height: this.height }; } setEnvironmentAndSkybox(environment, skybox) { if (this.element[$renderer].arRenderer.presentedScene === this) { return; } this.environment = environment; this.background = skybox; this.queueRender(); } setTarget(modelX, modelY, modelZ) { this.goalTarget.set(-modelX, -modelY, -modelZ); } setTargetDamperDecayTime(decayMilliseconds) { this.targetDamperX.setDecayTime(decayMilliseconds); this.targetDamperY.setDecayTime(decayMilliseconds); this.targetDamperZ.setDecayTime(decayMilliseconds); } getTarget() { return this.goalTarget.clone().multiplyScalar(-1); } jumpToGoal() { this.updateTarget(SETTLING_TIME); } updateTarget(delta) { const goal = this.goalTarget; const target2 = this.target.position; if (!goal.equals(target2)) { const normalization = this.boundingSphere.radius / 10; let { x, y, z } = target2; x = this.targetDamperX.update(x, goal.x, delta, normalization); y = this.targetDamperY.update(y, goal.y, delta, normalization); z = this.targetDamperZ.update(z, goal.z, delta, normalization); this.target.position.set(x, y, z); this.target.updateMatrixWorld(); this.queueRender(); return true; } else { return false; } } pointTowards(worldX, worldZ) { const { x, z } = this.position; this.yaw = Math.atan2(worldX - x, worldZ - z); } get model() { return this._model; } set yaw(radiansY) { this.rotation.y = radiansY; this.queueRender(); } get yaw() { return this.rotation.y; } set animationTime(value) { this.mixer.setTime(value); this.queueShadowRender(); } get animationTime() { if (this.currentAnimationAction != null) { const loopCount = Math.max(this.currentAnimationAction._loopCount, 0); if (this.currentAnimationAction.loop === LoopPingPong && (loopCount & 1) === 1) { return this.duration - this.currentAnimationAction.time; } else { return this.currentAnimationAction.time; } } return 0; } set animationTimeScale(value) { this.mixer.timeScale = value; } get animationTimeScale() { return this.mixer.timeScale; } get duration() { if (this.currentAnimationAction != null && this.currentAnimationAction.getClip()) { return this.currentAnimationAction.getClip().duration; } return 0; } get hasActiveAnimation() { return this.currentAnimationAction != null; } playAnimation(name = null, crossfadeTime = 0, loopMode = LoopRepeat, repetitionCount = Infinity) { if (this._currentGLTF == null) { return; } const { animations } = this; if (animations == null || animations.length === 0) { console.warn(`Cannot play animation (model does not have any animations)`); return; } let animationClip = null; if (name != null) { animationClip = this.animationsByName.get(name); if (animationClip == null) { const parsedAnimationIndex = parseInt(name); if (!isNaN(parsedAnimationIndex) && parsedAnimationIndex >= 0 && parsedAnimationIndex < animations.length) { animationClip = animations[parsedAnimationIndex]; } } } if (animationClip == null) { animationClip = animations[0]; } try { const { currentAnimationAction: lastAnimationAction } = this; const action = this.mixer.clipAction(animationClip, this); this.currentAnimationAction = action; if (this.element.paused) { this.mixer.stopAllAction(); } else { action.paused = false; if (lastAnimationAction != null && action !== lastAnimationAction) { action.crossFadeFrom(lastAnimationAction, crossfadeTime, false); } else if (this.animationTimeScale > 0 && this.animationTime == this.duration) { this.animationTime = 0; } } action.setLoop(loopMode, repetitionCount); action.enabled = true; action.clampWhenFinished = true; action.play(); } catch (error) { console.error(error); } } stopAnimation() { this.currentAnimationAction = null; this.mixer.stopAllAction(); } updateAnimation(step) { this.mixer.update(step); this.queueShadowRender(); } subscribeMixerEvent(event, callback) { this.mixer.addEventListener(event, callback); } updateShadow() { const shadow = this.shadow; if (shadow != null) { const side = this.element.arPlacement === "wall" ? "back" : "bottom"; shadow.setScene(this, this.shadowSoftness, side); shadow.needsUpdate = true; } } renderShadow(renderer) { const shadow = this.shadow; if (shadow != null && shadow.needsUpdate == true) { shadow.render(renderer, this); shadow.needsUpdate = false; } } queueShadowRender() { if (this.shadow != null) { this.shadow.needsUpdate = true; } } setShadowIntensity(shadowIntensity) { this.shadowIntensity = shadowIntensity; if (this._currentGLTF == null) { return; } this.setBakedShadowVisibility(); if (shadowIntensity <= 0 && this.shadow == null) { return; } if (this.shadow == null) { const side = this.element.arPlacement === "wall" ? "back" : "bottom"; this.shadow = new Shadow(this, this.shadowSoftness, side); } this.shadow.setIntensity(shadowIntensity); } setShadowSoftness(softness) { this.shadowSoftness = softness; const shadow = this.shadow; if (shadow != null) { shadow.setSoftness(softness); } } setShadowOffset(offset) { const shadow = this.shadow; if (shadow != null) { shadow.setOffset(offset); } } get raycaster() { return raycaster; } hitFromPoint(ndcPosition, object = this) { this.raycaster.setFromCamera(ndcPosition, this.getCamera()); const hits = this.raycaster.intersectObject(object, true); return hits.find((hit) => hit.object.visible && !hit.object.userData.shadow); } positionAndNormalFromPoint(ndcPosition, object = this) { const hit = this.hitFromPoint(ndcPosition, object); if (hit == null || hit.face == null) { return null; } if (hit.uv == null) { return { position: hit.point, normal: hit.face.normal, uv: null }; } hit.face.normal.applyNormalMatrix(new Matrix3().getNormalMatrix(hit.object.matrixWorld)); return { position: hit.point, normal: hit.face.normal, uv: hit.uv }; } surfaceFromPoint(ndcPosition, object = this) { const model = this.element.model; if (model == null) { return null; } const hit = this.hitFromPoint(ndcPosition, object); if (hit == null || hit.face == null) { return null; } const node = model[$nodeFromPoint](hit); const { meshes, primitives } = node.mesh.userData.associations; const va = new Vector3(); const vb = new Vector3(); const vc = new Vector3(); const { a, b, c } = hit.face; const mesh = hit.object; mesh.getVertexPosition(a, va); mesh.getVertexPosition(b, vb); mesh.getVertexPosition(c, vc); const tri = new Triangle(va, vb, vc); const uvw = new Vector3(); tri.getBarycoord(mesh.worldToLocal(hit.point), uvw); return `${meshes} ${primitives} ${a} ${b} ${c} ${uvw.x.toFixed(3)} ${uvw.y.toFixed(3)} ${uvw.z.toFixed(3)}`; } addHotspot(hotspot) { this.target.add(hotspot); this.annotationRenderer.domElement.appendChild(hotspot.element); } removeHotspot(hotspot) { this.target.remove(hotspot); } forHotspots(func) { const { children } = this.target; for (let i = 0, l = children.length; i < l; i++) { const hotspot = children[i]; if (hotspot instanceof Hotspot) { func(hotspot); } } } initializeSurface(hotspot) { if (hotspot.surface != null && hotspot.mesh == null) { const nodes = parseExpressions(hotspot.surface)[0].terms; if (nodes.length != 8) { console.warn(hotspot.surface + " does not have exactly 8 numbers."); return; } const primitiveNode = this.element.model[$nodeFromIndex](nodes[0].number, nodes[1].number); const tri = new Vector3(nodes[2].number, nodes[3].number, nodes[4].number); if (primitiveNode == null) { console.warn(hotspot.surface + " does not match a node/primitive in this glTF! Skipping this hotspot."); return; } const numVert = primitiveNode.mesh.geometry.attributes.position.count; if (tri.x >= numVert || tri.y >= numVert || tri.z >= numVert) { console.warn(hotspot.surface + " vertex indices out of range in this glTF! Skipping this hotspot."); return; } const bary = new Vector3(nodes[5].number, nodes[6].number, nodes[7].number); hotspot.mesh = primitiveNode.mesh; hotspot.tri = tri; hotspot.bary = bary; } } updateSurfaceHotspots() { const forceUpdate = !this.element.paused; this.forHotspots((hotspot) => { this.initializeSurface(hotspot); hotspot.updateSurface(forceUpdate); }); } updateHotspotsVisibility(viewerPosition) { this.forHotspots((hotspot) => { view.copy(viewerPosition); target.setFromMatrixPosition(hotspot.matrixWorld); view.sub(target); normalWorld.copy(hotspot.normal).transformDirection(this.target.matrixWorld); if (view.dot(normalWorld) < 0) { hotspot.hide(); } else { hotspot.show(); } }); } orientHotspots(radians) { this.forHotspots((hotspot) => { hotspot.orient(radians); }); } setHotspotsVisibility(visible) { this.forHotspots((hotspot) => { hotspot.visible = visible; }); } updateSchema(src) { var _a; const { schemaElement, element } = this; const { alt, poster, iosSrc } = element; if (src != null) { const encoding = [{ "@type": "MediaObject", contentUrl: src, encodingFormat: ((_a = src.split(".").pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === "gltf" ? "model/gltf+json" : "model/gltf-binary" }]; if (iosSrc) { encoding.push({ "@type": "MediaObject", contentUrl: iosSrc, encodingFormat: "model/vnd.usdz+zip" }); } const structuredData = { "@context": "http://schema.org/", "@type": "3DModel", image: poster !== null && poster !== void 0 ? poster : void 0, name: alt !== null && alt !== void 0 ? alt : void 0, encoding }; schemaElement.textContent = JSON.stringify(structuredData); document.head.appendChild(schemaElement); } else if (schemaElement.parentElement != null) { schemaElement.parentElement.removeChild(schemaElement); } } } export { ModelScene };