UNPKG

@google/model-viewer

Version:

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

440 lines (379 loc) 14.8 kB
/* * Copyright 2018 Google Inc. 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, BoxBufferGeometry, Cache, CubeCamera, DataTextureLoader, EventDispatcher, GammaEncoding, LinearFilter, LinearMipMapLinearFilter, Mesh, NearestFilter, RawShaderMaterial, RGBEEncoding, Scene, Texture, TextureEncoding, TextureLoader, WebGLRenderer, WebGLRenderTarget, WebGLRenderTargetCube} from 'three'; import {LinearEncoding} from 'three'; import {UnsignedByteType} from 'three'; import {RGBFormat} from 'three'; import {CubemapGenerator} from '../third_party/three/EquirectangularToCubeGenerator.js'; import {RGBELoader} from '../third_party/three/RGBELoader.js'; import {ProgressTracker} from '../utilities/progress-tracker.js'; import EnvironmentMapGenerator from './EnvironmentMapGenerator.js'; import {generatePMREM} from './PMREMGenerator.js'; import {encodings, texelIO} from './shader-chunk/common.glsl.js'; export interface EnvironmentMapAndSkybox { environmentMap: WebGLRenderTarget; skybox: WebGLRenderTarget|null; } export interface EnvironmentGenerationConfig { progressTracker?: ProgressTracker; } // Enable three's loader cache so we don't create redundant // Image objects to decode images fetched over the network. Cache.enabled = true; const HDR_FILE_RE = /\.hdr$/; const ldrLoader = new TextureLoader(); const hdrLoader = new RGBELoader(); const CUBEMAP_SIZE = 256; const GENERATED_BLUR = 0.04; const $environmentMapCache = Symbol('environmentMapCache'); const $generatedEnvironmentMap = Symbol('generatedEnvironmentMap'); const $loadEnvironmentMapFromUrl = Symbol('loadEnvironmentMapFromUrl'); const $loadGeneratedEnvironmentMap = Symbol('loadGeneratedEnvironmentMap'); // Attach a `userData` object for arbitrary data on textures that // originate from TextureUtils, similar to Object3D's userData, // for help debugging, providing metadata for tests, and semantically // describe the type of texture within the context of this application. const userData = { url: null, // 'Equirectangular', 'Cube', 'PMREM' mapping: null, }; export default class TextureUtils extends EventDispatcher { private renderer: WebGLRenderer; private[$generatedEnvironmentMap]: WebGLRenderTarget|null = null; private[$environmentMapCache] = new Map<string, Promise<WebGLRenderTarget>>(); constructor(renderer: WebGLRenderer) { super(); this.renderer = renderer; } equirectangularToCubemap(texture: Texture): WebGLRenderTargetCube { const generator = new CubemapGenerator(this.renderer); let target = generator.fromEquirectangular(texture, { resolution: CUBEMAP_SIZE, }); (target.texture as any).userData = { ...userData, ...({ url: (texture as any).userData ? (texture as any).userData.url : null, mapping: 'Cube', }) }; return target; } async load( url: string, progressCallback: (progress: number) => void = () => {}): Promise<Texture> { try { const isHDR: boolean = HDR_FILE_RE.test(url); const loader: DataTextureLoader = isHDR ? hdrLoader : ldrLoader; const texture: Texture = await new Promise<Texture>( (resolve, reject) => loader.load( url, resolve, (event: {loaded: number, total: number}) => { progressCallback(event.loaded / event.total * 0.9); }, reject)); progressCallback(1.0); (texture as any).userData = { ...userData, ...({ url: url, mapping: 'Equirectangular', }) }; if (isHDR) { texture.encoding = RGBEEncoding; texture.minFilter = NearestFilter; texture.magFilter = NearestFilter; texture.flipY = true; } else { texture.encoding = GammaEncoding; } return texture; } finally { if (progressCallback) { progressCallback(1); } } } async loadEquirectAsCubeMap( url: string, progressCallback: (progress: number) => void = () => {}): Promise<WebGLRenderTargetCube> { let equirect = null; try { equirect = await this.load(url, progressCallback); return await this.equirectangularToCubemap(equirect); } finally { if (equirect != null) { (equirect as any).dispose(); } } } /** * Returns a { skybox, environmentMap } object with the targets/textures * accordingly. `skybox` is a WebGLRenderCubeTarget, and `environmentMap` * is a Texture from a WebGLRenderCubeTarget. */ async generateEnvironmentMapAndSkybox( skyboxUrl: string|null = null, environmentMapUrl: string|null = null, options: EnvironmentGenerationConfig = {}): Promise<EnvironmentMapAndSkybox> { const {progressTracker} = options; const updateGenerationProgress = progressTracker != null ? progressTracker.beginActivity() : () => {}; try { let skyboxLoads: Promise<WebGLRenderTarget|null> = Promise.resolve(null); let environmentMapLoads: Promise<WebGLRenderTarget>; // If we have a skybox URL, attempt to load it as a cubemap if (!!skyboxUrl) { skyboxLoads = this[$loadEnvironmentMapFromUrl](skyboxUrl, progressTracker); } if (!!environmentMapUrl) { // We have an available environment map URL environmentMapLoads = this[$loadEnvironmentMapFromUrl]( environmentMapUrl, progressTracker); } else if (!!skyboxUrl) { // Fallback to deriving the environment map from an available skybox environmentMapLoads = skyboxLoads as Promise<WebGLRenderTarget>; } else { // Fallback to generating the environment map environmentMapLoads = this[$loadGeneratedEnvironmentMap](); } let [environmentMap, skybox] = await Promise.all([environmentMapLoads, skyboxLoads]); return {environmentMap, skybox}; } finally { updateGenerationProgress(1.0); } } /** * Loads a WebGLRenderTarget from a given URL. The render target in this * case will be assumed to be used as an environment map. */ private[$loadEnvironmentMapFromUrl]( url: string, progressTracker?: ProgressTracker): Promise<WebGLRenderTarget> { if (!this[$environmentMapCache].has(url)) { const progressCallback = progressTracker ? progressTracker.beginActivity() : () => {}; const environmentMapLoads = this.loadEquirectAsCubeMap(url, progressCallback) .then(interstitialEnvironmentMap => { const environmentMap = this.pmremPass(interstitialEnvironmentMap); // In this case, we don't care about the interstitial // environment map because it will never be used for anything, // so dispose of it right away: interstitialEnvironmentMap.dispose(); return environmentMap; }); this[$environmentMapCache].set(url, environmentMapLoads); } return this[$environmentMapCache].get(url)!; } /** * Loads a dynamically generated environment map. */ private[$loadGeneratedEnvironmentMap](): Promise<WebGLRenderTarget> { if (this[$generatedEnvironmentMap] == null) { const environmentMapGenerator = new EnvironmentMapGenerator(this.renderer); const interstitialEnvironmentMap = environmentMapGenerator.generate(); const blurredEnvironmentMap = this.gaussianBlur(interstitialEnvironmentMap, GENERATED_BLUR); this[$generatedEnvironmentMap] = this.pmremPass(blurredEnvironmentMap); // We should only ever generate this map once, and we will not be using // the environment map as a skybox, so go ahead and dispose of all // interstitial artifacts: interstitialEnvironmentMap.dispose(); blurredEnvironmentMap.dispose(); environmentMapGenerator.dispose(); } return Promise.resolve(this[$generatedEnvironmentMap]!); } gaussianBlur( cubeTarget: WebGLRenderTargetCube, standardDeviationRadians: number, outputEncoding?: TextureEncoding): WebGLRenderTargetCube { const blurScene = new Scene(); const geometry = new BoxBufferGeometry(); geometry.removeAttribute('uv'); const cubeResolution = cubeTarget.width; const standardDeviations = 3; const n = Math.ceil( standardDeviations * standardDeviationRadians * cubeResolution * 4 / Math.PI); const inverseIntegral = standardDeviations / ((n - 1) * Math.sqrt(2 * Math.PI)); let weights = []; for (let i = 0; i < n; ++i) { const x = standardDeviations * i / (n - 1); weights.push(inverseIntegral * Math.exp(-x * x / 2)); } const blurMaterial = new RawShaderMaterial({ defines: {n: n}, uniforms: { tCube: {value: null}, latitudinal: {value: false}, weights: {value: weights}, dTheta: {value: standardDeviationRadians * standardDeviations / (n - 1)}, inputEncoding: {value: encodings[LinearEncoding]}, outputEncoding: {value: encodings[LinearEncoding]} }, vertexShader: ` precision mediump float; precision mediump int; uniform mat4 modelViewMatrix; uniform mat4 projectionMatrix; attribute vec3 position; varying vec3 vPosition; void main() { vPosition = position; gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); } `, fragmentShader: ` precision mediump float; precision mediump int; varying vec3 vPosition; uniform float weights[n]; uniform samplerCube tCube; uniform bool latitudinal; uniform float dTheta; ${texelIO} void main() { vec4 texColor = vec4(0.0); for (int i = 0; i < n; i++) { for (int dir = -1; dir < 2; dir += 2) { if (i == 0 && dir == 1) continue; vec3 sampleDirection = vPosition; float xz = length(sampleDirection.xz); float weight = weights[i]; 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; texColor += weight * inputTexelToLinear(textureCube(tCube, sampleDirection)); } 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); sampleDirection.xz *= xzY.x / xz; sampleDirection.y = xzY.y; texColor += weight * inputTexelToLinear(textureCube(tCube, sampleDirection)); } } } gl_FragColor = texColor; gl_FragColor = linearToOutputTexel(gl_FragColor); } `, side: BackSide, depthTest: false, depthWrite: false }); blurScene.add(new Mesh(geometry, blurMaterial)); const blurUniforms = blurMaterial.uniforms; const cubeTexture = cubeTarget.texture; let blurTargetOptions = { type: cubeTexture.type, format: cubeTexture.format, encoding: cubeTexture.encoding, generateMipmaps: cubeTexture.generateMipmaps, minFilter: cubeTexture.minFilter, magFilter: cubeTexture.magFilter }; // Three.js bug: CubeCamera.d.ts constructor is not up to date with // CubeCamera.js let blurCamera = new (CubeCamera as any)(0.1, 100, cubeResolution, blurTargetOptions); const tempTexture = blurCamera.renderTarget.texture; blurUniforms.latitudinal.value = false; blurUniforms.tCube.value = cubeTexture; blurUniforms.inputEncoding.value = encodings[cubeTexture.encoding]; blurUniforms.outputEncoding.value = encodings[tempTexture.encoding]; blurCamera.update(this.renderer, blurScene); if (outputEncoding === GammaEncoding && cubeTexture.encoding !== GammaEncoding) { blurTargetOptions = { type: UnsignedByteType, format: RGBFormat, encoding: outputEncoding, generateMipmaps: true, minFilter: LinearMipMapLinearFilter, magFilter: LinearFilter }; } const outputCamera = new (CubeCamera as any)(0.1, 100, cubeResolution, blurTargetOptions); const outputTarget = outputCamera.renderTarget; (outputTarget.texture as any).userData = { ...userData, ...({ url: (cubeTexture as any).userData ? (cubeTexture as any).userData.url : null, mapping: 'Cube', }) }; blurUniforms.latitudinal.value = true; blurUniforms.tCube.value = tempTexture; blurUniforms.inputEncoding.value = encodings[tempTexture.encoding]; blurUniforms.outputEncoding.value = encodings[outputTarget.texture.encoding]; outputCamera.update(this.renderer, blurScene); tempTexture.dispose(); return outputTarget; } /** * Takes a cube-ish (@see equirectangularToCubemap) texture and * returns a texture of the prefiltered mipmapped radiance environment map * to be used as environment maps in models. */ pmremPass(target: WebGLRenderTargetCube): WebGLRenderTarget { const cubeUVTarget = generatePMREM(target, this.renderer); (cubeUVTarget.texture as any).userData = { ...userData, ...({ url: (target.texture as any).userData ? (target.texture as any).userData.url : null, mapping: 'PMREM', }) }; return cubeUVTarget; } async dispose() { const allTargetsLoad: Array<Promise<WebGLRenderTarget>> = []; // NOTE(cdata): We would use for-of iteration on the maps here, but // IE11 doesn't have the necessary iterator-returning methods. So, // disposal of these render targets is kind of convoluted as a result. this[$environmentMapCache].forEach((targetLoads) => { allTargetsLoad.push(targetLoads); }); this[$environmentMapCache].clear(); for (const targetLoads of allTargetsLoad) { try { const target = await targetLoads; target.dispose(); } catch (e) { // Suppress errors, so that all render targets will be disposed } } if (this[$generatedEnvironmentMap] != null) { this[$generatedEnvironmentMap]!.dispose(); this[$generatedEnvironmentMap] = null; } } }