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;
}
-
+
-
+
@@ -553,6 +553,7 @@
Camera Views.
min-camera-orbit="auto auto 5%"
touch-action="none"
poster="../../assets/SketchfabModels/ThorAndTheMidgardSerpent.webp"
+ tone-mapping="aces"
ar
>
diff --git a/packages/modelviewer.dev/examples/augmentedreality/index.html b/packages/modelviewer.dev/examples/augmentedreality/index.html
index 3fdd1833aa..cf53fc72c4 100644
--- a/packages/modelviewer.dev/examples/augmentedreality/index.html
+++ b/packages/modelviewer.dev/examples/augmentedreality/index.html
@@ -80,7 +80,7 @@
Customize a WebXR Augmented Reality session with HTML, CSS, and JS in Chrome
-
+
diff --git a/packages/modelviewer.dev/examples/color.html b/packages/modelviewer.dev/examples/color.html
index 4c3e69ceb9..38491ec539 100644
--- a/packages/modelviewer.dev/examples/color.html
+++ b/packages/modelviewer.dev/examples/color.html
@@ -144,7 +144,6 @@
Achieving Color-Accurate Presentation with glTF
How does rendering compare to photography?
@@ -603,7 +602,7 @@
What's the takeaway?
id="environments"
src="../assets/ShopifyModels/Mixer.glb"
skybox-image="../../shared-assets/environments/neutral.hdr"
- tone-mapping="neutral"
+
camera-controls
alt="3D model of a blender"
>
diff --git a/packages/modelviewer.dev/examples/lightingandenv/index.html b/packages/modelviewer.dev/examples/lightingandenv/index.html
index 30bc444310..ba1e3a81c7 100644
--- a/packages/modelviewer.dev/examples/lightingandenv/index.html
+++ b/packages/modelviewer.dev/examples/lightingandenv/index.html
@@ -139,7 +139,7 @@
Showing the difference between our two baked-in lighting
-
+
@@ -166,14 +166,14 @@
Showing the difference between our two baked-in lighting
Comparing tone mapping
Tone mapping is the critical last stage of the rendering pipeline that controls the final look of your model. It is necessary because the reflections are often much brighter than a screen can reproduce, so they must be smoothly mapped into the sRGB range, ideally while avoiding clipping artifacts or hue shifts. The image sensor and processing on a digital camera performs a similar step.
-
Khronos PBR Neutral is our new tone mapper, designed specifically for the color accuracy needs of e-commerce. It is guaranteed to avoid all hue shifts, has a relatively sharp rolloff in intensity, and a slower progression to white. This is designed to pass the widest range of base color values through unchanged to the screen, while preserving enough headroom for highlights to show well. Neutral will become our default in our next major release, v4.0.
-
ACES is a film industry standard that is widely used in graphics and is and has been our default tone mapper. However, it produces serious hue shifts and extreme desaturation, making bright yellow and cyan unattainable under any lighting. See for yourself in this example.
+
Khronos PBR Neutral is our new tone mapper, designed specifically for the color accuracy needs of e-commerce. It is guaranteed to avoid all hue shifts, has a relatively sharp rolloff in intensity, and a slower progression to white. This is designed to pass the widest range of base color values through unchanged to the screen, while preserving enough headroom for highlights to show well. Neutral is now our default as of v4.0.
+
ACES is a film industry standard that is widely used in graphics and was our default tone mapper prior to v4.0. However, it produces serious hue shifts and extreme desaturation, making bright yellow and cyan unattainable under any lighting. See for yourself in this example.
AgX is a relatively new tone mapper that is getting a lot of adoption in graphics. It has less hue shifting than ACES and may be a good option for matching existing artist workflows, but has the same drawback of significant desaturation. However, in more artistic scenes this can be beneficial since it allows for a slower intensity rolloff.
For an apples-to-apples comparison of ACES to Neutral with custom lighting, set the Neutral exposure to 1 and the ACES exposure to 0.77 to account for ACES being artificially bright. This compensation is automatic for our built-in lighting.