UNPKG

@olearis/view3d

Version:

Fast & Customizable glTF 3D model viewer, packed with full of features!

1,348 lines (1,294 loc) 533 kB
/* Copyright (c) NAVER Corp. name: @olearis/view3d license: MIT author: NAVER Corp. repository: https://github.com/naver/egjs-view3d version: 2.10.2 */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('three'), require('@egjs/component')) : typeof define === 'function' && define.amd ? define(['three', '@egjs/component'], factory) : (global = global || self, global.View3D = factory(global.THREE, global.Component)); }(this, (function (THREE, Component) { 'use strict'; /* * Copyright (c) 2020 NAVER Corp. * egjs projects are licensed under the MIT license */ /** * Error thrown by View3D */ class View3DError extends Error { /** * Create new instance of View3DError * @param {string} message Error message * @param {number} code Error code, see {@link ERROR_CODES} */ constructor(message, code) { super(message); Object.setPrototypeOf(this, View3DError.prototype); this.name = "View3DError"; this.code = code; } } /* * Copyright (c) 2020 NAVER Corp. * egjs projects are licensed under the MIT license */ /** * Error codes of {@link View3DError} * @type object * @property {0} WRONG_TYPE The given value's type is not expected * @property {1} ELEMENT_NOT_FOUND The element with given CSS selector does not exist * @property {2} CANVAS_NOT_FOUND The element given is not a \<canvas\> element * @property {3} WEBGL_NOT_SUPPORTED The browser does not support WebGL * @property {4} PROVIDE_SRC_FIRST `init()` is called before setting `src` * @property {5} FILE_NOT_SUPPORTED The given file is not supported * @property {6} NOT_INITIALIZED The action is called before the component is initialized * @property {7} MODEL_FAIL_TO_LOAD The 3D model failed to load */ const ERROR_CODES = { WRONG_TYPE: 0, ELEMENT_NOT_FOUND: 1, CANVAS_NOT_FOUND: 2, WEBGL_NOT_SUPPORTED: 3, PROVIDE_SRC_FIRST: 4, FILE_NOT_SUPPORTED: 5, NOT_INITIALIZED: 6, MODEL_FAIL_TO_LOAD: 7 }; const MESSAGES = { WRONG_TYPE: (val, types) => `${typeof val} is not a ${types.map(type => `"${type}"`).join(" or ")}.`, ELEMENT_NOT_FOUND: query => `Element with selector "${query}" not found.`, CANVAS_NOT_FOUND: "The canvas element was not found inside the given root element.", WEBGL_NOT_SUPPORTED: "WebGL is not supported on this browser.", PROVIDE_SRC_FIRST: "\"src\" should be provided before initialization.", FILE_NOT_SUPPORTED: src => `Given file "${src}" is not supported.`, NOT_INITIALIZED: "View3D is not initialized yet.", MODEL_FAIL_TO_LOAD: url => `Failed to load/parse the 3D model with the given url: "${url}". Check "loadError" event for actual error instance.` }; var ERROR = { CODES: ERROR_CODES, MESSAGES }; /* * Copyright (c) 2020 NAVER Corp. * egjs projects are licensed under the MIT license */ const isNumber = val => typeof val === "number"; const isString = val => typeof val === "string"; const isElement = val => !!val && val.nodeType === Node.ELEMENT_NODE; const getNullableElement = (el, parent) => { let targetEl = null; if (isString(el)) { const parentEl = parent ? parent : document; const queryResult = parentEl.querySelector(el); if (!queryResult) { return null; } targetEl = queryResult; } else if (isElement(el)) { targetEl = el; } return targetEl; }; const getElement = (el, parent) => { const targetEl = getNullableElement(el, parent); if (!targetEl) { if (isString(el)) { throw new View3DError(ERROR.MESSAGES.ELEMENT_NOT_FOUND(el), ERROR.CODES.ELEMENT_NOT_FOUND); } else { throw new View3DError(ERROR.MESSAGES.WRONG_TYPE(el, ["HTMLElement", "string"]), ERROR.CODES.WRONG_TYPE); } } return targetEl; }; const findCanvas = (root, selector) => { const canvas = root.querySelector(selector); if (!canvas) { throw new View3DError(ERROR.MESSAGES.CANVAS_NOT_FOUND, ERROR.CODES.CANVAS_NOT_FOUND); } return canvas; }; const isCSSSelector = val => { if (!isString(val)) return false; const dummyEl = document.createDocumentFragment(); try { dummyEl.querySelector(val); } catch (_a) { return false; } return true; }; const range = end => { if (!end || end <= 0) { return []; } return Array.apply(0, Array(end)).map((undef, idx) => idx); }; const toRadian = x => x * Math.PI / 180; const toDegree = x => x * 180 / Math.PI; const clamp = (x, min, max) => Math.max(Math.min(x, max), min); // Linear interpolation between a and b const lerp = (a, b, t) => { return a * (1 - t) + b * t; }; const circulate = (val, min, max) => { const size = Math.abs(max - min); if (val < min) { const offset = (min - val) % size; val = max - offset; } else if (val > max) { const offset = (val - max) % size; val = min + offset; } return val; }; // eslint-disable-next-line @typescript-eslint/ban-types const merge = (target, ...srcs) => { srcs.forEach(source => { Object.keys(source).forEach(key => { const value = source[key]; if (Array.isArray(target[key]) && Array.isArray(value)) { target[key] = [...target[key], ...value]; } else { target[key] = value; } }); }); return target; }; const getBoxPoints = box => { return [box.min.clone(), new THREE.Vector3(box.min.x, box.min.y, box.max.z), new THREE.Vector3(box.min.x, box.max.y, box.min.z), new THREE.Vector3(box.min.x, box.max.y, box.max.z), new THREE.Vector3(box.max.x, box.min.y, box.min.z), new THREE.Vector3(box.max.x, box.min.y, box.max.z), new THREE.Vector3(box.max.x, box.max.y, box.min.z), box.max.clone()]; }; const toPowerOfTwo = val => { let result = 1; while (result < val) { result *= 2; } return result; }; const getPrimaryAxisIndex = (basis, viewDir) => { let primaryIdx = 0; let maxDot = 0; basis.forEach((axes, axesIdx) => { const dotProduct = Math.abs(viewDir.dot(axes)); if (dotProduct > maxDot) { primaryIdx = axesIdx; maxDot = dotProduct; } }); return primaryIdx; }; // In radian const getRotationAngle = (center, v1, v2) => { const centerToV1 = new THREE.Vector2().subVectors(v1, center).normalize(); const centerToV2 = new THREE.Vector2().subVectors(v2, center).normalize(); // Get the rotation angle with the model's NDC coordinates as the center. const deg = centerToV2.angle() - centerToV1.angle(); const compDeg = -Math.sign(deg) * (2 * Math.PI - Math.abs(deg)); // Take the smaller deg const rotationAngle = Math.abs(deg) < Math.abs(compDeg) ? deg : compDeg; return rotationAngle; }; const getObjectOption = val => typeof val === "object" ? val : {}; const toBooleanString = val => val ? "true" : "false"; const getRotatedPosition = (distance, yawDeg, pitchDeg) => { const yaw = toRadian(yawDeg); const pitch = toRadian(pitchDeg); const newPos = new THREE.Vector3(0, 0, 0); newPos.y = distance * Math.sin(pitch); newPos.z = distance * Math.cos(pitch); newPos.x = newPos.z * Math.sin(-yaw); newPos.z = newPos.z * Math.cos(-yaw); return newPos; }; // In Radians const directionToYawPitch = direction => { const xz = new THREE.Vector2(direction.x, direction.z); const origin = new THREE.Vector2(); const yaw = Math.abs(direction.y) <= 0.99 ? getRotationAngle(origin, new THREE.Vector2(0, 1), xz) : 0; const pitch = Math.atan2(direction.y, xz.distanceTo(origin)); return { yaw, pitch }; }; const createLoadingContext = (view3D, src) => { const context = { src, loaded: 0, total: 0, lengthComputable: false, initialized: false }; view3D.loadingContext.push(context); return context; }; const getAttributeScale = attrib => { if (attrib.normalized && ArrayBuffer.isView(attrib.array)) { const buffer = attrib.array; const isSigned = isSignedArrayBuffer(buffer); const scale = 1 / (Math.pow(2, 8 * buffer.BYTES_PER_ELEMENT) - 1); return isSigned ? scale * 2 : scale; } else { return 1; } }; const getSkinnedVertex = (posIdx, mesh, positionScale, skinWeightScale) => { const geometry = mesh.geometry; const positions = geometry.attributes.position; const skinIndicies = geometry.attributes.skinIndex; const skinWeights = geometry.attributes.skinWeight; const skeleton = mesh.skeleton; const boneMatricies = skeleton.boneMatrices; const pos = new THREE.Vector3().fromBufferAttribute(positions, posIdx).multiplyScalar(positionScale); const skinned = new THREE.Vector4(0, 0, 0, 0); const skinVertex = new THREE.Vector4(pos.x, pos.y, pos.z).applyMatrix4(mesh.bindMatrix); const weights = [skinWeights.getX(posIdx), skinWeights.getY(posIdx), skinWeights.getZ(posIdx), skinWeights.getW(posIdx)].map(weight => weight * skinWeightScale); const indicies = [skinIndicies.getX(posIdx), skinIndicies.getY(posIdx), skinIndicies.getZ(posIdx), skinIndicies.getW(posIdx)]; weights.forEach((weight, index) => { const boneMatrix = new THREE.Matrix4().fromArray(boneMatricies, indicies[index] * 16); skinned.add(skinVertex.clone().applyMatrix4(boneMatrix).multiplyScalar(weight)); }); const transformed = new THREE.Vector3().fromArray(skinned.applyMatrix4(mesh.bindMatrixInverse).toArray()); transformed.applyMatrix4(mesh.matrixWorld); return transformed; }; const isSignedArrayBuffer = buffer => { const testBuffer = new buffer.constructor(1); testBuffer[0] = -1; return testBuffer[0] < 0; }; const checkHalfFloatAvailable = renderer => { if (renderer.capabilities.isWebGL2) { return true; } else { const gl = renderer.getContext(); const texture = gl.createTexture(); let available = true; try { const data = new Uint16Array(4); const ext = gl.getExtension("OES_texture_half_float"); if (!ext) { available = false; } else { gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, ext.HALF_FLOAT_OES, data); const err = gl.getError(); available = err === gl.NO_ERROR; } } catch (err) { available = false; } gl.deleteTexture(texture); return available; } }; const getFaceVertices = (model, meshIndex, faceIndex) => { var _a; if (!model || meshIndex < 0 || faceIndex < 0) return null; const mesh = model.meshes[meshIndex]; const indexes = (_a = mesh === null || mesh === void 0 ? void 0 : mesh.geometry.index) === null || _a === void 0 ? void 0 : _a.array; const face = indexes ? range(3).map(idx => indexes[3 * faceIndex + idx]) : null; if (!mesh || !indexes || !face || face.some(val => val == null)) return null; const position = mesh.geometry.getAttribute("position"); const vertices = face.map(index => { return new THREE.Vector3().fromBufferAttribute(position, index); }); return vertices; }; const getAnimatedFace = (model, meshIndex, faceIndex) => { const vertices = getFaceVertices(model, meshIndex, faceIndex); if (!vertices) return null; const mesh = model.meshes[meshIndex]; const indexes = mesh.geometry.getIndex(); const face = indexes.array.slice(3 * faceIndex, 3 * faceIndex + 3); if (mesh.isSkinnedMesh) { const geometry = mesh.geometry; const positions = geometry.attributes.position; const skinWeights = geometry.attributes.skinWeight; const positionScale = getAttributeScale(positions); const skinWeightScale = getAttributeScale(skinWeights); vertices.forEach((vertex, idx) => { const posIdx = face[idx]; const transformed = getSkinnedVertex(posIdx, mesh, positionScale, skinWeightScale); vertex.copy(transformed); }); } else { vertices.forEach(vertex => { vertex.applyMatrix4(mesh.matrixWorld); }); } return vertices; }; const subclip = (sourceClip, name, startTime, endTime) => { const clip = sourceClip.clone(); clip.name = name; const tracks = []; clip.tracks.forEach(track => { const valueSize = track.getValueSize(); const times = []; const values = []; for (let timeIdx = 0; timeIdx < track.times.length; ++timeIdx) { const time = track.times[timeIdx]; const nextTime = track.times[timeIdx + 1]; const prevTime = track.times[timeIdx - 1]; const isPrevFrame = nextTime && time < startTime && nextTime > startTime; const isMiddleFrame = time >= startTime && time < endTime; const isNextFrame = prevTime && time >= endTime && prevTime < endTime; if (!isPrevFrame && !isMiddleFrame && !isNextFrame) continue; times.push(time); for (let k = 0; k < valueSize; ++k) { values.push(track.values[timeIdx * valueSize + k]); } } if (times.length === 0) return; track.times = convertArray(times, track.times.constructor); track.values = convertArray(values, track.values.constructor); tracks.push(track); }); clip.tracks = tracks; for (let i = 0; i < clip.tracks.length; ++i) { clip.tracks[i].shift(-startTime); } clip.duration = endTime - startTime; return clip; }; // From three.js AnimationUtils // https://github.com/mrdoob/three.js/blob/68daccedef9c9c325cc5f4c929fcaf05229aa1b3/src/animation/AnimationUtils.js#L20 // The MIT License // Copyright © 2010-2022 three.js authors const convertArray = (array, type, forceClone = false) => { if (!array || // let 'undefined' and 'null' pass !forceClone && array.constructor === type) return array; if (typeof type.BYTES_PER_ELEMENT === "number") { return new type(array); // create typed array } return Array.prototype.slice.call(array); // create Array }; const parseAsBboxRatio = (arr, bbox) => { const min = bbox.min.toArray(); const size = new THREE.Vector3().subVectors(bbox.max, bbox.min).toArray(); return new THREE.Vector3().fromArray(arr.map((val, idx) => { if (!isString(val)) return val; const ratio = parseFloat(val) * 0.01; return min[idx] + ratio * size[idx]; })); }; /*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } /** * Full-screen textured quad shader */ var CopyShader = { uniforms: { 'tDiffuse': { value: null }, 'opacity': { value: 1.0 } }, vertexShader: /* glsl */"\n\n\t\tvarying vec2 vUv;\n\n\t\tvoid main() {\n\n\t\t\tvUv = uv;\n\t\t\tgl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n\n\t\t}", fragmentShader: /* glsl */"\n\n\t\tuniform float opacity;\n\n\t\tuniform sampler2D tDiffuse;\n\n\t\tvarying vec2 vUv;\n\n\t\tvoid main() {\n\n\t\t\tvec4 texel = texture2D( tDiffuse, vUv );\n\t\t\tgl_FragColor = opacity * texel;\n\n\t\t}" }; class Pass { constructor() { // if set to true, the pass is processed by the composer this.enabled = true; // if set to true, the pass indicates to swap read and write buffer after rendering this.needsSwap = true; // if set to true, the pass clears its buffer before rendering this.clear = false; // if set to true, the result of the pass is rendered to screen. This is set automatically by EffectComposer. this.renderToScreen = false; } setSize( /* width, height */) {} render( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) { console.error('THREE.Pass: .render() must be implemented in derived pass.'); } } // Helper for passes that need to fill the viewport with a single quad. var _camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); // https://github.com/mrdoob/three.js/pull/21358 var _geometry = new THREE.BufferGeometry(); _geometry.setAttribute('position', new THREE.Float32BufferAttribute([-1, 3, 0, -1, -1, 0, 3, -1, 0], 3)); _geometry.setAttribute('uv', new THREE.Float32BufferAttribute([0, 2, 0, 0, 2, 0], 2)); class FullScreenQuad { constructor(material) { this._mesh = new THREE.Mesh(_geometry, material); } dispose() { this._mesh.geometry.dispose(); } render(renderer) { renderer.render(this._mesh, _camera); } get material() { return this._mesh.material; } set material(value) { this._mesh.material = value; } } class ShaderPass extends Pass { constructor(shader, textureID) { super(); this.textureID = textureID !== undefined ? textureID : 'tDiffuse'; if (shader instanceof THREE.ShaderMaterial) { this.uniforms = shader.uniforms; this.material = shader; } else if (shader) { this.uniforms = THREE.UniformsUtils.clone(shader.uniforms); this.material = new THREE.ShaderMaterial({ defines: Object.assign({}, shader.defines), uniforms: this.uniforms, vertexShader: shader.vertexShader, fragmentShader: shader.fragmentShader }); } this.fsQuad = new FullScreenQuad(this.material); } render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */) { if (this.uniforms[this.textureID]) { this.uniforms[this.textureID].value = readBuffer.texture; } this.fsQuad.material = this.material; if (this.renderToScreen) { renderer.setRenderTarget(null); this.fsQuad.render(renderer); } else { renderer.setRenderTarget(writeBuffer); // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600 if (this.clear) renderer.clear(renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil); this.fsQuad.render(renderer); } } } class MaskPass extends Pass { constructor(scene, camera) { super(); this.scene = scene; this.camera = camera; this.clear = true; this.needsSwap = false; this.inverse = false; } render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */) { var context = renderer.getContext(); var state = renderer.state; // don't update color or depth state.buffers.color.setMask(false); state.buffers.depth.setMask(false); // lock buffers state.buffers.color.setLocked(true); state.buffers.depth.setLocked(true); // set up stencil var writeValue, clearValue; if (this.inverse) { writeValue = 0; clearValue = 1; } else { writeValue = 1; clearValue = 0; } state.buffers.stencil.setTest(true); state.buffers.stencil.setOp(context.REPLACE, context.REPLACE, context.REPLACE); state.buffers.stencil.setFunc(context.ALWAYS, writeValue, 0xffffffff); state.buffers.stencil.setClear(clearValue); state.buffers.stencil.setLocked(true); // draw into the stencil buffer renderer.setRenderTarget(readBuffer); if (this.clear) renderer.clear(); renderer.render(this.scene, this.camera); renderer.setRenderTarget(writeBuffer); if (this.clear) renderer.clear(); renderer.render(this.scene, this.camera); // unlock color and depth buffer for subsequent rendering state.buffers.color.setLocked(false); state.buffers.depth.setLocked(false); // only render where stencil is set to 1 state.buffers.stencil.setLocked(false); state.buffers.stencil.setFunc(context.EQUAL, 1, 0xffffffff); // draw if == 1 state.buffers.stencil.setOp(context.KEEP, context.KEEP, context.KEEP); state.buffers.stencil.setLocked(true); } } class ClearMaskPass extends Pass { constructor() { super(); this.needsSwap = false; } render(renderer /*, writeBuffer, readBuffer, deltaTime, maskActive */) { renderer.state.buffers.stencil.setLocked(false); renderer.state.buffers.stencil.setTest(false); } } class EffectComposer { constructor(renderer, renderTarget) { this.renderer = renderer; if (renderTarget === undefined) { var parameters = { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBAFormat }; var size = renderer.getSize(new THREE.Vector2()); this._pixelRatio = renderer.getPixelRatio(); this._width = size.width; this._height = size.height; renderTarget = new THREE.WebGLRenderTarget(this._width * this._pixelRatio, this._height * this._pixelRatio, parameters); renderTarget.texture.name = 'EffectComposer.rt1'; } else { this._pixelRatio = 1; this._width = renderTarget.width; this._height = renderTarget.height; } this.renderTarget1 = renderTarget; this.renderTarget2 = renderTarget.clone(); this.renderTarget2.texture.name = 'EffectComposer.rt2'; this.writeBuffer = this.renderTarget1; this.readBuffer = this.renderTarget2; this.renderToScreen = true; this.passes = []; // dependencies if (CopyShader === undefined) { console.error('THREE.EffectComposer relies on CopyShader'); } if (ShaderPass === undefined) { console.error('THREE.EffectComposer relies on ShaderPass'); } this.copyPass = new ShaderPass(CopyShader); this.clock = new THREE.Clock(); } swapBuffers() { var tmp = this.readBuffer; this.readBuffer = this.writeBuffer; this.writeBuffer = tmp; } addPass(pass) { this.passes.push(pass); pass.setSize(this._width * this._pixelRatio, this._height * this._pixelRatio); } insertPass(pass, index) { this.passes.splice(index, 0, pass); pass.setSize(this._width * this._pixelRatio, this._height * this._pixelRatio); } removePass(pass) { var index = this.passes.indexOf(pass); if (index !== -1) { this.passes.splice(index, 1); } } isLastEnabledPass(passIndex) { for (var i = passIndex + 1; i < this.passes.length; i++) { if (this.passes[i].enabled) { return false; } } return true; } render(deltaTime) { // deltaTime value is in seconds if (deltaTime === undefined) { deltaTime = this.clock.getDelta(); } var currentRenderTarget = this.renderer.getRenderTarget(); var maskActive = false; for (var i = 0, il = this.passes.length; i < il; i++) { var pass = this.passes[i]; if (pass.enabled === false) continue; pass.renderToScreen = this.renderToScreen && this.isLastEnabledPass(i); pass.render(this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive); if (pass.needsSwap) { if (maskActive) { var context = this.renderer.getContext(); var stencil = this.renderer.state.buffers.stencil; //context.stencilFunc( context.NOTEQUAL, 1, 0xffffffff ); stencil.setFunc(context.NOTEQUAL, 1, 0xffffffff); this.copyPass.render(this.renderer, this.writeBuffer, this.readBuffer, deltaTime); //context.stencilFunc( context.EQUAL, 1, 0xffffffff ); stencil.setFunc(context.EQUAL, 1, 0xffffffff); } this.swapBuffers(); } if (MaskPass !== undefined) { if (pass instanceof MaskPass) { maskActive = true; } else if (pass instanceof ClearMaskPass) { maskActive = false; } } } this.renderer.setRenderTarget(currentRenderTarget); } reset(renderTarget) { if (renderTarget === undefined) { var size = this.renderer.getSize(new THREE.Vector2()); this._pixelRatio = this.renderer.getPixelRatio(); this._width = size.width; this._height = size.height; renderTarget = this.renderTarget1.clone(); renderTarget.setSize(this._width * this._pixelRatio, this._height * this._pixelRatio); } this.renderTarget1.dispose(); this.renderTarget2.dispose(); this.renderTarget1 = renderTarget; this.renderTarget2 = renderTarget.clone(); this.writeBuffer = this.renderTarget1; this.readBuffer = this.renderTarget2; } setSize(width, height) { this._width = width; this._height = height; var effectiveWidth = this._width * this._pixelRatio; var effectiveHeight = this._height * this._pixelRatio; this.renderTarget1.setSize(effectiveWidth, effectiveHeight); this.renderTarget2.setSize(effectiveWidth, effectiveHeight); for (var i = 0; i < this.passes.length; i++) { this.passes[i].setSize(effectiveWidth, effectiveHeight); } } setPixelRatio(pixelRatio) { this._pixelRatio = pixelRatio; this.setSize(this._width, this._height); } } // Helper for passes that need to fill the viewport with a single quad. var _camera$1 = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); // https://github.com/mrdoob/three.js/pull/21358 var _geometry$1 = new THREE.BufferGeometry(); _geometry$1.setAttribute('position', new THREE.Float32BufferAttribute([-1, 3, 0, -1, -1, 0, 3, -1, 0], 3)); _geometry$1.setAttribute('uv', new THREE.Float32BufferAttribute([0, 2, 0, 0, 2, 0], 2)); class RenderPass extends Pass { constructor(scene, camera, overrideMaterial, clearColor, clearAlpha) { super(); this.scene = scene; this.camera = camera; this.overrideMaterial = overrideMaterial; this.clearColor = clearColor; this.clearAlpha = clearAlpha !== undefined ? clearAlpha : 0; this.clear = true; this.clearDepth = false; this.needsSwap = false; this._oldClearColor = new THREE.Color(); } render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */) { var oldAutoClear = renderer.autoClear; renderer.autoClear = false; var oldClearAlpha, oldOverrideMaterial; if (this.overrideMaterial !== undefined) { oldOverrideMaterial = this.scene.overrideMaterial; this.scene.overrideMaterial = this.overrideMaterial; } if (this.clearColor) { renderer.getClearColor(this._oldClearColor); oldClearAlpha = renderer.getClearAlpha(); renderer.setClearColor(this.clearColor, this.clearAlpha); } if (this.clearDepth) { renderer.clearDepth(); } renderer.setRenderTarget(this.renderToScreen ? null : readBuffer); // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600 if (this.clear) renderer.clear(renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil); renderer.render(this.scene, this.camera); if (this.clearColor) { renderer.setClearColor(this._oldClearColor, oldClearAlpha); } if (this.overrideMaterial !== undefined) { this.scene.overrideMaterial = oldOverrideMaterial; } renderer.autoClear = oldAutoClear; } } class SavePass extends Pass { constructor(renderTarget) { super(); if (CopyShader === undefined) console.error('THREE.SavePass relies on CopyShader'); var shader = CopyShader; this.textureID = 'tDiffuse'; this.uniforms = THREE.UniformsUtils.clone(shader.uniforms); this.material = new THREE.ShaderMaterial({ uniforms: this.uniforms, vertexShader: shader.vertexShader, fragmentShader: shader.fragmentShader }); this.renderTarget = renderTarget; if (this.renderTarget === undefined) { this.renderTarget = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBFormat }); this.renderTarget.texture.name = 'SavePass.rt'; } this.needsSwap = false; this.fsQuad = new FullScreenQuad(this.material); } render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */) { if (this.uniforms[this.textureID]) { this.uniforms[this.textureID].value = readBuffer.texture; } renderer.setRenderTarget(this.renderTarget); if (this.clear) renderer.clear(); this.fsQuad.render(renderer); } } /** * Blend two textures */ var BlendShader = { uniforms: { 'tDiffuse1': { value: null }, 'tDiffuse2': { value: null }, 'mixRatio': { value: 0.5 }, 'opacity': { value: 1.0 } }, vertexShader: /* glsl */"\n\n\t\tvarying vec2 vUv;\n\n\t\tvoid main() {\n\n\t\t\tvUv = uv;\n\t\t\tgl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n\n\t\t}", fragmentShader: /* glsl */"\n\n\t\tuniform float opacity;\n\t\tuniform float mixRatio;\n\n\t\tuniform sampler2D tDiffuse1;\n\t\tuniform sampler2D tDiffuse2;\n\n\t\tvarying vec2 vUv;\n\n\t\tvoid main() {\n\n\t\t\tvec4 texel1 = texture2D( tDiffuse1, vUv );\n\t\t\tvec4 texel2 = texture2D( tDiffuse2, vUv );\n\t\t\tgl_FragColor = opacity * mix( texel1, texel2, mixRatio );\n\n\t\t}" }; /** * Gamma Correction Shader * http://en.wikipedia.org/wiki/gamma_correction */ var GammaCorrectionShader = { uniforms: { 'tDiffuse': { value: null } }, vertexShader: /* glsl */"\n\n\t\tvarying vec2 vUv;\n\n\t\tvoid main() {\n\n\t\t\tvUv = uv;\n\t\t\tgl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n\n\t\t}", fragmentShader: /* glsl */"\n\n\t\tuniform sampler2D tDiffuse;\n\n\t\tvarying vec2 vUv;\n\n\t\tvoid main() {\n\n\t\t\tvec4 tex = texture2D( tDiffuse, vUv );\n\n\t\t\tgl_FragColor = LinearTosRGB( tex ); // optional: LinearToGamma( tex, float( GAMMA_FACTOR ) );\n\n\t\t}" }; /* * Copyright (c) 2020 NAVER Corp. * egjs projects are licensed under the MIT license */ // Browser related constants const IS_IOS = () => /iPad|iPhone|iPod/.test(navigator.userAgent) || navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1; const IS_ANDROID = () => /android/i.test(navigator.userAgent); const EVENTS = { MOUSE_DOWN: "mousedown", MOUSE_MOVE: "mousemove", MOUSE_UP: "mouseup", TOUCH_START: "touchstart", TOUCH_MOVE: "touchmove", TOUCH_END: "touchend", WHEEL: "wheel", RESIZE: "resize", CONTEXT_MENU: "contextmenu", MOUSE_ENTER: "mouseenter", MOUSE_LEAVE: "mouseleave", POINTER_DOWN: "pointerdown", POINTER_MOVE: "pointermove", POINTER_UP: "pointerup", POINTER_ENTER: "pointerenter", POINTER_LEAVE: "pointerleave", LOAD: "load", ERROR: "error", CLICK: "click", DOUBLE_CLICK: "dblclick", CONTEXT_LOST: "webglcontextlost", CONTEXT_RESTORED: "webglcontextrestored" }; const CURSOR = { GRAB: "grab", GRABBING: "grabbing", NONE: "" }; // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent.button var MOUSE_BUTTON; (function (MOUSE_BUTTON) { MOUSE_BUTTON[MOUSE_BUTTON["LEFT"] = 0] = "LEFT"; MOUSE_BUTTON[MOUSE_BUTTON["MIDDLE"] = 1] = "MIDDLE"; MOUSE_BUTTON[MOUSE_BUTTON["RIGHT"] = 2] = "RIGHT"; })(MOUSE_BUTTON || (MOUSE_BUTTON = {})); const ANONYMOUS = "anonymous"; const EL_DIV = "div"; const EL_BUTTON = "button"; /* * Copyright (c) 2020 NAVER Corp. * egjs projects are licensed under the MIT license */ /** * "auto" * @type {"auto"} */ const AUTO = "auto"; /** * Event type object with event name strings of {@link View3D} * @type {object} * @property {"ready"} READY {@link /docs/events/ready Ready event} * @property {"loadStart"} LOAD_START {@link /docs/events/loadStart Load start event} * @property {"load"} LOAD {@link /docs/events/load Load event} * @property {"loadError"} LOAD_ERROR {@link /docs/events/loadError Load error event} * @property {"resize"} RESIZE {@link /docs/events/resize Resize event} * @property {"beforeRender"} BEFORE_RENDER {@link /docs/events/beforeRender Before render event} * @property {"render"} RENDER {@link /docs/events/render Render event} * @property {"progress"} PROGRESS {@link /docs/events/progress Progress event} * @property {"inputStart"} INPUT_START {@link /docs/events/inputStart Input start event} * @property {"inputEnd"} INPUT_END {@link /docs/events/inputEnd Input end event} * @property {"cameraChange"} CAMERA_CHANGE {@link /docs/events/cameraChange Camera change event} * @property {"animationStart"} ANIMATION_START {@link /docs/events/animationStart Animation start event} * @property {"animationLoop"} ANIMATION_LOOP {@link /docs/events/animationLoop Animation loop event} * @property {"animationFinished"} ANIMATION_FINISHED {@link /docs/events/animationFinished Animation finished event} * @property {"annotationFocus"} ANNOTATION_FOCUS {@link /docs/events/annotationFocus Annotation focus event} * @property {"annotationUnfocus"} ANNOTATION_UNFOCUS {@link /docs/events/annotationUnfocus Annotation unfocus event} * @property {"quickLookTap"} QUICK_LOOK_TAP {@link /docs/events/quickLookTap Quick Look Tap event} * @property {"arStart"} AR_START {@link /docs/events/arStart AR start evemt} * @property {"arEnd"} AR_END {@link /docs/events/arEnd AR end event} * @property {"arModelPlaced"} AR_MODEL_PLACED {@link /docs/events/arModelPlaced AR model placed event} * @example * ```ts * import { EVENTS } from "@egjs/view3d"; * EVENTS.RESIZE; // "resize" * ``` */ const EVENTS$1 = { READY: "ready", LOAD_START: "loadStart", LOAD: "load", LOAD_ERROR: "loadError", LOAD_FINISH: "loadFinish", MODEL_CHANGE: "modelChange", RESIZE: "resize", BEFORE_RENDER: "beforeRender", RENDER: "render", PROGRESS: "progress", INPUT_START: "inputStart", INPUT_END: "inputEnd", CAMERA_CHANGE: "cameraChange", ANIMATION_START: "animationStart", ANIMATION_LOOP: "animationLoop", ANIMATION_FINISHED: "animationFinished", ANNOTATION_FOCUS: "annotationFocus", ANNOTATION_UNFOCUS: "annotationUnfocus", AR_START: "arStart", AR_END: "arEnd", AR_MODEL_PLACED: "arModelPlaced", QUICK_LOOK_TAP: "quickLookTap" }; /** * Collection of predefined easing functions * @type {object} * @property {function} SINE_WAVE * @property {function} EASE_OUT_CUBIC * @property {function} EASE_OUT_BOUNCE * @example * ```ts * import View3D, { EASING } from "@egjs/view3d"; * * new RotateControl({ * easing: EASING.EASE_OUT_CUBIC, * }); * ``` */ const EASING = { SINE_WAVE: x => Math.sin(x * Math.PI * 2), EASE_OUT_CUBIC: x => 1 - Math.pow(1 - x, 3), EASE_OUT_BOUNCE: x => { const n1 = 7.5625; const d1 = 2.75; if (x < 1 / d1) { return n1 * x * x; } else if (x < 2 / d1) { return n1 * (x -= 1.5 / d1) * x + 0.75; } else if (x < 2.5 / d1) { return n1 * (x -= 2.25 / d1) * x + 0.9375; } else { return n1 * (x -= 2.625 / d1) * x + 0.984375; } } }; /** * Default class names that View3D uses * @type {object} * @property {"view3d-wrapper"} WRAPPER A class name for wrapper element * @property {"view3d-canvas"} CANVAS A class name for canvas element * @property {"view3d-poster"} POSTER A class name for poster element * @property {"view3d-ar-overlay"} AR_OVERLAY A class name for AR overlay element * @property {"view3d-annotation-wrapper"} ANNOTATION_WRAPPER A class name for annotation wrapper element * @property {"view3d-annotation"} ANNOTATION A class name for annotation element * @property {"default"} ANNOTATION_DEFAULT A class name for default style annotation element * @property {"selected"} ANNOTATION_SELECTED A class name for selected annotation element * @property {"flip-x"} ANNOTATION_FLIP_X A class name for annotation element which has tooltip on the left side * @property {"flip-y"} ANNOTATION_FLIP_Y A class name for annotation element which has tooltip on the bottom side * @property {"ctx-lost"} CTX_LOST A class name for canvas element which will be added on context lost */ const DEFAULT_CLASS = { WRAPPER: "view3d-wrapper", CANVAS: "view3d-canvas", POSTER: "view3d-poster", AR_OVERLAY: "view3d-ar-overlay", ANNOTATION_WRAPPER: "view3d-annotation-wrapper", ANNOTATION: "view3d-annotation", ANNOTATION_TOOLTIP: "view3d-annotation-tooltip", ANNOTATION_DEFAULT: "default", ANNOTATION_SELECTED: "selected", ANNOTATION_HIDDEN: "hidden", ANNOTATION_FLIP_X: "flip-x", ANNOTATION_FLIP_Y: "flip-y", CTX_LOST: "ctx-lost" }; /** * Possible values for the toneMapping option. * This is used to approximate the appearance of high dynamic range (HDR) on the low dynamic range medium of a standard computer monitor or mobile device's screen. * @type {object} * @property {THREE.LinearToneMapping} LINEAR * @property {THREE.ReinhardToneMapping} REINHARD * @property {THREE.CineonToneMapping} CINEON * @property {THREE.ACESFilmicToneMapping} ACES_FILMIC */ const TONE_MAPPING = { LINEAR: THREE.LinearToneMapping, REINHARD: THREE.ReinhardToneMapping, CINEON: THREE.CineonToneMapping, ACES_FILMIC: THREE.ACESFilmicToneMapping }; /** * Types of zoom control * @type {object} * @property {"fov"} FOV Zoom by chaning fov(field-of-view). This will prevent camera from going inside the model. * @property {"distance"} DISTANCE Zoom by changing camera distance from the model. */ const ZOOM_TYPE = { FOV: "fov", DISTANCE: "distance" }; /** * Available AR session types * @type {object} * @property {"WebXR"} WEBXR An AR session based on {@link https://developer.mozilla.org/en-US/docs/Web/API/WebXR_Device_API WebXR Device API} * @property {"SceneViewer"} SCENE_VIEWER An AR session based on {@link https://developers.google.com/ar/develop/java/scene-viewer Google SceneViewer}, which is only available in Android * @property {"QuickLook"} QUICK_LOOK An AR session based on Apple {@link https://developer.apple.com/augmented-reality/quick-look/ AR Quick Look}, which is only available in iOS */ const AR_SESSION_TYPE = { WEBXR: "webAR", SCENE_VIEWER: "sceneViewer", QUICK_LOOK: "quickLook" }; /** * @type {object} * @property {"ar_only"} ONLY_AR * @property {"3d_only"} ONLY_3D * @property {"ar_preferred"} PREFER_AR * @property {"3d_preferred"} PREFER_3D */ const SCENE_VIEWER_MODE = { ONLY_AR: "ar_only", ONLY_3D: "3d_only", PREFER_AR: "ar_preferred", PREFER_3D: "3d_preferred" }; /** * <img src="https://docs-assets.developer.apple.com/published/b122cc68df/10cb0534-e1f6-42ed-aadb-5390c55ad3ff.png" /> * @see https://developer.apple.com/documentation/arkit/adding_an_apple_pay_button_or_a_custom_action_in_ar_quick_look * @property {"plain"} PLAIN * @property {"pay"} PAY * @property {"buy"} BUY * @property {"check-out"} CHECK_OUT * @property {"book"} BOOK * @property {"donate"} DONATE * @property {"subscribe"} SUBSCRIBE */ const QUICK_LOOK_APPLE_PAY_BUTTON_TYPE = { PLAIN: "plain", PAY: "pay", BUY: "buy", CHECK_OUT: "check-out", BOOK: "book", DONATE: "donate", SUBSCRIBE: "subscribe" }; /** * Available size of the custom banner * @type {object} * @property {"small"} SMALL 81pt * @property {"medium"} MEDIUM 121pt * @property {"large"} LARGE 161pt */ const QUICK_LOOK_CUSTOM_BANNER_SIZE = { SMALL: "small", MEDIUM: "medium", LARGE: "large" }; /** * Input types * @type {object} * @property {0} ROTATE Rotate input * @property {1} TRANSLATE Translate input * @property {2} ZOOM Zoom input */ const INPUT_TYPE = { ROTATE: 0, TRANSLATE: 1, ZOOM: 2 }; /** * Animation repeat modes * @type {object} * @property {"one"} ONE Repeat single animation * @property {"none"} NONE Pause on animation's last frame * @property {"all"} ALL Repeat all animations */ const ANIMATION_REPEAT_MODE = { ONE: "one", NONE: "none", ALL: "all" }; /* * Copyright (c) 2020 NAVER Corp. * egjs projects are licensed under the MIT license */ /** * Renderer that renders View3D's Scene */ class Renderer { /** * Create new Renderer instance * @param {View3D} view3D An instance of View3D */ constructor(view3D) { this._defaultRenderLoop = delta => { const view3D = this._view3D; const { control, autoPlayer, animator } = view3D; if (!animator.animating && !control.animating && !autoPlayer.animating) return; this._renderFrame(delta); }; this._onContextLost = () => { const canvas = this._canvas; canvas.classList.add(DEFAULT_CLASS.CTX_LOST); }; this._onContextRestore = () => { const canvas = this._canvas; const scene = this._view3D.scene; canvas.classList.remove(DEFAULT_CLASS.CTX_LOST); scene.initTextures(); this.renderSingleFrame(); }; const canvas = findCanvas(view3D.rootEl, view3D.canvasSelector); this._canvas = canvas; this._view3D = view3D; this._renderQueued = false; const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true, preserveDrawingBuffer: true }); renderer.toneMapping = view3D.toneMapping; renderer.toneMappingExposure = view3D.exposure; renderer.outputEncoding = THREE.sRGBEncoding; renderer.setClearColor(0x000000, 0); this._halfFloatAvailable = checkHalfFloatAvailable(renderer); this._renderer = renderer; this._clock = new THREE.Clock(false); this._canvasSize = new THREE.Vector2(); canvas.addEventListener(EVENTS.CONTEXT_LOST, this._onContextLost); canvas.addEventListener(EVENTS.CONTEXT_RESTORED, this._onContextRestore); } // private motionPass: MotionBlurPass; /** * {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement HTMLCanvasElement} given when creating View3D instance * @type HTMLCanvasElement * @readonly */ get canvas() { return this._canvas; } /** * Current {@link https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext WebGLRenderingContext} * @type WebGLRenderingContext * @readonly */ get context() { return this._renderer.getContext(); } /** * Three.js {@link https://threejs.org/docs/#api/en/renderers/WebGLRenderer WebGLRenderer} instance * @type THREE.WebGLRenderer * @readonly */ get threeRenderer() { return this._renderer; } /** * Default render loop of View3D * @type {function} * @readonly */ get defaultRenderLoop() { return this._defaultRenderLoop; } /** * The rendering width and height of the canvas * @type {object} * @param {number} width Width of the canvas * @param {number} height Height of the canvas * @readonly */ get size() { const renderingSize = this._renderer.getSize(new THREE.Vector2()); return { width: renderingSize.width, height: renderingSize.y }; } /** * Canvas element's actual size * @type THREE.Vector2 * @readonly */ get canvasSize() { return this._canvasSize; } /** * An object containing details about the capabilities of the current RenderingContext. * Merged with three.js WebGLRenderer's capabilities. */ get capabilities() { const renderer = this._renderer; return Object.assign(Object.assign({}, renderer.capabilities), { halfFloat: this._halfFloatAvailable }); } effectsOn(effects) { this._effectsOn = effects; } setBlenMixRatio(mixRatio) { this.blendPass.uniforms["mixRatio"].value = mixRatio; } /** * Destroy the renderer and stop active animation loop */ destroy() { const canvas = this._canvas; this.stopAnimationLoop(); this._renderer.dispose(); canvas.removeEventListener(EVENTS.CONTEXT_LOST, this._onContextLost); canvas.removeEventListener(EVENTS.CONTEXT_RESTORED, this._onContextRestore); } /** * Resize the renderer based on current canvas width / height * @returns {void} */ resize() { const renderer = this._renderer; const canvas = this._canvas; if (renderer.xr.isPresenting) return; const width = canvas.clientWidth || 1; const height = canvas.clientHeight || 1; renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(width, height, false); this._canvasSize.set(width, height); } setAnimationLoop(callback) { const view3D = this._view3D; const clock = this._clock; const canvas = this._canvas; // Render Pass Setup this.renderPass = new RenderPass(view3D.scene.root, view3D.camera.threeCamera); const gammaCorrectionPass = new ShaderPass(GammaCorrectionShader); const renderTargetParameters = { colorSpace: "srgb", stencilBuffer: false }; const renderTarget = new THREE.WebGLRenderTarget((canvas.clientWidth || 1) * window.devicePixelRatio, (canvas.clientHeight || 1) * window.devicePixelRatio, renderTargetParameters); // save pass const savePass = new SavePass(renderTarget); // blend pass this.blendPass = new ShaderPass(BlendShader, "tDiffuse1"); this.blendPass.uniforms["tDiffuse2"].value = savePass.renderTarget.texture; this.blendPass.uniforms["mixRatio"].value = 0.0; // output pass const outputPass = new ShaderPass(CopyShader); outputPass.renderToScreen = true; this.composer = new EffectComposer(this._renderer); this.composer.setSize(window.innerWidth, window.innerHeight); this.composer.setPixelRatio(window.devicePixelRatio); this.composer.addPass(this.renderPass); this.composer.addPass(gammaCorrectionPass); this.composer.addPass(this.blendPass); this.composer.addPass(savePass); this.composer.addPass(outputPass); clock.start(); this._renderer.setAnimationLoop((timestamp, frame) => { const delta = Math.min(clock.getDelta(), view3D.maxDeltaTime); callback(delta, frame); }); } stopAnimationLoop() { this._clock.stop(); // See https://threejs.org/docs/#api/en/renderers/WebGLRenderer.setAnimationLoop this._renderer.setAnimationLoop(null); } renderSingleFrame(immediate = false) { const renderer = this._renderer; if (!renderer.xr.isPresenting) { if (immediate) { this._renderFrame(0); } else if (!this._renderQueued) { requestAnimationFrame(() => { this._renderFrame(0); }); this._renderQueued = true; } } } _renderFrame(delta) { const view3D = this._view3D; const threeRenderer = this._renderer; const { scene, camera, control, autoPlayer, animator, annotation } = vi