UNPKG

@google/model-viewer

Version:

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

512 lines (452 loc) 18.2 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 {BufferAttribute, BufferGeometry, CubeUVReflectionMapping, LinearEncoding, LinearToneMapping, Mesh, NearestFilter, NoBlending, OrthographicCamera, PerspectiveCamera, RawShaderMaterial, RGBEEncoding, RGBEFormat, Scene, Texture, UnsignedByteType, Vector2, WebGLRenderer, WebGLRenderTarget} from 'three'; import EnvironmentScene from './EnvironmentScene.js'; import {encodings, getDirectionChunk, texelIO} from './shader-chunk/common.glsl.js'; import {bilinearCubeUVChunk} from './shader-chunk/cube_uv_reflection_fragment.glsl.js'; const LOD_MIN = 4; const LOD_MAX = 8; // The roughness values associated with the extra mips. These must match // cube_uv_reflection_fragment.glsl.js. const EXTRA_LOD_ROUGHNESS = [0.22, 0.32, 0.5, 0.7, 1.0]; // The standard deviations (radians) associated with the extra mips. These are // chosen to approximate a Trowbridge-Reitz distribution function times the // geometric shadowing function. const EXTRA_LOD_SIGMA = [0.12, 0.25, 0.35, 0.43, 0.5]; const SIZE_MAX = Math.pow(2, LOD_MAX); const TOTAL_LODS = LOD_MAX - LOD_MIN + 1 + EXTRA_LOD_ROUGHNESS.length; // Number of standard deviations at which to cut off the discrete approximation. const STANDARD_DEVIATIONS = 3; // The maximum length of the blur for loop, chosen to equal the number needed // for GENERATED_SIGMA. Smaller sigmas will use fewer samples and exit early, // but not recompile the shader. const MAX_SAMPLES = 20; const GENERATED_SIGMA = 0.04; const DEFAULT_NEAR = 0.1; const DEFAULT_FAR = 100; const $roughness = Symbol('roughness'); const $sigma = Symbol('sigma'); const $sizeLod = Symbol('sizeLod'); const $lodPlanes = Symbol('lodPlanes'); const $blurMaterial = Symbol('blurMaterial'); const $flatCamera = Symbol('flatCamera'); const $pingPongRenderTarget = Symbol('pingP$pingPongRenderTarget'); const $sceneToCubeUV = Symbol('sceneToCubeUV'); const $equirectangularToCubeUV = Symbol('equirectangularToCubeUV'); const $createRenderTarget = Symbol('createRenderTarget'); const $applyPMREM = Symbol('applyPMREM'); const $blur = Symbol('blur'); const $halfBlur = Symbol('halfBlur'); /** * This class generates a Prefiltered, Mipmapped Radiance Environment Map * (PMREM) from a cubeMap environment texture. This allows different levels of * blur to be quickly accessed based on material roughness. It is packed into a * special CubeUV format that allows us to perform custom interpolation so that * we can support nonlinear formats such as RGBE. Unlike a traditional mipmap * chain, it only goes down to the LOD_MIN level (above), and then creates extra * even more filtered 'mips' at the same LOD_MIN resolution, associated with * higher roughness levels. In this way we maintain resolution to smoothly * interpolate diffuse lighting while limiting sampling computation. */ export class PMREMGenerator { // These arrays will each be TOTAL_LODS in length, each referring to a 'mip'. private[$roughness]: Array<number> = []; private[$sigma]: Array<number> = []; private[$sizeLod]: Array<number> = []; private[$lodPlanes]: Array<BufferGeometry> = []; private[$blurMaterial] = new BlurMaterial(MAX_SAMPLES); private[$flatCamera] = new OrthographicCamera(0, 1, 0, 1, 0, 1); private[$pingPongRenderTarget]: WebGLRenderTarget; constructor(private renderer: WebGLRenderer) { let lod = LOD_MAX; for (let i = 0; i < TOTAL_LODS; i++) { const sizeLod = Math.pow(2, lod); this[$sizeLod].push(sizeLod); let sigma = 1.0 / sizeLod; let roughness = (1 + Math.sqrt(1 + 4 * Math.PI * sizeLod)) / (2 * Math.PI * sizeLod); if (i > LOD_MAX - LOD_MIN) { roughness = EXTRA_LOD_ROUGHNESS[i - LOD_MAX + LOD_MIN - 1]; sigma = EXTRA_LOD_SIGMA[i - LOD_MAX + LOD_MIN - 1]; } else if (i == 0) { sigma = 0; } this[$sigma].push(sigma); this[$roughness].push(roughness); const texelSize = 1.0 / (sizeLod - 1); const min = -texelSize / 2; const max = 1 + texelSize / 2; const uv1 = [min, min, max, min, max, max, min, min, max, max, min, max]; const cubeFaces = 6; const vertices = 6; const positionSize = 3; const uvSize = 2; const faceIndexSize = 1; const position = new Float32Array(positionSize * vertices * cubeFaces); const uv = new Float32Array(uvSize * vertices * cubeFaces); const faceIndex = new Float32Array(faceIndexSize * vertices * cubeFaces); for (let face = 0; face < cubeFaces; face++) { const x = (face % 3) * 2 / 3 - 1; const y = face > 2 ? 0 : -1; const coordinates = [ [x, y, 0], [x + 2 / 3, y, 0], [x + 2 / 3, y + 1, 0], [x, y, 0], [x + 2 / 3, y + 1, 0], [x, y + 1, 0] ]; position.set( ([] as number[]).concat(...coordinates), positionSize * vertices * face); uv.set(uv1, uvSize * vertices * face); const fill = [face, face, face, face, face, face]; faceIndex.set(fill, faceIndexSize * vertices * face); } const planes = new BufferGeometry(); planes.addAttribute( 'position', new BufferAttribute(position, positionSize)); planes.addAttribute('uv', new BufferAttribute(uv, uvSize)); planes.addAttribute( 'faceIndex', new BufferAttribute(faceIndex, faceIndexSize)); this[$lodPlanes].push(planes); if (lod > LOD_MIN) { lod--; } } } /** * Generates a PMREM from our default EnvironmentScene, which is a blurry * greyscale room with several boxes on the floor and several lit windows. */ fromDefault(): WebGLRenderTarget { const dpr = this.renderer.getPixelRatio(); this.renderer.setPixelRatio(1); const defaultScene = new EnvironmentScene; const cubeUVRenderTarget = this[$sceneToCubeUV](defaultScene, DEFAULT_NEAR, DEFAULT_FAR); this[$blur](cubeUVRenderTarget, 0, 0, GENERATED_SIGMA); this[$applyPMREM](cubeUVRenderTarget); defaultScene.dispose(); this.renderer.setPixelRatio(dpr); return cubeUVRenderTarget; } /** * Generates a PMREM from a supplied Scene, which can be faster than using an * image if networking bandwidth is low. Optional near and far planes ensure * the scene is rendered in its entirety (the cubeCamera is placed at the * origin). */ fromScene( scene: Scene, near: number = DEFAULT_NEAR, far: number = DEFAULT_FAR): WebGLRenderTarget { const dpr = this.renderer.getPixelRatio(); this.renderer.setPixelRatio(1); const cubeUVRenderTarget = this[$sceneToCubeUV](scene, near, far); this[$applyPMREM](cubeUVRenderTarget); this.renderer.setPixelRatio(dpr); return cubeUVRenderTarget; } /** * Generates a PMREM from an equirectangular texture, which can be either LDR * (RGBFormat) or HDR (RGBEFormat). */ fromEquirectangular(equirectangular: Texture): WebGLRenderTarget { const dpr = this.renderer.getPixelRatio(); this.renderer.setPixelRatio(1); equirectangular.magFilter = NearestFilter; equirectangular.minFilter = NearestFilter; equirectangular.generateMipmaps = false; const cubeUVRenderTarget = this[$equirectangularToCubeUV](equirectangular); this[$applyPMREM](cubeUVRenderTarget); this.renderer.setPixelRatio(dpr); return cubeUVRenderTarget; } private[$sceneToCubeUV](scene: Scene, near: number, far: number): WebGLRenderTarget { const params = { magFilter: NearestFilter, minFilter: NearestFilter, generateMipmaps: false, type: UnsignedByteType, format: RGBEFormat, encoding: RGBEEncoding }; const cubeUVRenderTarget = this[$createRenderTarget](params); this[$pingPongRenderTarget] = this[$createRenderTarget](params); const fov = 90; const aspect = 1; const cubeCamera = new PerspectiveCamera(fov, aspect, near, far); const upSign = [1, 1, 1, 1, -1, 1]; const forwardSign = [1, 1, -1, -1, -1, 1]; const gammaOutput = this.renderer.gammaOutput; const toneMapping = this.renderer.toneMapping; const toneMappingExposure = this.renderer.toneMappingExposure; this.renderer.toneMapping = LinearToneMapping; this.renderer.toneMappingExposure = 1.0; this.renderer.gammaOutput = false; scene.scale.z *= -1; this.renderer.setRenderTarget(cubeUVRenderTarget); for (let i = 0; i < 6; i++) { const col = i % 3; if (col == 0) { cubeCamera.up.set(0, upSign[i], 0); cubeCamera.lookAt(forwardSign[i], 0, 0); } else if (col == 1) { cubeCamera.up.set(0, 0, upSign[i]); cubeCamera.lookAt(0, forwardSign[i], 0); } else { cubeCamera.up.set(0, upSign[i], 0); cubeCamera.lookAt(0, 0, forwardSign[i]); } this.renderer.setViewport( col * SIZE_MAX, i > 2 ? SIZE_MAX : 0, SIZE_MAX, SIZE_MAX); this.renderer.render(scene, cubeCamera); } this.renderer.toneMapping = toneMapping; this.renderer.toneMappingExposure = toneMappingExposure; this.renderer.gammaOutput = gammaOutput; scene.scale.z *= -1; return cubeUVRenderTarget; } private[$equirectangularToCubeUV](equirectangular: Texture): WebGLRenderTarget { const params = { magFilter: NearestFilter, minFilter: NearestFilter, generateMipmaps: false, type: equirectangular.type, format: equirectangular.format, encoding: equirectangular.encoding }; const cubeUVRenderTarget = this[$createRenderTarget](params); this[$pingPongRenderTarget] = this[$createRenderTarget](params); const scene = new Scene(); scene.add(new Mesh(this[$lodPlanes][0], this[$blurMaterial])); const uniforms = this[$blurMaterial].uniforms; uniforms.envMap.value = equirectangular; uniforms.copyEquirectangular.value = true; uniforms.texelSize.value = new Vector2( 1.0 / equirectangular.image.width, 1.0 / equirectangular.image.height); uniforms.inputEncoding.value = encodings[equirectangular.encoding]; uniforms.outputEncoding.value = encodings[equirectangular.encoding]; this.renderer.setRenderTarget(cubeUVRenderTarget); this.renderer.setViewport(0, 0, 3 * SIZE_MAX, 2 * SIZE_MAX); this.renderer.render(scene, this[$flatCamera]); return cubeUVRenderTarget; } private[$createRenderTarget](params: Object): WebGLRenderTarget { const cubeUVRenderTarget = new WebGLRenderTarget(3 * SIZE_MAX, 3 * SIZE_MAX, params); cubeUVRenderTarget.texture.mapping = CubeUVReflectionMapping; cubeUVRenderTarget.texture.name = 'PMREM.cubeUv'; return cubeUVRenderTarget; } private[$applyPMREM](cubeUVRenderTarget: WebGLRenderTarget) { for (let i = 1; i < TOTAL_LODS; i++) { const sigma = Math.sqrt( this[$sigma][i] * this[$sigma][i] - this[$sigma][i - 1] * this[$sigma][i - 1]); this[$blur](cubeUVRenderTarget, i - 1, i, sigma); } this[$pingPongRenderTarget].dispose(); } /** * This is a two-pass Gaussian blur for a cubemap. Normally this is done * vertically and horizontally, but this breaks down on a cube. Here we apply * the blur latitudinally (around the poles), and then longitudinally (towards * the poles) to approximate the orthogonally-separable blur. It is least * accurate at the poles, but still does a decent job. */ private[$blur]( cubeUVRenderTarget: WebGLRenderTarget, lodIn: number, lodOut: number, sigma: number) { this[$halfBlur]( cubeUVRenderTarget, this[$pingPongRenderTarget], lodIn, lodOut, sigma, 'latitudinal'); this[$halfBlur]( this[$pingPongRenderTarget], cubeUVRenderTarget, lodOut, lodOut, sigma, 'longitudinal'); } private[$halfBlur]( targetIn: WebGLRenderTarget, targetOut: WebGLRenderTarget, lodIn: number, lodOut: number, sigmaRadians: number, direction: string) { if (direction !== 'latitudinal' && direction !== 'longitudinal') { console.error( 'blur direction must be either latitudinal or longitudinal!'); } const blurScene = new Scene(); blurScene.add(new Mesh(this[$lodPlanes][lodOut], this[$blurMaterial])); const blurUniforms = this[$blurMaterial].uniforms; const pixels = this[$sizeLod][lodIn] - 1; const radiansPerPixel = Math.PI / (2 * pixels); const sigmaPixels = sigmaRadians / radiansPerPixel; const samples = 1 + Math.floor(STANDARD_DEVIATIONS * sigmaPixels); if (samples > MAX_SAMPLES) { console.warn(`sigmaRadians, ${ sigmaRadians}, is too large and will clip, as it requested ${ samples} samples when the maximum is set to ${MAX_SAMPLES}`); } let weights = []; let sum = 0; for (let i = 0; i < MAX_SAMPLES; ++i) { const x = i / sigmaPixels; const weight = Math.exp(-x * x / 2); weights.push(weight); if (i == 0) { sum += weight; } else if (i < samples) { sum += 2 * weight; } } weights = weights.map(w => w / sum); blurUniforms.envMap.value = targetIn.texture; blurUniforms.copyEquirectangular.value = false; blurUniforms.samples.value = samples; blurUniforms.weights.value = weights; blurUniforms.latitudinal.value = direction === 'latitudinal'; blurUniforms.dTheta.value = radiansPerPixel; blurUniforms.mipInt.value = LOD_MAX - lodIn; blurUniforms.inputEncoding.value = encodings[targetIn.texture.encoding]; blurUniforms.outputEncoding.value = encodings[targetIn.texture.encoding]; const outputSize = this[$sizeLod][lodOut]; const x = 3 * Math.max(0, SIZE_MAX - 2 * outputSize); const y = (lodOut === 0 ? 0 : 2 * SIZE_MAX) + 2 * outputSize * (lodOut > LOD_MAX - LOD_MIN ? lodOut - LOD_MAX + LOD_MIN : 0); this.renderer.autoClear = false; this.renderer.setRenderTarget(targetOut); this.renderer.setViewport(x, y, 3 * outputSize, 2 * outputSize); this.renderer.render(blurScene, this[$flatCamera]); } }; class BlurMaterial extends RawShaderMaterial { constructor(maxSamples: number) { const weights = new Float32Array(maxSamples); const texelSize = new Vector2(1, 1); super({ defines: {n: maxSamples}, uniforms: { envMap: {value: null}, copyEquirectangular: {value: false}, texelSize: {value: texelSize}, samples: {value: 1}, weights: {value: weights}, latitudinal: {value: false}, dTheta: {value: 0}, mipInt: {value: 0}, inputEncoding: {value: encodings[LinearEncoding]}, outputEncoding: {value: encodings[LinearEncoding]} }, vertexShader: ` precision mediump float; precision mediump int; attribute vec3 position; attribute vec2 uv; attribute float faceIndex; varying vec3 vOutputDirection; ${getDirectionChunk} void main() { vOutputDirection = getDirection(uv, faceIndex); gl_Position = vec4( position, 1.0 ); } `, fragmentShader: ` precision mediump float; precision mediump int; varying vec3 vOutputDirection; uniform sampler2D envMap; uniform bool copyEquirectangular; uniform vec2 texelSize; uniform int samples; uniform float weights[n]; uniform bool latitudinal; uniform float dTheta; uniform float mipInt; #define RECIPROCAL_PI 0.31830988618 #define RECIPROCAL_PI2 0.15915494 ${texelIO} vec4 envMapTexelToLinear(vec4 color) { return inputTexelToLinear(color); } ${bilinearCubeUVChunk} void main() { gl_FragColor = vec4(0.0); if (copyEquirectangular) { vec3 direction = normalize(vOutputDirection); vec2 uv; uv.y = asin(clamp(direction.y, -1.0, 1.0)) * RECIPROCAL_PI + 0.5; uv.x = atan(direction.z, direction.x) * RECIPROCAL_PI2 + 0.5; vec2 f = fract(uv / texelSize - 0.5); uv -= f * texelSize; vec3 tl = envMapTexelToLinear(texture2D(envMap, uv)).rgb; uv.x += texelSize.x; vec3 tr = envMapTexelToLinear(texture2D(envMap, uv)).rgb; uv.y += texelSize.y; vec3 br = envMapTexelToLinear(texture2D(envMap, uv)).rgb; uv.x -= texelSize.x; vec3 bl = envMapTexelToLinear(texture2D(envMap, uv)).rgb; vec3 tm = mix(tl, tr, f.x); vec3 bm = mix(bl, br, f.x); gl_FragColor.rgb = mix(tm, bm, f.y); } else { float xz = length(vOutputDirection.xz); for (int i = 0; i < n; i++) { if (i >= samples) break; for (int dir = -1; dir < 2; dir += 2) { if (i == 0 && dir == 1) continue; vec3 sampleDirection = vOutputDirection; if (latitudinal) { float diTheta = dTheta * float(dir * i) / xz; mat2 R = mat2(cos(diTheta), sin(diTheta), -sin(diTheta), cos(diTheta)); sampleDirection.xz = R * sampleDirection.xz; } else { float diTheta = dTheta * float(dir * i); mat2 R = mat2(cos(diTheta), sin(diTheta), -sin(diTheta), cos(diTheta)); vec2 xzY = R * vec2(xz, sampleDirection.y); if (xzY.x < 0.0) { sampleDirection = vec3(0.0, sign(sampleDirection.y), 0.0); } else { sampleDirection.xz *= xzY.x / xz; sampleDirection.y = xzY.y; } } gl_FragColor.rgb += weights[i] * bilinearCubeUV(envMap, sampleDirection, mipInt); } } } gl_FragColor = linearToOutputTexel(gl_FragColor); } `, blending: NoBlending, depthTest: false, depthWrite: false }); this.type = 'GaussianBlur'; } }