UNPKG

@google/model-viewer

Version:

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

166 lines (136 loc) 5.54 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. */ import {Mesh, MeshBasicMaterial, OrthographicCamera, PlaneGeometry, RGBAFormat, Vector3, WebGLRenderTarget} from 'three'; import {WebGLRenderer} from 'three'; import ModelScene from './ModelScene'; export interface ShadowGenerationConfig { textureWidth?: number; textureHeight?: number; } const $camera = Symbol('camera'); const $renderTarget = Symbol('renderTarget'); const BASE_SHADOW_OPACITY = 0.1; const DEFAULT_CONFIG: ShadowGenerationConfig = { textureWidth: 512, textureHeight: 512, }; const shadowGeneratorMaterial = new MeshBasicMaterial({ color: 0x000000, }); const shadowTextureMaterial = new MeshBasicMaterial({ transparent: true, opacity: BASE_SHADOW_OPACITY, }); /** * Creates a mesh that can receive and render pseudo-shadows * only updated when calling its render method. This is different * from non-auto-updating shadows because the resulting material * applied to the mesh is disconnected from the renderer's shadow map * and can be freely rotated and positioned like a regular texture. */ export default class StaticShadow extends Mesh { private[$renderTarget]: WebGLRenderTarget; private[$camera]: OrthographicCamera; /** * Create a shadow mesh. */ constructor() { const geometry = new PlaneGeometry(1, 1); geometry.rotateX(-Math.PI / 2); super(geometry, shadowTextureMaterial.clone()); this.name = 'StaticShadow'; this[$renderTarget] = new WebGLRenderTarget( DEFAULT_CONFIG.textureWidth!, DEFAULT_CONFIG.textureHeight!, { format: RGBAFormat, }); (this.material as any).map = this[$renderTarget].texture; (this.material as any).needsUpdate = true; this[$camera] = new OrthographicCamera(-1, 1, 1, -1); } get intensity(): number { return (this.material as any).opacity / BASE_SHADOW_OPACITY; } set intensity(intensity: number) { const intensityIsNumber = typeof intensity === 'number' && !(self as any).isNaN(intensity); (this.material as any).opacity = BASE_SHADOW_OPACITY * (intensityIsNumber ? intensity : 0.0); this.visible = (this.material as any).opacity > 0; } /** * Updates the generated static shadow. The size of the camera is dependent * on the current scale of the StaticShadow that will host the texture. * It's expected for the StaticShadow to be facing the light source. */ render( renderer: WebGLRenderer, scene: ModelScene, config: ShadowGenerationConfig = {}) { const userSceneOverrideMaterial = scene.overrideMaterial; const userSceneBackground = scene.background; const userClearAlpha = renderer.getClearAlpha(); const userRenderTarget = renderer.getRenderTarget(); const shadowParent = this.parent; config = Object.assign({}, config, DEFAULT_CONFIG); renderer.setClearAlpha(0); scene.overrideMaterial = shadowGeneratorMaterial; scene.background = null; // Update render target size if necessary if (this[$renderTarget].width !== config.textureWidth || this[$renderTarget].height !== config.textureHeight) { this[$renderTarget].setSize(config.textureWidth!, config.textureHeight!); } const {boundingBox, size} = scene.model; const modelCenter = boundingBox.getCenter(new Vector3); // Nothing within shadowOffset of the bottom of the model casts a shadow // (this is to avoid having a baked-in shadow plane cast its own shadow). const shadowOffset = size.y * 0.002; this[$camera].position.x = modelCenter.x; this[$camera].position.z = modelCenter.z; this[$camera].position.y = boundingBox.max.y + shadowOffset; this[$camera].lookAt(modelCenter); this[$camera].updateMatrixWorld(true); this.scale.x = size.x; this.scale.z = size.z; this.position.x = modelCenter.x; this.position.z = modelCenter.z; // We keep our shadow from Z-fighting with a baked-in shadow by lowering it // by shadowOffset. this.position.y = boundingBox.min.y - shadowOffset; this[$camera].top = size.z / 2; this[$camera].bottom = size.z / -2; this[$camera].left = size.x / -2; this[$camera].right = size.x / 2; this[$camera].near = 0; this[$camera].far = size.y; this[$camera].updateProjectionMatrix(); // There's a chance the shadow will be in the scene that's being rerendered; // temporarily remove it incase. if (shadowParent) { shadowParent.remove(this); } renderer.setRenderTarget(this[$renderTarget]); renderer.clear(); renderer.render(scene, this[$camera]); if (shadowParent) { shadowParent.add(this); } (this.material as any).needsUpdate = true; // Reset the values on the renderer and scene scene.overrideMaterial = userSceneOverrideMaterial; scene.background = userSceneBackground; renderer.setClearAlpha(userClearAlpha); renderer.setRenderTarget(userRenderTarget); } }