@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
166 lines (136 loc) • 5.54 kB
text/typescript
/* @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);
}
}