UNPKG

@google/model-viewer

Version:

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

287 lines 12.4 kB
/* @license * Copyright 2022 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 { BackSide, DoubleSide, Box3, Mesh, MeshBasicMaterial, MeshDepthMaterial, Object3D, OrthographicCamera, PlaneGeometry, RGBAFormat, ShaderMaterial, Vector3, WebGLRenderTarget } from 'three'; import { HorizontalBlurShader } from 'three/examples/jsm/shaders/HorizontalBlurShader.js'; import { VerticalBlurShader } from 'three/examples/jsm/shaders/VerticalBlurShader.js'; import { lerp } from 'three/src/math/MathUtils.js'; // The softness [0, 1] of the shadow is mapped to a resolution between // 2^LOG_MAX_RESOLUTION and 2^LOG_MIN_RESOLUTION. const LOG_MAX_RESOLUTION = 9; const LOG_MIN_RESOLUTION = 6; // Animated models are not in general contained in their bounding box, as this // is calculated only for their resting pose. We create a cubic shadow volume // for animated models sized to their largest bounding box dimension multiplied // by this scale factor. const ANIMATION_SCALING = 2; // Since hard shadows are not lightened by blurring and depth, set a lower // default intensity to make them more perceptually similar to the intensity of // the soft shadows. const DEFAULT_HARD_INTENSITY = 0.3; /** * The Shadow class creates a shadow that fits a given scene and follows a * target. This shadow will follow the scene without any updates needed so long * as the shadow and scene are both parented to the same object (call it the * scene) and this scene is passed as the target parameter to the shadow's * constructor. We also must constrain the scene to motion within the horizontal * plane and call the setRotation() method whenever the scene's Y-axis rotation * changes. For motion outside of the horizontal plane, this.needsUpdate must be * set to true. * * The softness of the shadow is controlled by changing its resolution, making * softer shadows faster, but less precise. */ export class Shadow extends Object3D { constructor(scene, softness, side) { super(); this.camera = new OrthographicCamera(); // private cameraHelper = new CameraHelper(this.camera); this.renderTarget = null; this.renderTargetBlur = null; this.depthMaterial = new MeshDepthMaterial(); this.horizontalBlurMaterial = new ShaderMaterial(HorizontalBlurShader); this.verticalBlurMaterial = new ShaderMaterial(VerticalBlurShader); this.intensity = 0; this.softness = 1; this.boundingBox = new Box3; this.size = new Vector3; this.maxDimension = 0; this.isAnimated = false; this.needsUpdate = false; const { camera } = this; camera.rotation.x = Math.PI / 2; camera.left = -0.5; camera.right = 0.5; camera.bottom = -0.5; camera.top = 0.5; this.add(camera); // this.add(this.cameraHelper); // this.cameraHelper.updateMatrixWorld = function() { // this.matrixWorld = this.camera.matrixWorld; // }; const plane = new PlaneGeometry(); const shadowMaterial = new MeshBasicMaterial({ // color: new Color(1, 0, 0), opacity: 1, transparent: true, side: BackSide, }); this.floor = new Mesh(plane, shadowMaterial); this.floor.userData.noHit = true; camera.add(this.floor); // the plane onto which to blur the texture this.blurPlane = new Mesh(plane); this.blurPlane.visible = false; camera.add(this.blurPlane); scene.target.add(this); // like MeshDepthMaterial, but goes from black to transparent this.depthMaterial.onBeforeCompile = function (shader) { shader.fragmentShader = shader.fragmentShader.replace('gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );', 'gl_FragColor = vec4( vec3( 0.0 ), ( 1.0 - fragCoordZ ) * opacity );'); }; // Render both sides, back sides face the light source and // front sides supply depth information for soft shadows this.depthMaterial.side = DoubleSide; this.horizontalBlurMaterial.depthTest = false; this.verticalBlurMaterial.depthTest = false; this.setScene(scene, softness, side); } /** * Update the shadow's size and position for a new scene. Softness is also * needed, as this controls the shadow's resolution. */ setScene(scene, softness, side) { const { boundingBox, size, rotation, position } = this; this.isAnimated = scene.animationNames.length > 0; this.boundingBox.copy(scene.boundingBox); this.size.copy(scene.size); this.maxDimension = Math.max(size.x, size.y, size.z) * (this.isAnimated ? ANIMATION_SCALING : 1); this.boundingBox.getCenter(position); if (side === 'back') { const { min, max } = boundingBox; [min.y, min.z] = [min.z, min.y]; [max.y, max.z] = [max.z, max.y]; [size.y, size.z] = [size.z, size.y]; rotation.x = Math.PI / 2; rotation.y = Math.PI; } else { rotation.x = 0; rotation.y = 0; } if (this.isAnimated) { const minY = boundingBox.min.y; const maxY = boundingBox.max.y; size.y = this.maxDimension; boundingBox.expandByVector(size.subScalar(this.maxDimension).multiplyScalar(-0.5)); boundingBox.min.y = minY; boundingBox.max.y = maxY; size.set(this.maxDimension, maxY - minY, this.maxDimension); } if (side === 'bottom') { position.y = boundingBox.min.y; } else { position.z = boundingBox.min.y; } this.setSoftness(softness); } /** * Update the shadow's resolution based on softness (between 0 and 1). Should * not be called frequently, as this results in reallocation. */ setSoftness(softness) { this.softness = softness; const { size, camera } = this; const scaleY = (this.isAnimated ? ANIMATION_SCALING : 1); const resolution = scaleY * Math.pow(2, LOG_MAX_RESOLUTION - softness * (LOG_MAX_RESOLUTION - LOG_MIN_RESOLUTION)); this.setMapSize(resolution); const softFar = size.y / 2; const hardFar = size.y * scaleY; camera.near = 0; camera.far = lerp(hardFar, softFar, softness); // we have co-opted opacity to scale the depth to clip this.depthMaterial.opacity = 1.0 / softness; camera.updateProjectionMatrix(); // this.cameraHelper.update(); this.setIntensity(this.intensity); this.setOffset(0); } /** * Lower-level version of the above function. */ setMapSize(maxMapSize) { const { size } = this; if (this.isAnimated) { maxMapSize *= ANIMATION_SCALING; } const baseWidth = Math.floor(size.x > size.z ? maxMapSize : maxMapSize * size.x / size.z); const baseHeight = Math.floor(size.x > size.z ? maxMapSize * size.z / size.x : maxMapSize); // width of blur filter in pixels (not adjustable) const TAP_WIDTH = 10; const width = TAP_WIDTH + baseWidth; const height = TAP_WIDTH + baseHeight; if (this.renderTarget != null && (this.renderTarget.width !== width || this.renderTarget.height !== height)) { this.renderTarget.dispose(); this.renderTarget = null; this.renderTargetBlur.dispose(); this.renderTargetBlur = null; } if (this.renderTarget == null) { const params = { format: RGBAFormat }; this.renderTarget = new WebGLRenderTarget(width, height, params); this.renderTargetBlur = new WebGLRenderTarget(width, height, params); this.floor.material.map = this.renderTarget.texture; } // These pads account for the softening radius around the shadow. this.camera.scale.set(size.x * (1 + TAP_WIDTH / baseWidth), size.z * (1 + TAP_WIDTH / baseHeight), 1); this.needsUpdate = true; } /** * Set the shadow's intensity (0 to 1), which is just its opacity. Turns off * shadow rendering if zero. */ setIntensity(intensity) { this.intensity = intensity; if (intensity > 0) { this.visible = true; this.floor.visible = true; this.floor.material.opacity = intensity * lerp(DEFAULT_HARD_INTENSITY, 1, this.softness * this.softness); } else { this.visible = false; this.floor.visible = false; } } getIntensity() { return this.intensity; } /** * An offset can be specified to move the * shadow vertically relative to the bottom of the scene. Positive is up, so * values are generally negative. A small offset keeps our shadow from * z-fighting with any baked-in shadow plane. */ setOffset(offset) { this.floor.position.z = -offset + this.gap(); } gap() { return 0.001 * this.maxDimension; } render(renderer, scene) { // this.cameraHelper.visible = false; // force the depthMaterial to everything scene.overrideMaterial = this.depthMaterial; // set renderer clear alpha const initialClearAlpha = renderer.getClearAlpha(); renderer.setClearAlpha(0); this.floor.visible = false; // disable XR for offscreen rendering const xrEnabled = renderer.xr.enabled; renderer.xr.enabled = false; // render to the render target to get the depths const oldRenderTarget = renderer.getRenderTarget(); renderer.setRenderTarget(this.renderTarget); renderer.render(scene, this.camera); // and reset the override material scene.overrideMaterial = null; this.floor.visible = true; this.blurShadow(renderer); // reset and render the normal scene renderer.xr.enabled = xrEnabled; renderer.setRenderTarget(oldRenderTarget); renderer.setClearAlpha(initialClearAlpha); // this.cameraHelper.visible = true; } blurShadow(renderer) { const { camera, horizontalBlurMaterial, verticalBlurMaterial, renderTarget, renderTargetBlur, blurPlane } = this; blurPlane.visible = true; // blur horizontally and draw in the renderTargetBlur blurPlane.material = horizontalBlurMaterial; horizontalBlurMaterial.uniforms.h.value = 1 / this.renderTarget.width; horizontalBlurMaterial.uniforms.tDiffuse.value = this.renderTarget.texture; renderer.setRenderTarget(renderTargetBlur); renderer.render(blurPlane, camera); // blur vertically and draw in the main renderTarget blurPlane.material = verticalBlurMaterial; verticalBlurMaterial.uniforms.v.value = 1 / this.renderTarget.height; verticalBlurMaterial.uniforms.tDiffuse.value = this.renderTargetBlur.texture; renderer.setRenderTarget(renderTarget); renderer.render(blurPlane, camera); blurPlane.visible = false; } dispose() { if (this.renderTarget != null) { this.renderTarget.dispose(); } if (this.renderTargetBlur != null) { this.renderTargetBlur.dispose(); } this.depthMaterial.dispose(); this.horizontalBlurMaterial.dispose(); this.verticalBlurMaterial.dispose(); this.floor.material.dispose(); this.floor.geometry.dispose(); this.blurPlane.geometry.dispose(); this.removeFromParent(); } } //# sourceMappingURL=Shadow.js.map