Skip to content

Commit 89be7ed

Browse files
authored
Merge pull request #723 from pmndrs/cdl
CDL implementation for tone mapping
2 parents 34ba414 + fd724a1 commit 89be7ed

File tree

9 files changed

+382
-28
lines changed

9 files changed

+382
-28
lines changed

manual/assets/js/src/demos/tone-mapping.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from "three";
1212

1313
import {
14+
CDLPreset,
1415
ClearPass,
1516
EffectPass,
1617
GeometryPass,
@@ -21,6 +22,7 @@ import {
2122
} from "postprocessing";
2223

2324
import { GLTF, GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
25+
import { BindingApi } from "@tweakpane/core";
2426
import { Pane } from "tweakpane";
2527
import { SpatialControls } from "spatial-controls";
2628
import * as Utils from "../utils/index.js";
@@ -120,13 +122,25 @@ window.addEventListener("load", () => void load().then((assets) => {
120122

121123
// Settings
122124

125+
const params = {
126+
preset: CDLPreset.DEFAULT
127+
};
128+
129+
let binding: BindingApi | undefined;
130+
123131
const container = document.getElementById("viewport")!;
124132
const pane = new Pane({ container: container.querySelector<HTMLElement>(".tp")! });
125133
const fpsGraph = Utils.createFPSGraph(pane);
126134
const folder = pane.addFolder({ title: "Settings" });
127135
folder.addBinding(light, "intensity", { label: "lightIntensity", min: 0, max: 100, step: 0.1 });
128136
folder.addBinding(renderer, "toneMappingExposure", { min: 0, max: 4, step: 0.01 });
129-
folder.addBinding(effect, "toneMapping", { options: Utils.enumToRecord(ToneMapping) });
137+
138+
folder.addBinding(effect, "toneMapping", { options: Utils.enumToRecord(ToneMapping) })
139+
.on("change", (e) => void (binding!.hidden = (e.value !== ToneMapping.AGX)));
140+
141+
binding = folder.addBinding(params, "preset", { label: "CDL preset", options: Utils.enumToRecord(CDLPreset) })
142+
.on("change", (e) => effect.cdl.applyPreset(e.value));
143+
130144
Utils.addBlendModeBindings(folder, effect.blendMode);
131145

132146
// Resize Handler

manual/content/demos/color-grading/tone-mapping.en.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ const toneMappingEffect = new ToneMappingEffect({
2525
> [!TIP]
2626
> Tone mapping should generally be applied late in a render pipeline, but before anti-aliasing and the final color space conversion.
2727
28+
## Primary Color Grading
29+
30+
The `ToneMappingEffect` uses the American Society of Cinematographers Color Decision List (ASC CDL) format to configure primary
31+
color grading information. This format defines the math for Slope, Offset, Power and Saturation and provides a way to influence the look of the tone-mapped image.
32+
33+
> [!INFO]
34+
> Only `ToneMapping.AGX` currently supports CDL parameters.
35+
2836
## External Resources
2937

3038
* [Tone Mapping Techniques](https://64.github.io/tonemapping)
39+
* [ASC CDL](https://en.wikipedia.org/wiki/ASC_CDL)

src/effects/ToneMappingEffect.ts

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
1+
import { ShaderChunk } from "three";
2+
import { CDLPreset } from "../enums/CDLPreset.js";
13
import { ToneMapping } from "../enums/ToneMapping.js";
4+
import { ColorDecisionList } from "../utils/ColorDecisionList.js";
25
import { Effect } from "./Effect.js";
36

47
import fragmentShader from "./shaders/tone-mapping.frag";
58

6-
const toneMappingOperators = new Map<ToneMapping, string>([
7-
[ToneMapping.LINEAR, "LinearToneMapping(texel)"],
8-
[ToneMapping.REINHARD, "ReinhardToneMapping(texel)"],
9-
[ToneMapping.CINEON, "CineonToneMapping(texel)"],
10-
[ToneMapping.ACES_FILMIC, "ACESFilmicToneMapping(texel)"],
11-
[ToneMapping.AGX, "AgXToneMapping(texel)"],
12-
[ToneMapping.NEUTRAL, "NeutralToneMapping(texel)"],
13-
[ToneMapping.CUSTOM, "CustomToneMapping(texel)"]
14-
]);
15-
169
/**
1710
* ToneMappingEffect options.
1811
*
@@ -29,6 +22,14 @@ export interface ToneMappingEffectOptions {
2922

3023
toneMapping?: ToneMapping;
3124

25+
/**
26+
* A CDL Preset. Only applies to {@link ToneMapping.AGX}.
27+
*
28+
* @defaultValue null
29+
*/
30+
31+
cdlPreset?: CDLPreset | null;
32+
3233
}
3334

3435
/**
@@ -39,18 +40,40 @@ export interface ToneMappingEffectOptions {
3940

4041
export class ToneMappingEffect extends Effect implements ToneMappingEffectOptions {
4142

43+
/**
44+
* ASC CDL settings for primary color grading.
45+
*/
46+
47+
readonly cdl: ColorDecisionList;
48+
4249
/**
4350
* Constructs a new tone mapping effect.
4451
*
4552
* @param options - The options.
4653
*/
4754

48-
constructor({ toneMapping = ToneMapping.AGX }: ToneMappingEffectOptions = {}) {
55+
constructor({ toneMapping = ToneMapping.AGX, cdlPreset = null }: ToneMappingEffectOptions = {}) {
4956

5057
super("ToneMappingEffect");
5158

52-
this.fragmentShader = fragmentShader;
59+
this.fragmentShader = fragmentShader.replace(
60+
// Resolve the #include early to ensure that applyCDL gets prefixed.
61+
// This is only necessary because the shader chunk uses a function from the effect shader.
62+
"#include <tonemapping_pars_fragment>",
63+
ShaderChunk.tonemapping_pars_fragment.replace(
64+
/(color = AgXOutsetMatrix \* color;)/,
65+
"color = applyCDL(color);\n$1"
66+
)
67+
);
68+
69+
this.cdl = new ColorDecisionList();
70+
this.cdl.addEventListener("toggle", () => this.onCDLToggle());
71+
72+
this.input.uniforms.set("cdl", this.cdl.uniform);
73+
this.input.defines.set("USE_CDL", true);
74+
5375
this.toneMapping = toneMapping;
76+
this.cdl.applyPreset(cdlPreset);
5477

5578
}
5679

@@ -65,22 +88,31 @@ export class ToneMappingEffect extends Effect implements ToneMappingEffectOption
6588
if(this.toneMapping !== value) {
6689

6790
const defines = this.input.defines;
68-
defines.clear();
6991
defines.set("TONE_MAPPING", value);
92+
this.setChanged();
7093

71-
const operator = toneMappingOperators.get(value);
94+
}
7295

73-
if(operator === undefined) {
96+
}
7497

75-
throw new Error(`Invalid tone mapping: ${value}`);
98+
/**
99+
* Performs tasks when the CDL is enabled or disabled.
100+
*/
76101

77-
}
102+
private onCDLToggle(): void {
78103

79-
defines.set("toneMapping(texel)", operator);
80-
this.setChanged();
104+
if(this.cdl.enabled) {
105+
106+
this.input.defines.set("USE_CDL", true);
107+
108+
} else {
109+
110+
this.input.defines.delete("USE_CDL");
81111

82112
}
83113

114+
this.setChanged();
115+
84116
}
85117

86118
}
Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,80 @@
1+
#ifdef USE_CDL
2+
3+
struct ColorDecisionList {
4+
vec3 slope;
5+
vec3 offset;
6+
vec3 power;
7+
float saturation;
8+
};
9+
10+
uniform ColorDecisionList cdl;
11+
12+
/**
13+
* Applies ASC CDL v1.2 color grade to input color in an unspecified log or linear space.
14+
*
15+
* @see https://blender.stackexchange.com/a/55239/43930
16+
* @see https://docs.acescentral.com/specifications/acescc/
17+
* @param color - A color in a log space (such as LogC, ACEScc, or AgX Log).
18+
* @return - The transformed color (same color space).
19+
*/
20+
21+
vec3 applyCDL(in vec3 color) {
22+
23+
// ASC CDL v1.2 explicitly requires Rec. 709 luminance coefficients.
24+
float l = dot(color, vec3(0.2126, 0.7152, 0.0722));
25+
vec3 v = max(color * cdl.slope + cdl.offset, 0.0);
26+
vec3 pv = pow(v, cdl.power);
27+
28+
if(v.r > 0.0) { v.r = pv.r; }
29+
if(v.g > 0.0) { v.g = pv.g; }
30+
if(v.b > 0.0) { v.b = pv.b; }
31+
32+
return (v - l) * cdl.saturation + l;
33+
34+
}
35+
36+
#else
37+
38+
#define applyCDL(color) color
39+
40+
#endif
41+
142
#include <tonemapping_pars_fragment>
243

344
vec4 mainImage(const in vec4 inputColor, const in vec2 uv, const in GData gData) {
445

5-
return vec4(toneMapping(inputColor.rgb), inputColor.a);
46+
vec3 result;
47+
48+
#if TONE_MAPPING == 0
49+
50+
result = LinearToneMapping(inputColor.rgb);
51+
52+
#elif TONE_MAPPING == 1
53+
54+
result = ReinhardToneMapping(inputColor.rgb);
55+
56+
#elif TONE_MAPPING == 2
57+
58+
result = CineonToneMapping(inputColor.rgb);
59+
60+
#elif TONE_MAPPING == 3
61+
62+
result = ACESFilmicToneMapping(inputColor.rgb);
63+
64+
#elif TONE_MAPPING == 4
65+
66+
result = AgXToneMapping(inputColor.rgb);
67+
68+
#elif TONE_MAPPING == 5
69+
70+
result = NeutralToneMapping(inputColor.rgb);
71+
72+
#elif TONE_MAPPING == 6
73+
74+
result = CustomToneMapping(inputColor.rgb);
75+
76+
#endif
77+
78+
return vec4(result, inputColor.a);
679

780
}

src/enums/CDLPreset.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* An enumeration of CDL presets for different looks.
3+
*
4+
* @category Enums
5+
*/
6+
7+
export enum CDLPreset {
8+
9+
/**
10+
* The baseline look.
11+
*
12+
* Good for testing, but not well suited for production use due to flat colors.
13+
*/
14+
15+
DEFAULT,
16+
17+
/**
18+
* A warmer look with more vivid colors.
19+
*/
20+
21+
GOLDEN,
22+
23+
/**
24+
* Punchy colors with more saturation and higher contrast.
25+
*/
26+
27+
PUNCHY,
28+
29+
/**
30+
* Adjusted for production use to have a similar feeling of contrast as ACES over a wide range of scenes.
31+
*/
32+
33+
NEEDLE
34+
35+
}

src/enums/ToneMapping.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,37 @@ export enum ToneMapping {
1111
* No tone mapping, only exposure. Colors will be clamped to the output range.
1212
*/
1313

14-
LINEAR,
14+
LINEAR = 0,
1515

1616
/**
1717
* Basic Reinhard tone mapping.
1818
*
1919
* @see https://www.cs.utah.edu/docs/techreports/2002/pdf/UUCS-02-001.pdf
2020
*/
2121

22-
REINHARD,
22+
REINHARD = 1,
2323

2424
/**
2525
* Optimized filmic operator by Jim Hejl and Richard Burgess-Dawson.
2626
*
2727
* @see http://filmicworlds.com/blog/filmic-tonemapping-operators
2828
*/
2929

30-
CINEON,
30+
CINEON = 2,
3131

3232
/**
3333
* ACES filmic tone mapping with a scale of 1.0/0.6.
3434
*/
3535

36-
ACES_FILMIC,
36+
ACES_FILMIC = 3,
3737

3838
/**
3939
* Filmic tone mapping based on Blender's implementation using rec 2020 primaries.
4040
*
4141
* @see https://github.com/EaryChow/AgX
4242
*/
4343

44-
AGX,
44+
AGX = 4,
4545

4646
/**
4747
* Neutral tone mapping by Khronos.
@@ -50,7 +50,7 @@ export enum ToneMapping {
5050
* @see https://modelviewer.dev/examples/tone-mapping
5151
*/
5252

53-
NEUTRAL,
53+
NEUTRAL = 5,
5454

5555
/**
5656
* Custom tone mapping.
@@ -60,6 +60,6 @@ export enum ToneMapping {
6060
* @see https://threejs.org/docs/?q=shader#api/en/renderers/shaders/ShaderChunk
6161
*/
6262

63-
CUSTOM
63+
CUSTOM = 6
6464

6565
}

src/enums/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from "./ColorChannel.js";
2+
export * from "./CDLPreset.js";
23
export * from "./DepthCopyMode.js";
34
export * from "./DepthTestStrategy.js";
45
export * from "./EffectShaderSection.js";

0 commit comments

Comments
 (0)