UNPKG

@google/model-viewer

Version:

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

257 lines 10.1 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, Mesh, Object3D, PerspectiveCamera, Scene, ShaderLib, ShaderMaterial, Vector3 } from 'three'; import { $needsRender, $renderer } from '../model-viewer-base.js'; import { resolveDpr } from '../utilities.js'; import Model, { DEFAULT_FOV_DEG } from './Model.js'; import { cubeUVChunk } from './shader-chunk/cube_uv_reflection_fragment.glsl.js'; import { Shadow } from './Shadow.js'; export const IlluminationRole = { Primary: 'primary', Secondary: 'secondary' }; const DEFAULT_TAN_FOV = Math.tan((DEFAULT_FOV_DEG / 2) * Math.PI / 180); 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 class ModelScene extends Scene { constructor({ canvas, element, width, height }) { super(); this[_a] = false; this.aspect = 1; this.shadow = null; this.shadowIntensity = 0; this.shadowSoftness = 1; this.width = 1; this.height = 1; this.isVisible = false; this.isDirty = false; this.exposure = 1; this.framedFieldOfView = DEFAULT_FOV_DEG; // 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.model = new Model(); // 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; this.frameModel(); // 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.element[$renderer].render(performance.now()); }); } } /** * Set's the framedFieldOfView based on the aspect ratio of the window in * order to keep the model fully visible at any camera orientation. */ frameModel() { const vertical = DEFAULT_TAN_FOV * Math.max(1, this.model.fieldOfViewAspect / this.aspect); this.framedFieldOfView = 2 * Math.atan(vertical) * 180 / Math.PI; } /** * Returns the size of the corresponding canvas element. */ getSize() { return { width: this.width, height: this.height }; } /** * 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; if (this.shadow != null) { this.shadow.setRotation(radiansY); } } /** * 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.frameModel(); this.setShadowIntensity(this.shadowIntensity); if (this.shadow != null) { this.shadow.setModel(this.model, this.shadowSoftness); } // Uncomment if using showShadowHelper below // if (this.children.length > 1) { // (this.children[1] as CameraHelper).update(); // } this.element[$needsRender](); this.dispatchEvent({ type: 'model-load', url: event.url }); } /** * Sets the shadow's intensity, lazily creating the shadow as necessary. */ setShadowIntensity(shadowIntensity) { this.shadowIntensity = shadowIntensity; if (shadowIntensity > 0 && this.model.hasModel()) { if (this.shadow == null) { this.shadow = new Shadow(this.model, this.pivot, this.shadowSoftness); this.pivot.add(this.shadow); // showShadowHelper(this); } this.shadow.setIntensity(shadowIntensity); } } /** * Sets the shadow's softness by mapping a [0, 1] softness parameter to the * shadow's resolution. This involves reallocation, so it should not be * changed frequently. Softer shadows are cheaper to render. */ setShadowSoftness(softness) { this.shadowSoftness = softness; if (this.shadow != null) { this.shadow.setSoftness(softness); } } createSkyboxMesh() { const geometry = new BoxBufferGeometry(1, 1, 1); geometry.deleteAttribute('normal'); geometry.deleteAttribute('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