UNPKG

@google/model-viewer-effects

Version:

Easily add and combine post-processing effects with <model-viewer>!

355 lines 14.5 kB
/* @license * Copyright 2023 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. */ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var _a, _b, _c; import { ReactiveElement } from 'lit'; import { property } from 'lit/decorators.js'; import { EffectComposer as PPEffectComposer, EffectPass, NormalPass, RenderPass, Selection } from 'postprocessing'; import { HalfFloatType, NeutralToneMapping, UnsignedByteType } from 'three'; import { disposeEffectPass, isConvolution, validateLiteralType } from './utilities.js'; export const $scene = Symbol('scene'); export const $composer = Symbol('composer'); export const $modelViewerElement = Symbol('modelViewerElement'); export const $effectComposer = Symbol('effectComposer'); export const $renderPass = Symbol('renderPass'); export const $normalPass = Symbol('normalPass'); export const $effectPasses = Symbol('effectsPass'); export const $requires = Symbol('requires'); export const $effects = Symbol('effects'); export const $selection = Symbol('selection'); export const $onSceneLoad = Symbol('onSceneLoad'); export const $resetEffectPasses = Symbol('resetEffectPasses'); export const $userEffectCount = Symbol('userEffectCount'); export const $tonemapping = Symbol('tonemapping'); const $updateProperties = Symbol('updateProperties'); /** * Light wrapper around {@link EffectComposer} for storing the `scene` and * `camera at a top level, and setting them for every {@link Pass} added. */ export class EffectComposer extends PPEffectComposer { constructor(renderer, options) { super(renderer, options); this[_a] = NeutralToneMapping; } preRender() { // the EffectComposer expects autoClear to be false so that buffers aren't // cleared between renders while the threeRenderer should be true so that // the frames are cleared each render. const renderer = this.getRenderer(); renderer.autoClear = false; renderer.toneMapping = this[$tonemapping]; } postRender() { const renderer = this.getRenderer(); renderer.toneMapping = NeutralToneMapping; renderer.autoClear = true; } render(deltaTime) { this.preRender(); super.render(deltaTime); this.postRender(); } /** * Adds a pass, optionally at a specific index. * Additionally sets `scene` and `camera`. * @param pass A new pass. * @param index An index at which the pass should be inserted. */ addPass(pass, index) { super.addPass(pass, index); this.refresh(); } setMainCamera(camera) { this.camera = camera; super.setMainCamera(camera); } setMainScene(scene) { this.scene = scene; super.setMainScene(scene); } /** * Effect Materials that use the camera need to be manually updated whenever * the camera settings update. */ refresh() { if (this.camera && this.scene) { super.setMainCamera(this.camera); super.setMainScene(this.scene); } } beforeRender(_time, _delta) { var _d; if (this.dirtyRender) { (_d = this.scene) === null || _d === void 0 ? void 0 : _d.queueRender(); } } } _a = $tonemapping; export const RENDER_MODES = ['performance', 'quality']; const N_DEFAULT_PASSES = 2; // RenderPass, NormalPass export class MVEffectComposer extends ReactiveElement { static get is() { return 'effect-composer'; } get [(_b = $userEffectCount, $effectComposer)]() { if (!this[$composer]) throw new Error('The EffectComposer has not been instantiated yet. Please make sure the component is properly mounted on the Document within a <model-viewer> element.'); return this[$composer]; } /** * Array of custom {@link MVPass}'s added with {@link addPass}. */ get userPasses() { return this[$effectComposer].passes.slice(N_DEFAULT_PASSES, N_DEFAULT_PASSES + this[$userEffectCount]); } get modelViewerElement() { if (!this[$modelViewerElement]) throw new Error('<effect-composer> must be a child of a <model-viewer> component.'); return this[$modelViewerElement]; } /** * The Texture buffer of the inbuilt {@link NormalPass}. */ get normalBuffer() { return this[$normalPass].texture; } /** * A selection of all {@link Mesh}'s in the ModelScene. */ get selection() { return this[$selection]; } /** * Creates a new MVEffectComposer element. * * @warning The EffectComposer instance is created only on connection with the * DOM, so that the renderMode is properly taken into account. Do not interact * with this class if it is not mounted to the DOM. */ constructor() { super(); /** * `quality` | `performance`. Changing this after the element was constructed * has no effect. * * Using `quality` improves banding on certain effects, at a memory cost. Use * in HDR scenarios. * * `performance` should be sufficient for most use-cases. * @default 'performance' */ this.renderMode = 'performance'; /** * Anti-Aliasing using the MSAA algorithm. Doesn't work well with depth-based * effects. * * Recommended to use with a factor of 2. * @default 0 */ this.msaa = 0; this[_b] = 0; this[_c] = () => { var _d; this[$effectComposer].refresh(); // Place all Geometries in the selection this[$selection].clear(); (_d = this[$scene]) === null || _d === void 0 ? void 0 : _d.traverse((obj) => obj.hasOwnProperty('geometry') && this[$selection].add(obj)); this.dispatchEvent(new CustomEvent('updated-selection')); }; this[$renderPass] = new RenderPass(); this[$normalPass] = new NormalPass(); this[$selection] = new Selection(); } connectedCallback() { var _d; super.connectedCallback && super.connectedCallback(); if (((_d = this.parentNode) === null || _d === void 0 ? void 0 : _d.nodeName.toLowerCase()) === 'model-viewer') { this[$modelViewerElement] = this.parentNode; } try { validateLiteralType(RENDER_MODES, this.renderMode); } catch (e) { console.error(e.message + '\nrenderMode defaulting to: performance'); } this[$composer] = new EffectComposer(undefined, { stencilBuffer: true, multisampling: this.msaa, frameBufferType: this.renderMode === 'quality' ? HalfFloatType : UnsignedByteType, }); this.modelViewerElement.registerEffectComposer(this[$effectComposer]); this[$effectComposer].addPass(this[$renderPass], 0); this[$effectComposer].addPass(this[$normalPass], 1); this[$onSceneLoad](); this.modelViewerElement.addEventListener('before-render', this[$onSceneLoad]); this.updateEffects(); } disconnectedCallback() { super.disconnectedCallback && super.disconnectedCallback(); this.modelViewerElement.unregisterEffectComposer(); this.modelViewerElement.removeEventListener('before-render', this[$onSceneLoad]); this[$effectComposer].dispose(); } updated(changedProperties) { super.updated(changedProperties); if (changedProperties.has('msaa')) { this[$effectComposer].multisampling = this.msaa; } if (changedProperties.has('renderMode') && changedProperties.get('renderMode') !== undefined) { throw new Error('renderMode cannot be changed after startup.'); } } /** * Adds a custom Pass that extends the {@link Pass} class. * All passes added through this method will be prepended before all other * web-component effects. * * This method automatically sets the `mainScene` and `mainCamera` of the * pass. * @param {Pass} pass Custom Pass to add. The camera and scene are set * automatically. * @param {boolean} requireNormals Whether any effect in this pass uses * the {@link normalBuffer} * @param {boolean} requireDirtyRender Enable this if the effect requires a * render frame every frame. Significant performance impact from enabling * this. */ addPass(pass, requireNormals, requireDirtyRender) { pass.requireNormals = requireNormals; pass.requireDirtyRender = requireDirtyRender; this[$effectComposer].addPass(pass, this[$userEffectCount] + N_DEFAULT_PASSES); // push after current userPasses, before any // web-component effects. this[$userEffectCount]++; // Enable the normalPass and dirtyRendering if required by any effect. this[$updateProperties](); } /** * Removes and optionally disposes of a previously added Pass. * @param pass Custom Pass to remove * @param {Boolean} dispose Disposes of the Pass properties and effects. * Default is `true`. */ removePass(pass, dispose = true) { if (!this[$effectComposer].passes.includes(pass)) throw new Error(`Pass ${pass.name} not found.`); this[$effectComposer].removePass(pass); if (dispose) pass.dispose(); // Enable the normalPass and dirtyRendering if required by any effect. this[$updateProperties](); this[$userEffectCount]--; } /** * Updates all existing EffectPasses, adding any new `<model-viewer-effects>` * Effects in the order they were added, after any custom Passes added * with {@link addPass}. * * Runs automatically whenever a new Effect is added. */ updateEffects() { this[$resetEffectPasses](); // Iterate over all effects (web-component), and combines as many as // possible. Convolution effects must sit on their own EffectPass. In order // to preserve the correct effect order, the convolution effects separate // all effects before and after into separate EffectPasses. const effects = this[$effects]; let i = 0; while (i < effects.length) { const separateIndex = effects.slice(i).findIndex((effect) => effect.requireSeparatePass || isConvolution(effect)); if (separateIndex != 0) { const effectPass = new EffectPass(undefined, ...effects.slice(i, separateIndex == -1 ? effects.length : separateIndex)); this[$effectComposer].addPass(effectPass); } if (separateIndex != -1) { const convolutionPass = new EffectPass(undefined, effects[i + separateIndex]); this[$effectComposer].addPass(convolutionPass); i += separateIndex + 1; } else { break; // A convolution was not found, the first Effect pass contains // all effects from i to effects.length } } // Enable the normalPass and dirtyRendering if required by any effect. this[$updateProperties](); this.queueRender(); } /** * Request a render-frame manually. */ queueRender() { var _d; (_d = this[$scene]) === null || _d === void 0 ? void 0 : _d.queueRender(); } get [$scene]() { return this[$effectComposer].scene; } /** * Gets child effects */ get [$effects]() { // iterate over all web-component children effects const effects = []; for (let i = 0; i < this.children.length; i++) { const childEffect = this.children.item(i); if (!childEffect.effects) continue; const childEffects = childEffect.effects; if (childEffects) { effects.push(...childEffects.filter((effect) => !effect.disabled)); } } return effects; } /** * Gets effectPasses of child effects */ get [$effectPasses]() { return this[$effectComposer].passes.slice(N_DEFAULT_PASSES + this[$userEffectCount]); } [(_c = $onSceneLoad, $updateProperties)]() { this[$normalPass].enabled = this[$requires]('requireNormals'); this[$normalPass].renderToScreen = false; this[$effectComposer].dirtyRender = this[$requires]('requireDirtyRender'); this[$renderPass].renderToScreen = this[$effectComposer].passes.length === N_DEFAULT_PASSES; } [$requires](property) { return this[$effectComposer].passes.some((pass) => pass[property] || (pass.effects && pass.effects.some((effect) => effect[property]))); } [$resetEffectPasses]() { this[$effectPasses].forEach((pass) => { this[$effectComposer].removePass(pass); disposeEffectPass(pass); }); } } __decorate([ property({ type: String, attribute: 'render-mode' }) ], MVEffectComposer.prototype, "renderMode", void 0); __decorate([ property({ type: Number, attribute: 'msaa' }) ], MVEffectComposer.prototype, "msaa", void 0); //# sourceMappingURL=effect-composer.js.map