Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions packages/model-viewer/src/three-components/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export class Renderer extends
ShaderChunk.tonemapping_pars_fragment =
ShaderChunk.tonemapping_pars_fragment.replace(
'vec3 CustomToneMapping( vec3 color ) { return color; }', `
float startCompression = 0.8;
float startCompression = 0.8 - 0.04;
float desaturation = 0.15;
vec3 CustomToneMapping( vec3 color ) {
color *= toneMappingExposure;
Expand All @@ -153,14 +153,11 @@ export class Renderer extends
float peak = max(color.r, max(color.g, color.b));
if (peak < startCompression) return color;

float invPeak = 1. / peak;
float extraBrightness = dot(color * (1. - startCompression * invPeak), vec3(1, 1, 1));

float d = 1. - startCompression;
float newPeak = 1. - d * d / (peak + d - startCompression);
color *= newPeak * invPeak;
color *= newPeak / peak;

float g = 1. - 1. / (desaturation * extraBrightness + 1.);
float g = 1. - 1. / (desaturation * (peak - newPeak) + 1.);
return mix(color, vec3(1, 1, 1), g);
}`);

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/modelviewer.dev/assets/commerce-log.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
204 changes: 55 additions & 149 deletions packages/modelviewer.dev/examples/color.html
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ <h2>Achieving Color-Accurate Presentation with glTF</h2>
<a href="#accuracy">What is color accuracy?</a><br/>
<a href="#matching">What's wrong with the rendered color matching the baseColor?</a><br/>
<a href="#photography">How does rendering compare to photography?</a><br/>
<a href="#tone-mapping">How does tone mapping work?</a><br/>
<a href="#validate-render">How do we validate a glTF 3D render?</a><br/>
<a href="#perception">What role does perception play?</a><br/>
<a href="#validate-gltf">How do we validate a glTF model?</a><br/>
Expand All @@ -145,6 +144,7 @@ <h2>Achieving Color-Accurate Presentation with glTF</h2>
<model-viewer
src="../assets/ShopifyModels/GeoPlanter.glb"
poster="../assets/ShopifyModels/GeoPlanter.webp"
tone-mapping="commerce"
shadow-intensity="1"
camera-controls
alt="3D model of a cactus"
Expand Down Expand Up @@ -296,7 +296,7 @@ <h3 id="matching">What's wrong with the rendered color matching the baseColor?</
which ours passes. It can be shown with physics that under perfectly uniform
lighting, these white spheres should each uniformly reflect exactly the same
light as is incident from the environment, and hence be indistinguishable
from it.</p>
from the background.</p>

<p>Note this result with the yellow spheres is actually pretty close to the
unlit result - there's almost no discernable difference between shiny and
Expand Down Expand Up @@ -389,118 +389,52 @@ <h3 id="photography">How does rendering compare to photography?</h3>
rest of the scene. In order to maintain perception while compressing down to
SDR, a nonlinear tone mapping curve is used.</p>

<h3 id="tone-mapping">How does tone mapping work?</h3>

<p>Tone mapping is a general term, which can refer to basically any color
conversion function. Even separate operations that a photographer might
apply in post processing, such as gamma correction, saturation, contrast,
and even things like sepia can all be combined into a single resulting
function that here we're calling tone mapping. However, we are only
interested in hue-neutral functions and those are the only type of
tone mapping functions we'll be discussing here. Therefore the focus will be
primarily on luma, or brightness.</p>

<p>The tone mapping function used by <code>&lt;model-viewer&gt;</code> is
ACES, which is a standard developed by the film industry and is widely used
for 3D rendering. Like most tone mapping curves, it is fairly linear in the
central focus of its contrast range, then asymptotes out to smoothly
compress the long tails of brights and darks into the required zero to one
output range, the idea being that humans perceive less difference between
over-bright and over-dark zones as compared to the bulk of the scene.
However, since some output range is reserved for these extra-bright
highlights, the range left over to represent the input range of matte
baseColors is also reduced somewhat. This is why the paper-white sphere does
not produce white pixels.</p>

<p>Sometimes when working with matte objects and trying to compare output
color to baseColor, this tone mapping compression will be noticed and
identified as the source of the apparent tone discrepancy. The immediate
thought is usually, let's fix this by not applying tone mapping! The problem
is there is actually no such thing as "no tone mapping", since somehow the
unbounded input range must be converted to the zero-to-one range that the
encoder expects. If this step is not done, the encoder simply clamps the
values, which amounts to a piecewise-linear tone mapping function with sharp
corners that introduce perceptual errors for shiny objects, as shown in the
example below.</p>
<p>Tone mapping comes at the difficult intersection between art and science.
We have found through painful experience that the existing tone mapping
functions do not meet the needs of e-commerce for color-accuracy, and so we
have developed a Commerce tone mapper for exactly this purpose. If you have
precise sRGB color swatches that you have used to create your glTF materials
and want them to come through as unaltered as possible, we strongly
recommend using our "commerce" tone-mapping function and our default
lighting or another suitable grayscale lighting environment.</p>

<p>Below is an example where you can see first-hand how much difference tone
mapping makes. ACES has been a defacto standard in the PBR industry for some
time, but it should be easy to see its serious flaws, including both hue
skews (blue to purple, red to orange) and saturation loss. AgX is a newer
and better tone mapper that holds hue better, but still has significant
contrast and saturation loss, which is desirable for its intended use cases
in games and film. For detail, please see our <a
href="tone-mapping.html">technical document</a> on the tradeoffs in tone
mapping and how our Commerce tone mapper was designed.</p>

<figure>
<figure>
<model-viewer
id="toneMapping"
src="../../shared-assets/models/silver-gold.gltf"
skybox-image="../../shared-assets/environments/neutral.hdr"
id="tone-mapping"
src="../assets/ShopifyModels/Mixer.glb"
tone-mapping="commerce"
camera-controls
alt="3D model of six example material spheres"
alt="Tone mapping comparisons for difference 3D models"
>
<label for="toneMapped">Tone Mapped: </label>
<input id="toneMapped" type="checkbox">
<p>Tone Mapping:
<select id="tone">
<option value="commerce">Commerce</option>
<option value="aces">ACES</option>
<option value="agx">AgX</option>
</select><br/>
Model:
<select id="model">
<option value="Mixer">Mixer</option>
<option value="GeoPlanter">GeoPlanter</option>
<option value="Chair">Chair</option>
<option value="ToyTrain">ToyTrain</option>
<option value="Canoe">Canoe</option>
</select>
</p>
</model-viewer>
<figcaption>Toggle ACES tone mapping to see the difference it makes.</figcaption>
</figure>

<p>Note that once again the shiny and matte white plastic spheres are
indistinguishable, even without cranking up the exposure. Since half of the
matte white sphere is now rendering pure white, there is no headroom for
shiny highlights. Likewise, the top half of the sphere loses its 3D
appearance since the shading was removed by clamping the values. Tick the
checkbox to go back to ACES tone mapping for a quick comparison. Remember to
look away and back again after switching; another trick of human perception
is how dependent it is on anchoring. The yellow will look washed-out
immediately after switching from saturated yellow, but this perception fades
after looking around.</p>

<p>This example also highlights a second key element of good tone mapping
functions: desaturating overexposed colors. Look at the golden sphere
(lower-left) and compare to the previous version with ACES tone mapping
applied. The baseColor of a metal multiplies the incoming light, so a white
light on a golden sphere produces a yellow reflection (fully saturated
yellow, in this case of a fully saturated baseColor). With clamped tone
mapping, the highlight is indeed saturated yellow, but this does not look
perceptually right, even though you could make the argument it is physically
correct.</p>

<p>Good tone mapping curves like ACES not only compress the luma, but also
push colors toward white the brighter they are. This is why the highlights
on the golden sphere become white instead of yellow. This follows both the
behavior of camera sensors and our eyes when responding to overexposed
colored light. You can see this effect simply by looking at a candle's flame
or a spark, the brightest parts of which tend to look white despite their
color. Nvidia has helpfully provided more <a
href="https://developer.nvidia.com/preparing-real-hdr">details</a> on tone
mapping and HDR for the interested reader.</p>

<p>One final way to avoid tone mapping that is sometimes suggested is to
choose an exposure such that all pixels are inside the [0, 1] range, such
that value clamping is avoided. For matte objects with low-dynamic-range
lighting, this can give semi-decent results, but it breaks down completely
for shiny objects, as shown in the following screenshot.</p>

<figure>
<img src="../assets/ExposureFit.png"/>
<figcaption>Image of the above spheres with no tone mapping and exposure
set to avoid clamping.</figcaption>
<figcaption>Comparison of tone mapping functions for difference models.</figcaption>
</figure>

<p>The trouble is that the specular highlights are orders of magnitude
brighter than the majority of the scene, so to fit them into the output
range requires the exposure to be lowered by more than a factor of 50. This
kills the brightness and contrast of the majority of the scene, because of
just a few small highlights. And this neutral environment does not have very
high dynamic range; if you were to use an outdoor environment that includes
the sun, the exposure would have to be so low that nearly the entire render
would be black.</p>

<p>Everything shown here is rendered to an 8-bit sRGB output, but HDR
displays and formats are getting more common. Might we be able to avoid tone
mapping by keeping the HDR of our raw image in an HDR output format? The
short answer is no, because HDR displays may be high dynamic range compared
to traditional SDR, but they are still orders of magnitude short of what our
eyes experience in the real world, so all the same reasons for tone mapping
still apply. However, it is important to note that the choice of tone
mapping function should be dependent on the output format. Ideally it would
even depend on the display's contrast ratio, its brightness settings, and
the level of ambient lighting around it, but this data is unlikely to be
available.</p>

<h3 id="validate-render">How do we validate a glTF 3D render?</h3>

<p>Hopefully the preceding discussion has convinced you that simply
Expand Down Expand Up @@ -648,6 +582,7 @@ <h3 id="takeaway">What's the takeaway?</h3>
id="environments"
src="../assets/ShopifyModels/Mixer.glb"
skybox-image="../../shared-assets/environments/neutral.hdr"
tone-mapping="commerce"
camera-controls
alt="3D model of a blender"
>
Expand Down Expand Up @@ -678,6 +613,19 @@ <h3 id="takeaway">What's the takeaway?</h3>
</script>

<script type="module">
const tone2MV = document.querySelector('#tone-mapping');
const tone = document.querySelector('#tone');
const model = document.querySelector('#model');

tone.addEventListener('input',() => {
tone2MV.toneMapping = tone.value;
});

model.addEventListener('input',() => {
tone2MV.src = "../assets/ShopifyModels/" + model.value + ".glb";

Check warning

Code scanning / CodeQL

DOM text reinterpreted as HTML

[DOM text](1) is reinterpreted as HTML without escaping meta-characters.
});


const expMV = document.querySelector("#exposure");
const exposureDisplay = document.querySelector("#exposure-value");

Expand All @@ -688,48 +636,6 @@ <h3 id="takeaway">What's the takeaway?</h3>
exposureDisplay.textContent = expMV.exposure;
});

const toneMV = document.querySelector("#toneMapping");
let threeRenderer;
let scene;
for (let p = toneMV; p != null; p = Object.getPrototypeOf(p)) {
const privateAPI = Object.getOwnPropertySymbols(p);
const renderer = privateAPI.find((value) => value.toString() == 'Symbol(renderer)');
const sceneSym = privateAPI.find((value) => value.toString() == 'Symbol(scene)');
if(renderer != null){
threeRenderer = toneMV[renderer].threeRenderer;
}
if(sceneSym != null){
scene = toneMV[sceneSym];
}
if(threeRenderer != null && scene != null){
break;
}
}
// threeRenderer.toneMapping = 1;
// toneMV.exposure = 0.02;

const makeToneMapped = (enabled) => {
const {materials} = toneMV.model;
const privateAPI = Object.getOwnPropertySymbols(materials[0]);
const threeMaterials = privateAPI.find((value) => value.toString() == 'Symbol(correlatedObjects)');

for(const material of materials){
for(const threeMaterial of material[threeMaterials]){
threeMaterial.toneMapped = enabled;
}
}
scene.isDirty = true;
};

const checkbox = document.querySelector('#toneMapped');

toneMV.addEventListener('load', () => {
makeToneMapped(false);
checkbox.addEventListener('change', () => {
makeToneMapped(checkbox.checked);
});
});

const envMV = document.querySelector("#environments");
const envCycle = [
"../../shared-assets/environments/spruit_sunrise_1k_HDR.hdr",
Expand Down
Loading