Skip to content

Commit be89e36

Browse files
mvaligurskyMartin Valigurskywilleastcott
authored
Added color correction using LUT texture to CameraFrame postprocessing (#7720)
* Added color correction using LUT texture to CameraFrame postprocessing * updated script * script update * update script --------- Co-authored-by: Martin Valigursky <[email protected]> Co-authored-by: Will Eastcott <[email protected]>
1 parent 146be5b commit be89e36

File tree

12 files changed

+220
-6
lines changed

12 files changed

+220
-6
lines changed
40.8 KB
Loading

examples/src/examples/graphics/hdr.controls.mjs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as pc from 'playcanvas';
55
* @returns {JSX.Element} The returned JSX Element.
66
*/
77
export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => {
8-
const { BindingTwoWay, BooleanInput, SelectInput, LabelGroup, Panel } = ReactPCUI;
8+
const { BindingTwoWay, BooleanInput, SelectInput, LabelGroup, Panel, SliderInput } = ReactPCUI;
99
return fragment(
1010
jsx(
1111
Panel,
@@ -36,6 +36,17 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => {
3636
{ v: pc.TONEMAP_NEUTRAL, t: 'NEUTRAL' }
3737
]
3838
})
39+
),
40+
jsx(
41+
LabelGroup,
42+
{ text: 'LUT Intensity' },
43+
jsx(SliderInput, {
44+
binding: new BindingTwoWay(),
45+
link: { observer, path: 'data.colorLutIntensity' },
46+
min: 0,
47+
max: 1,
48+
precision: 2
49+
})
3950
)
4051
);
4152
};

examples/src/examples/graphics/hdr.example.mjs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ const assets = {
1515
'texture',
1616
{ url: `${rootPath}/static/assets/cubemaps/helipad-env-atlas.png` },
1717
{ type: pc.TEXTURETYPE_RGBP, mipmaps: false }
18-
)
18+
),
19+
colorLut: new pc.Asset('colorLut', 'texture', { url: `${rootPath}/static/assets/cube-luts/lut-blue.png` })
1920
};
2021

2122
const gfxOptions = {
@@ -163,6 +164,11 @@ assetListLoader.load(() => {
163164
cameraFrame.vignette.outer = 1;
164165
cameraFrame.vignette.curvature = 0.5;
165166
cameraFrame.vignette.intensity = 0.5;
167+
168+
// Apply Color LUT
169+
cameraFrame.colorLUT.texture = assets.colorLut.resource;
170+
cameraFrame.colorLUT.intensity = 1.0;
171+
166172
cameraFrame.update();
167173

168174
// apply UI changes
@@ -178,12 +184,18 @@ assetListLoader.load(() => {
178184
cameraFrame.rendering.toneMapping = value;
179185
cameraFrame.update();
180186
}
187+
188+
if (path === 'data.colorLutIntensity') {
189+
cameraFrame.colorLUT.intensity = value;
190+
cameraFrame.update();
191+
}
181192
});
182193

183194
// set initial values
184195
data.set('data', {
185196
hdr: true,
186-
sceneTonemapping: pc.TONEMAP_ACES
197+
sceneTonemapping: pc.TONEMAP_ACES,
198+
colorLutIntensity: 1.0
187199
});
188200
});
189201

scripts/esm/camera-frame.mjs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
// Camera Frame v 1.1
2+
13
import { CameraFrame as EngineCameraFrame, Script, Color } from 'playcanvas';
24

5+
/**
6+
* @import { Asset } from 'playcanvas';
7+
*/
8+
39
/** @enum {number} */
410
const ToneMapping = {
511
LINEAR: 0, // TONEMAP_LINEAR
@@ -217,6 +223,24 @@ class Grading {
217223
tint = new Color(1, 1, 1, 1);
218224
}
219225

226+
/** @interface */
227+
class ColorLUT {
228+
/**
229+
* @attribute
230+
* @type {Asset}
231+
* @resource texture
232+
*/
233+
texture = null;
234+
235+
/**
236+
* @visibleif {texture}
237+
* @range [0, 1]
238+
* @precision 3
239+
* @step 0.001
240+
*/
241+
intensity = 1;
242+
}
243+
220244
/** @interface */
221245
class Vignette {
222246
enabled = false;
@@ -359,6 +383,12 @@ class CameraFrame extends Script {
359383
*/
360384
grading = new Grading();
361385

386+
/**
387+
* @attribute
388+
* @type {ColorLUT}
389+
*/
390+
colorLUT = new ColorLUT();
391+
362392
/**
363393
* @attribute
364394
* @type {Vignette}
@@ -409,7 +439,7 @@ class CameraFrame extends Script {
409439
postUpdate(dt) {
410440

411441
const cf = this.engineCameraFrame;
412-
const { rendering, bloom, grading, vignette, fringing, taa, ssao, dof } = this;
442+
const { rendering, bloom, grading, vignette, fringing, taa, ssao, dof, colorLUT } = this;
413443

414444
const dstRendering = cf.rendering;
415445
dstRendering.renderFormats.length = 0;
@@ -453,6 +483,15 @@ class CameraFrame extends Script {
453483
dstGrading.tint.copy(grading.tint);
454484
}
455485

486+
// colorLUT
487+
const dstColorLUT = cf.colorLUT;
488+
if (colorLUT.texture?.resource) {
489+
dstColorLUT.texture = colorLUT.texture.resource;
490+
dstColorLUT.intensity = colorLUT.intensity;
491+
} else {
492+
dstColorLUT.texture = null;
493+
}
494+
456495
// vignette
457496
const dstVignette = cf.vignette;
458497
dstVignette.intensity = vignette.enabled ? vignette.intensity : 0;

src/extras/render-passes/camera-frame.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { CameraFrameOptions, RenderPassCameraFrame } from './render-pass-camera-
88
/**
99
* @import { AppBase } from '../../framework/app-base.js'
1010
* @import { CameraComponent } from '../../framework/components/camera/component.js'
11+
* @import { Texture } from '../../platform/graphics/texture.js'
1112
*/
1213

1314
/**
@@ -98,6 +99,14 @@ import { CameraFrameOptions, RenderPassCameraFrame } from './render-pass-camera-
9899
* @property {Color} tint - The tint color of the grading effect. Defaults to white.
99100
*/
100101

102+
/**
103+
* @typedef {Object} ColorLUT
104+
* Properties related to the color lookup table (LUT) effect, a postprocessing technique used to
105+
* apply a color transformation to the image.
106+
* @property {Texture|null} texture - The texture of the color LUT effect. Defaults to null.
107+
* @property {number} intensity - The intensity of the color LUT effect. Defaults to 1.
108+
*/
109+
101110
/**
102111
* @typedef {Object} Vignette
103112
* Properties related to the vignette effect, a postprocessing technique that darkens the image
@@ -224,6 +233,16 @@ class CameraFrame {
224233
tint: new Color(1, 1, 1, 1)
225234
};
226235

236+
/**
237+
* Color LUT settings.
238+
*
239+
* @type {ColorLUT}
240+
*/
241+
colorLUT = {
242+
texture: null,
243+
intensity: 1
244+
};
245+
227246
/**
228247
* Vignette settings.
229248
*
@@ -444,6 +463,9 @@ class CameraFrame {
444463
composePass.gradingTint = grading.tint;
445464
}
446465

466+
composePass.colorLUT = this.colorLUT.texture;
467+
composePass.colorLUTIntensity = this.colorLUT.intensity;
468+
447469
composePass.vignetteEnabled = vignette.intensity > 0;
448470
if (composePass.vignetteEnabled) {
449471
composePass.vignetteInner = vignette.inner;

src/extras/render-passes/render-pass-compose.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import { ShaderUtils } from '../../scene/shader-lib/shader-utils.js';
88
import { composeChunksGLSL } from '../../scene/shader-lib/glsl/collections/compose-chunks-glsl.js';
99
import { composeChunksWGSL } from '../../scene/shader-lib/wgsl/collections/compose-chunks-wgsl.js';
1010

11+
/**
12+
* @import { Texture } from '../../platform/graphics/texture.js';
13+
*/
14+
1115
/**
1216
* Render pass implementation of the final post-processing composition.
1317
*
@@ -63,6 +67,13 @@ class RenderPassCompose extends RenderPassShaderQuad {
6367

6468
_gammaCorrection = GAMMA_SRGB;
6569

70+
/**
71+
* @type {Texture|null}
72+
*/
73+
_colorLUT = null;
74+
75+
colorLUTIntensity = 1;
76+
6677
_key = '';
6778

6879
_debug = null;
@@ -88,6 +99,9 @@ class RenderPassCompose extends RenderPassShaderQuad {
8899
this.sceneTextureInvResId = scope.resolve('sceneTextureInvRes');
89100
this.sceneTextureInvResValue = new Float32Array(2);
90101
this.sharpnessId = scope.resolve('sharpness');
102+
this.colorLUTId = scope.resolve('colorLUT');
103+
this.colorLUTParams = new Float32Array(4);
104+
this.colorLUTParamsId = scope.resolve('colorLUTParams');
91105
}
92106

93107
set debug(value) {
@@ -101,6 +115,17 @@ class RenderPassCompose extends RenderPassShaderQuad {
101115
return this._debug;
102116
}
103117

118+
set colorLUT(value) {
119+
if (this._colorLUT !== value) {
120+
this._colorLUT = value;
121+
this._shaderDirty = true;
122+
}
123+
}
124+
125+
get colorLUT() {
126+
return this._colorLUT;
127+
}
128+
104129
set bloomTexture(value) {
105130
if (this._bloomTexture !== value) {
106131
this._bloomTexture = value;
@@ -236,6 +261,7 @@ class RenderPassCompose extends RenderPassShaderQuad {
236261
`-${this.blurTextureUpscale ? 'dofupscale' : ''}` +
237262
`-${this.ssaoTexture ? 'ssao' : 'nossao'}` +
238263
`-${this.gradingEnabled ? 'grading' : 'nograding'}` +
264+
`-${this.colorLUT ? 'colorlut' : 'nocolorlut'}` +
239265
`-${this.vignetteEnabled ? 'vignette' : 'novignette'}` +
240266
`-${this.fringingEnabled ? 'fringing' : 'nofringing'}` +
241267
`-${this.taaEnabled ? 'taa' : 'notaa'}` +
@@ -253,6 +279,7 @@ class RenderPassCompose extends RenderPassShaderQuad {
253279
if (this.blurTextureUpscale) defines.set('DOF_UPSCALE', true);
254280
if (this.ssaoTexture) defines.set('SSAO', true);
255281
if (this.gradingEnabled) defines.set('GRADING', true);
282+
if (this.colorLUT) defines.set('COLOR_LUT', true);
256283
if (this.vignetteEnabled) defines.set('VIGNETTE', true);
257284
if (this.fringingEnabled) defines.set('FRINGING', true);
258285
if (this.taaEnabled) defines.set('TAA', true);
@@ -299,6 +326,16 @@ class RenderPassCompose extends RenderPassShaderQuad {
299326
this.tintId.setValue([this.gradingTint.r, this.gradingTint.g, this.gradingTint.b]);
300327
}
301328

329+
const lutTexture = this._colorLUT;
330+
if (lutTexture) {
331+
this.colorLUTParams[0] = lutTexture.width;
332+
this.colorLUTParams[1] = lutTexture.height;
333+
this.colorLUTParams[2] = lutTexture.height - 1.0;
334+
this.colorLUTParams[3] = this.colorLUTIntensity;
335+
this.colorLUTParamsId.setValue(this.colorLUTParams);
336+
this.colorLUTId.setValue(lutTexture);
337+
}
338+
302339
if (this._vignetteEnabled) {
303340
this.vignetterParamsId.setValue([this.vignetteInner, this.vignetteOuter, this.vignetteCurvature, this.vignetteIntensity]);
304341
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export default /* glsl */`
2+
#ifdef COLOR_LUT
3+
uniform sampler2D colorLUT;
4+
uniform vec4 colorLUTParams; // width, height, maxColor, intensity
5+
6+
vec3 applyColorLUT(vec3 color) {
7+
vec3 c = clamp(color, 0.0, 1.0);
8+
9+
float width = colorLUTParams.x;
10+
float height = colorLUTParams.y;
11+
float maxColor = colorLUTParams.z;
12+
13+
// Calculate blue axis slice
14+
float cell = c.b * maxColor;
15+
float cell_l = floor(cell);
16+
float cell_h = ceil(cell);
17+
18+
// Half-texel offsets
19+
float half_px_x = 0.5 / width;
20+
float half_px_y = 0.5 / height;
21+
22+
// Red and green offsets within a tile
23+
float r_offset = half_px_x + c.r / height * (maxColor / height);
24+
float g_offset = half_px_y + c.g * (maxColor / height);
25+
26+
// texture coordinates for the two blue slices
27+
vec2 uv_l = vec2(cell_l / height + r_offset, g_offset);
28+
vec2 uv_h = vec2(cell_h / height + r_offset, g_offset);
29+
30+
// Sample both and interpolate
31+
vec3 color_l = texture2DLod(colorLUT, uv_l, 0.0).rgb;
32+
vec3 color_h = texture2DLod(colorLUT, uv_h, 0.0).rgb;
33+
34+
vec3 lutColor = mix(color_l, color_h, fract(cell));
35+
return mix(color, lutColor, colorLUTParams.w);
36+
}
37+
#endif
38+
`;

src/scene/shader-lib/glsl/chunks/render-pass/frag/compose/compose.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export default /* glsl */`
1313
#include "composeVignettePS"
1414
#include "composeFringingPS"
1515
#include "composeCasPS"
16+
#include "composeColorLutPS"
1617
1718
void main() {
1819
vec2 uv = uv0;
@@ -60,6 +61,11 @@ export default /* glsl */`
6061
// Apply Tone Mapping
6162
result = toneMap(result);
6263
64+
// Apply Color LUT after tone mapping, in LDR space
65+
#ifdef COLOR_LUT
66+
result = applyColorLUT(result);
67+
#endif
68+
6369
// Apply Vignette
6470
#ifdef VIGNETTE
6571
result = applyVignette(result, uv);

src/scene/shader-lib/glsl/collections/compose-chunks-glsl.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import composeGradingPS from '../chunks/render-pass/frag/compose/compose-grading
66
import composeVignettePS from '../chunks/render-pass/frag/compose/compose-vignette.js';
77
import composeFringingPS from '../chunks/render-pass/frag/compose/compose-fringing.js';
88
import composeCasPS from '../chunks/render-pass/frag/compose/compose-cas.js';
9+
import composeColorLutPS from '../chunks/render-pass/frag/compose/compose-color-lut.js';
910

1011
export const composeChunksGLSL = {
1112
composePS,
@@ -15,5 +16,6 @@ export const composeChunksGLSL = {
1516
composeGradingPS,
1617
composeVignettePS,
1718
composeFringingPS,
18-
composeCasPS
19+
composeCasPS,
20+
composeColorLutPS
1921
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
export default /* wgsl */`
2+
#ifdef COLOR_LUT
3+
var colorLUT: texture_2d<f32>;
4+
var colorLUTSampler: sampler;
5+
uniform colorLUTParams: vec4f; // width, height, maxColor, intensity
6+
7+
fn applyColorLUT(color: vec3f) -> vec3f {
8+
var c: vec3f = clamp(color, vec3f(0.0), vec3f(1.0));
9+
10+
let width: f32 = uniform.colorLUTParams.x;
11+
let height: f32 = uniform.colorLUTParams.y;
12+
let maxColor: f32 = uniform.colorLUTParams.z;
13+
14+
// Calculate blue axis slice
15+
let cell: f32 = c.b * maxColor;
16+
let cell_l: f32 = floor(cell);
17+
let cell_h: f32 = ceil(cell);
18+
19+
// Half-texel offsets
20+
let half_px_x: f32 = 0.5 / width;
21+
let half_px_y: f32 = 0.5 / height;
22+
23+
// Red and green offsets within a tile
24+
let r_offset: f32 = half_px_x + c.r / height * (maxColor / height);
25+
let g_offset: f32 = half_px_y + c.g * (maxColor / height);
26+
27+
// texture coordinates for the two blue slices
28+
let uv_l: vec2f = vec2f(cell_l / height + r_offset, g_offset);
29+
let uv_h: vec2f = vec2f(cell_h / height + r_offset, g_offset);
30+
31+
// Sample both and interpolate
32+
let color_l: vec3f = textureSampleLevel(colorLUT, colorLUTSampler, uv_l, 0.0).rgb;
33+
let color_h: vec3f = textureSampleLevel(colorLUT, colorLUTSampler, uv_h, 0.0).rgb;
34+
35+
let lutColor: vec3f = mix(color_l, color_h, fract(cell));
36+
return mix(color, lutColor, uniform.colorLUTParams.w);
37+
}
38+
#endif
39+
`;

0 commit comments

Comments
 (0)