UNPKG

@google/model-viewer

Version:

Easily display interactive 3D models on the web and in AR!

231 lines 9.02 kB
/* @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. */ var _a; import { BackSide, BoxBufferGeometry, Color, Object3D, PerspectiveCamera, Scene, ShaderLib, ShaderMaterial, Vector3 } from 'three'; import { Mesh } from 'three'; import { resolveDpr } from '../utilities.js'; import Model from './Model.js'; import { cubeUVChunk } from './shader-chunk/cube_uv_reflection_fragment.glsl.js'; import StaticShadow from './StaticShadow.js'; export const IlluminationRole = { Primary: 'primary', Secondary: 'secondary' }; const $paused = Symbol('paused'); /** * A THREE.Scene object that takes a Model and CanvasHTMLElement and * constructs a framed scene based off of the canvas dimensions. * Provides lights and cameras to be used in a renderer. */ export default class ModelScene extends Scene { constructor({ canvas, element, width, height, renderer }) { super(); this[_a] = false; this.aspect = 1; this.width = 1; this.height = 1; this.isVisible = false; this.isDirty = false; this.exposure = 1; // These default camera values are never used, as they are reset once the // model is loaded and framing is computed. this.camera = new PerspectiveCamera(45, 1, 0.1, 100); this.name = 'ModelScene'; this.element = element; this.canvas = canvas; this.context = canvas.getContext('2d'); this.renderer = renderer; this.model = new Model(); this.shadow = new StaticShadow(); // These default camera values are never used, as they are reset once the // model is loaded and framing is computed. this.camera = new PerspectiveCamera(45, 1, 0.1, 100); this.camera.name = 'MainCamera'; this.activeCamera = this.camera; this.pivot = new Object3D(); this.pivot.name = 'Pivot'; this.pivotCenter = new Vector3; this.skyboxMesh = this.createSkyboxMesh(); this.add(this.pivot); this.pivot.add(this.model); this.setSize(width, height); this.background = new Color(0xffffff); this.model.addEventListener('model-load', (event) => this.onModelLoad(event)); } get paused() { return this[$paused]; } pause() { this[$paused] = true; } resume() { this[$paused] = false; } /** * Sets the model via URL. */ async setModelSource(source, progressCallback) { try { await this.model.setSource(source, progressCallback); } catch (e) { throw new Error(`Could not set model source to '${source}': ${e.message}`); } } /** * Receives the size of the 2D canvas element to make according * adjustments in the scene. */ setSize(width, height) { if (width !== this.width || height !== this.height) { this.width = Math.max(width, 1); this.height = Math.max(height, 1); // In practice, invocations of setSize are throttled at the element level, // so no need to throttle here: const dpr = resolveDpr(); this.canvas.width = this.width * dpr; this.canvas.height = this.height * dpr; this.canvas.style.width = `${this.width}px`; this.canvas.style.height = `${this.height}px`; this.aspect = this.width / this.height; // Immediately queue a render to happen at microtask timing. This is // necessary because setting the width and height of the canvas has the // side-effect of clearing it, and also if we wait for the next rAF to // render again we might get hit with yet-another-resize, or worse we // may not actually be marked as dirty and so render will just not // happen. Queuing a render to happen here means we will render twice on // a resize frame, but it avoids most of the visual artifacts associated // with other potential mitigations for this problem. See discussion in // https://github.com/GoogleWebComponents/model-viewer/pull/619 for // additional considerations. Promise.resolve().then(() => { this.renderer.render(performance.now()); }); } } /** * Returns the size of the corresponding canvas element. */ getSize() { return { width: this.width, height: this.height }; } resetModelPose() { this.model.position.set(0, 0, 0); this.model.rotation.set(0, 0, 0); this.model.scale.set(1, 1, 1); } /** * Returns the current camera. */ getCamera() { return this.activeCamera; } /** * Sets the passed in camera to be used for rendering. */ setCamera(camera) { this.activeCamera = camera; } /** * Sets the rotation of the model's pivot, around its pivotCenter point. */ setPivotRotation(radiansY) { this.pivot.rotation.y = radiansY; this.pivot.position.x = -this.pivotCenter.x; this.pivot.position.z = -this.pivotCenter.z; this.pivot.position.applyAxisAngle(this.pivot.up, radiansY); this.pivot.position.x += this.pivotCenter.x; this.pivot.position.z += this.pivotCenter.z; } /** * Gets the current rotation value of the pivot */ getPivotRotation() { return this.pivot.rotation.y; } /** * Called when the model's contents have loaded, or changed. */ onModelLoad(event) { this.updateStaticShadow(); this.dispatchEvent({ type: 'model-load', url: event.url }); } /** * Called to update the shadow rendering when the model changes. */ updateStaticShadow() { if (!this.model.hasModel() || this.model.size.length() === 0) { this.pivot.remove(this.shadow); return; } // Remove and cache the current pivot rotation so that the shadow's // capture is unrotated so it can be freely rotated when applied // as a texture. const currentRotation = this.pivot.rotation.y; this.setPivotRotation(0); this.shadow.render(this.renderer.renderer, this); // Lazily add the shadow so we're only displaying it once it has // a generated texture. this.pivot.add(this.shadow); this.setPivotRotation(currentRotation); } createSkyboxMesh() { const geometry = new BoxBufferGeometry(1, 1, 1); geometry.removeAttribute('normal'); geometry.removeAttribute('uv'); const material = new ShaderMaterial({ uniforms: { envMap: { value: null }, opacity: { value: 1.0 } }, vertexShader: ShaderLib.cube.vertexShader, fragmentShader: ShaderLib.cube.fragmentShader, side: BackSide, // Turn off the depth buffer so that even a small box still ends up // enclosing a scene of any size. depthTest: false, depthWrite: false, fog: false, }); material.extensions = { derivatives: true, fragDepth: false, drawBuffers: false, shaderTextureLOD: false }; const samplerUV = ` #define ENVMAP_TYPE_CUBE_UV #define PI 3.14159265359 ${cubeUVChunk} uniform sampler2D envMap; `; material.onBeforeCompile = (shader) => { shader.fragmentShader = shader.fragmentShader.replace('uniform samplerCube tCube;', samplerUV) .replace('vec4 texColor = textureCube( tCube, vec3( tFlip * vWorldDirection.x, vWorldDirection.yz ) );', 'gl_FragColor = textureCubeUV( envMap, vWorldDirection, 0.0 );') .replace('gl_FragColor = mapTexelToLinear( texColor );', ''); }; const skyboxMesh = new Mesh(geometry, material); skyboxMesh.frustumCulled = false; // This centers the box on the camera, ensuring the view is not affected by // the camera's motion, which makes it appear inifitely large, as it should. skyboxMesh.onBeforeRender = function (_renderer, _scene, camera) { this.matrixWorld.copyPosition(camera.matrixWorld); }; return skyboxMesh; } skyboxMaterial() { return this.skyboxMesh.material; } } _a = $paused; //# sourceMappingURL=ModelScene.js.map