diff --git a/packages/model-viewer-effects/src/effect-composer.ts b/packages/model-viewer-effects/src/effect-composer.ts index c998b5d700..a5820787db 100644 --- a/packages/model-viewer-effects/src/effect-composer.ts +++ b/packages/model-viewer-effects/src/effect-composer.ts @@ -13,14 +13,15 @@ * limitations under the License. */ -import { ReactiveElement } from 'lit'; -import { EffectComposer as PPEffectComposer, EffectPass, NormalPass, RenderPass, Selection, Pass } from 'postprocessing'; -import { disposeEffectPass, isConvolution, validateLiteralType } from './utilities.js'; -import { ModelViewerElement } from '@google/model-viewer'; -import { IMVEffect, IntegrationOptions, MVEffectBase } from './effects/mixins/effect-base.js'; -import { ModelScene } from '@google/model-viewer/lib/three-components/ModelScene.js'; -import { ACESFilmicToneMapping, Camera, HalfFloatType, ToneMapping, UnsignedByteType, WebGLRenderer } from 'three'; -import { property } from 'lit/decorators.js'; +import {ModelViewerElement} from '@google/model-viewer'; +import {ModelScene} from '@google/model-viewer/lib/three-components/ModelScene.js'; +import {ReactiveElement} from 'lit'; +import {property} from 'lit/decorators.js'; +import {EffectComposer as PPEffectComposer, EffectPass, NormalPass, Pass, RenderPass, Selection} from 'postprocessing'; +import {ACESFilmicToneMapping, Camera, HalfFloatType, NeutralToneMapping, ToneMapping, UnsignedByteType, WebGLRenderer} from 'three'; + +import {IMVEffect, IntegrationOptions, MVEffectBase} from './effects/mixins/effect-base.js'; +import {disposeEffectPass, isConvolution, validateLiteralType} from './utilities.js'; export const $scene = Symbol('scene'); export const $composer = Symbol('composer'); @@ -39,32 +40,30 @@ 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. + * 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 { public camera?: Camera; public scene?: ModelScene; public dirtyRender?: boolean; - [$tonemapping]: ToneMapping = ACESFilmicToneMapping; + [$tonemapping]: ToneMapping = NeutralToneMapping; - constructor( - renderer?: WebGLRenderer, - options?: { - depthBuffer?: boolean; - stencilBuffer?: boolean; - alpha?: boolean; - multisampling?: number; - frameBufferType?: number; - } - ) { + constructor(renderer?: WebGLRenderer, options?: { + depthBuffer?: boolean; + stencilBuffer?: boolean; + alpha?: boolean; + multisampling?: number; + frameBufferType?: number; + }) { super(renderer, options); } private 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. + // 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]; @@ -76,7 +75,7 @@ export class EffectComposer extends PPEffectComposer { renderer.autoClear = true; } - override render(deltaTime?: number | undefined): void { + override render(deltaTime?: number|undefined): void { this.preRender(); super.render(deltaTime); this.postRender(); @@ -104,7 +103,8 @@ export class EffectComposer extends PPEffectComposer { } /** - * Effect Materials that use the camera need to be manually updated whenever the camera settings update. + * Effect Materials that use the camera need to be manually updated whenever + * the camera settings update. */ refresh(): void { if (this.camera && this.scene) { @@ -120,12 +120,12 @@ export class EffectComposer extends PPEffectComposer { } } -export type MVPass = Pass & IntegrationOptions; +export type MVPass = Pass&IntegrationOptions; export const RENDER_MODES = ['performance', 'quality'] as const; -export type RenderMode = typeof RENDER_MODES[number]; +export type RenderMode = typeof RENDER_MODES[number]; -const N_DEFAULT_PASSES = 2; // RenderPass, NormalPass +const N_DEFAULT_PASSES = 2; // RenderPass, NormalPass export class MVEffectComposer extends ReactiveElement { static get is() { @@ -133,34 +133,38 @@ export class MVEffectComposer extends ReactiveElement { } /** - * `quality` | `performance`. Changing this after the element was constructed has no effect. + * `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. + * 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' */ - @property({ type: String, attribute: 'render-mode' }) + @property({type: String, attribute: 'render-mode'}) renderMode: RenderMode = 'performance'; /** - * Anti-Aliasing using the MSAA algorithm. Doesn't work well with depth-based effects. + * Anti-Aliasing using the MSAA algorithm. Doesn't work well with depth-based + * effects. * * Recommended to use with a factor of 2. * @default 0 */ - @property({ type: Number, attribute: 'msaa' }) - msaa: number = 0; - - protected [$composer]?: EffectComposer; - protected [$modelViewerElement]?: ModelViewerElement; - protected [$renderPass]: RenderPass; - protected [$normalPass]: NormalPass; - protected [$selection]: Selection; - protected [$userEffectCount]: number = 0; - - get [$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 element.'); + @property({type: Number, attribute: 'msaa'}) msaa: number = 0; + + protected[$composer]?: EffectComposer; + protected[$modelViewerElement]?: ModelViewerElement; + protected[$renderPass]: RenderPass; + protected[$normalPass]: NormalPass; + protected[$selection]: Selection; + protected[$userEffectCount]: number = 0; + + get[$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 element.'); return this[$composer]; } @@ -168,11 +172,14 @@ export class MVEffectComposer extends ReactiveElement { * Array of custom {@link MVPass}'s added with {@link addPass}. */ get userPasses(): MVPass[] { - return this[$effectComposer].passes.slice(N_DEFAULT_PASSES, N_DEFAULT_PASSES + this[$userEffectCount]); + return this[$effectComposer].passes.slice( + N_DEFAULT_PASSES, N_DEFAULT_PASSES + this[$userEffectCount]); } get modelViewerElement() { - if (!this[$modelViewerElement]) throw new Error(' must be a child of a component.'); + if (!this[$modelViewerElement]) + throw new Error( + ' must be a child of a component.'); return this[$modelViewerElement]; } @@ -193,9 +200,9 @@ export class MVEffectComposer extends ReactiveElement { /** * 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. + * @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(); @@ -212,53 +219,69 @@ export class MVEffectComposer extends ReactiveElement { try { validateLiteralType(RENDER_MODES, this.renderMode); - } catch(e) { - console.error((e as Error).message + "\nrenderMode defaulting to: performance"); + } catch (e) { + console.error( + (e as Error).message + '\nrenderMode defaulting to: performance'); } this[$composer] = new EffectComposer(undefined, { stencilBuffer: true, multisampling: this.msaa, - frameBufferType: this.renderMode === 'quality' ? HalfFloatType : UnsignedByteType, + 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.modelViewerElement.addEventListener( + 'before-render', this[$onSceneLoad]); this.updateEffects(); } disconnectedCallback() { super.disconnectedCallback && super.disconnectedCallback(); this.modelViewerElement.unregisterEffectComposer(); - this.modelViewerElement.removeEventListener('before-render', this[$onSceneLoad]); + this.modelViewerElement.removeEventListener( + 'before-render', this[$onSceneLoad]); this[$effectComposer].dispose(); } - updated(changedProperties: Map) { + updated(changedProperties: Map) { super.updated(changedProperties); if (changedProperties.has('msaa')) { this[$effectComposer].multisampling = this.msaa; } - if (changedProperties.has('renderMode') && changedProperties.get('renderMode') !== undefined) { + 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. + * 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. + * 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: Pass, requireNormals?: boolean, requireDirtyRender?: boolean): void { + addPass(pass: Pass, requireNormals?: boolean, requireDirtyRender?: boolean): + void { (pass as MVPass).requireNormals = requireNormals; (pass as MVPass).requireDirtyRender = requireDirtyRender; - this[$effectComposer].addPass(pass, this[$userEffectCount] + N_DEFAULT_PASSES); // push after current userPasses, before any web-component effects. + 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](); @@ -267,44 +290,55 @@ export class MVEffectComposer extends ReactiveElement { /** * 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`. + * @param {Boolean} dispose Disposes of the Pass properties and effects. + * Default is `true`. */ removePass(pass: Pass, dispose: boolean = true): void { - if (!this[$effectComposer].passes.includes(pass)) throw new Error(`Pass ${pass.name} not found.`); + if (!this[$effectComposer].passes.includes(pass)) + throw new Error(`Pass ${pass.name} not found.`); this[$effectComposer].removePass(pass); - if (dispose) pass.dispose(); + 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 `` Effects - * in the order they were added, after any custom Passes added with {@link addPass}. + * Updates all existing EffectPasses, adding any new `` + * Effects in the order they were added, after any custom Passes added + * with {@link addPass}. * * Runs automatically whenever a new Effect is added. */ updateEffects(): void { 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. + // 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)); + 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)); + 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]); + 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 + break; // A convolution was not found, the first Effect pass contains + // all effects from i to effects.length } } @@ -321,19 +355,20 @@ export class MVEffectComposer extends ReactiveElement { this[$scene]?.queueRender(); } - get [$scene]() { + get[$scene]() { return this[$effectComposer].scene; } /** * Gets child effects */ - get [$effects](): IMVEffect[] { + get[$effects](): IMVEffect[] { // iterate over all web-component children effects const effects: IMVEffect[] = []; for (let i = 0; i < this.children.length; i++) { const childEffect = this.children.item(i) as MVEffectBase; - if (!childEffect.effects) continue; + if (!childEffect.effects) + continue; const childEffects = childEffect.effects; if (childEffects) { effects.push(...childEffects.filter((effect) => !effect.disabled)); @@ -345,15 +380,17 @@ export class MVEffectComposer extends ReactiveElement { /** * Gets effectPasses of child effects */ - get [$effectPasses]() { - return this[$effectComposer].passes.slice(N_DEFAULT_PASSES + this[$userEffectCount]) as EffectPass[]; + get[$effectPasses]() { + return this[$effectComposer].passes.slice( + N_DEFAULT_PASSES + this[$userEffectCount]) as EffectPass[]; } [$onSceneLoad] = (): void => { this[$effectComposer].refresh(); // Place all Geometries in the selection this[$selection].clear(); - this[$scene]?.traverse((obj) => obj.hasOwnProperty('geometry') && this[$selection].add(obj)); + this[$scene]?.traverse( + (obj) => obj.hasOwnProperty('geometry') && this[$selection].add(obj)); this.dispatchEvent(new CustomEvent('updated-selection')); }; @@ -361,13 +398,16 @@ export class MVEffectComposer extends ReactiveElement { 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; + this[$renderPass].renderToScreen = + this[$effectComposer].passes.length === N_DEFAULT_PASSES; } - [$requires](property: 'requireNormals' | 'requireSeparatePass' | 'requireDirtyRender'): boolean { + [$requires](property: 'requireNormals'|'requireSeparatePass'| + 'requireDirtyRender'): boolean { return this[$effectComposer].passes.some( - (pass: any) => pass[property] || (pass.effects && pass.effects.some((effect: IMVEffect) => effect[property])) - ); + (pass: any) => pass[property] || + (pass.effects && + pass.effects.some((effect: IMVEffect) => effect[property]))); } [$resetEffectPasses](): void { diff --git a/packages/model-viewer/src/features/environment.ts b/packages/model-viewer/src/features/environment.ts index 031017c8ce..2cded27c0f 100644 --- a/packages/model-viewer/src/features/environment.ts +++ b/packages/model-viewer/src/features/environment.ts @@ -88,11 +88,10 @@ export const EnvironmentMixin = >( } if (changedProperties.has('toneMapping')) { - this[$scene].toneMapping = (this.toneMapping === 'commerce' || - this.toneMapping === 'neutral') ? - NeutralToneMapping : + this[$scene].toneMapping = this.toneMapping === 'aces' ? + ACESFilmicToneMapping : this.toneMapping === 'agx' ? AgXToneMapping : - ACESFilmicToneMapping; + NeutralToneMapping; this[$needsRender](); } diff --git a/packages/modelviewer.dev/assets/poster-damagedhelmet.webp b/packages/modelviewer.dev/assets/poster-damagedhelmet.webp index 4cb01026b5..809418ebd5 100644 Binary files a/packages/modelviewer.dev/assets/poster-damagedhelmet.webp and b/packages/modelviewer.dev/assets/poster-damagedhelmet.webp differ diff --git a/packages/modelviewer.dev/data/docs.json b/packages/modelviewer.dev/data/docs.json index 41d6f50b7d..501e3cfaee 100644 --- a/packages/modelviewer.dev/data/docs.json +++ b/packages/modelviewer.dev/data/docs.json @@ -897,13 +897,13 @@ { "name": "tone-mapping", "htmlName": "toneMapping", - "description": "Selects the function that compresses the HDR rendering to an SDR image on your screen. ACES is a film industry standard that is commonly used, though it has serious color-accuracy problems. AgX is a new and improved tone mapper seeing broad adoption in film and games. Khronos PBR Neutral ('neutral') is a standard function designed specifically for accurate color reproduction in e-commerce. Our current default is and has been ACES, but in v4.0 this default will change to neutral. The deprecated name commerce is an alias for neutral.", + "description": "Selects the function that compresses the HDR rendering to an SDR image on your screen. ACES is a film industry standard that is commonly used, though it has serious color-accuracy problems. AgX is a new and improved tone mapper seeing broad adoption in film and games. Khronos PBR Neutral ('neutral') is a standard function designed specifically for accurate color reproduction in e-commerce. Our current default is neutral, but prior to v4.0 this default was ACES. The deprecated name commerce is an alias for neutral.", "links": [ "tone-mapping example" ], "default": { - "default": "aces", - "options": "aces, agx, neutral" + "default": "neutral", + "options": "neutral, aces, agx" } }, { diff --git a/packages/modelviewer.dev/examples/annotations/index.html b/packages/modelviewer.dev/examples/annotations/index.html index 5d5cf09402..a684f139e5 100644 --- a/packages/modelviewer.dev/examples/annotations/index.html +++ b/packages/modelviewer.dev/examples/annotations/index.html @@ -107,7 +107,7 @@

display: none; } - +