Skip to content

Added color correction using LUT texture to CameraFrame postprocessing #7720

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
Binary file added examples/assets/cube-luts/lut-blue.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 12 additions & 1 deletion examples/src/examples/graphics/hdr.controls.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as pc from 'playcanvas';
* @returns {JSX.Element} The returned JSX Element.
*/
export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => {
const { BindingTwoWay, BooleanInput, SelectInput, LabelGroup, Panel } = ReactPCUI;
const { BindingTwoWay, BooleanInput, SelectInput, LabelGroup, Panel, SliderInput } = ReactPCUI;
return fragment(
jsx(
Panel,
Expand Down Expand Up @@ -36,6 +36,17 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => {
{ v: pc.TONEMAP_NEUTRAL, t: 'NEUTRAL' }
]
})
),
jsx(
LabelGroup,
{ text: 'LUT Intensity' },
jsx(SliderInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'data.colorLutIntensity' },
min: 0,
max: 1,
precision: 2
})
)
);
};
16 changes: 14 additions & 2 deletions examples/src/examples/graphics/hdr.example.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ const assets = {
'texture',
{ url: `${rootPath}/static/assets/cubemaps/helipad-env-atlas.png` },
{ type: pc.TEXTURETYPE_RGBP, mipmaps: false }
)
),
colorLut: new pc.Asset('colorLut', 'texture', { url: `${rootPath}/static/assets/cube-luts/lut-blue.png` })
};

const gfxOptions = {
Expand Down Expand Up @@ -163,6 +164,11 @@ assetListLoader.load(() => {
cameraFrame.vignette.outer = 1;
cameraFrame.vignette.curvature = 0.5;
cameraFrame.vignette.intensity = 0.5;

// Apply Color LUT
cameraFrame.colorLUT.texture = assets.colorLut.resource;
cameraFrame.colorLUT.intensity = 1.0;

cameraFrame.update();

// apply UI changes
Expand All @@ -178,12 +184,18 @@ assetListLoader.load(() => {
cameraFrame.rendering.toneMapping = value;
cameraFrame.update();
}

if (path === 'data.colorLutIntensity') {
cameraFrame.colorLUT.intensity = value;
cameraFrame.update();
}
});

// set initial values
data.set('data', {
hdr: true,
sceneTonemapping: pc.TONEMAP_ACES
sceneTonemapping: pc.TONEMAP_ACES,
colorLutIntensity: 1.0
});
});

Expand Down
41 changes: 40 additions & 1 deletion scripts/esm/camera-frame.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
// Camera Frame v 1.1

import { CameraFrame as EngineCameraFrame, Script, Color } from 'playcanvas';

/**
* @import { Asset } from 'playcanvas';
*/

/** @enum {number} */
const ToneMapping = {
LINEAR: 0, // TONEMAP_LINEAR
Expand Down Expand Up @@ -217,6 +223,24 @@ class Grading {
tint = new Color(1, 1, 1, 1);
}

/** @interface */
class ColorLUT {
/**
* @attribute
* @type {Asset}
* @resource texture
*/
texture = null;

/**
* @visibleif {texture}
* @range [0, 1]
* @precision 3
* @step 0.001
*/
intensity = 1;
}

/** @interface */
class Vignette {
enabled = false;
Expand Down Expand Up @@ -359,6 +383,12 @@ class CameraFrame extends Script {
*/
grading = new Grading();

/**
* @attribute
* @type {ColorLUT}
*/
colorLUT = new ColorLUT();

/**
* @attribute
* @type {Vignette}
Expand Down Expand Up @@ -409,7 +439,7 @@ class CameraFrame extends Script {
postUpdate(dt) {

const cf = this.engineCameraFrame;
const { rendering, bloom, grading, vignette, fringing, taa, ssao, dof } = this;
const { rendering, bloom, grading, vignette, fringing, taa, ssao, dof, colorLUT } = this;

const dstRendering = cf.rendering;
dstRendering.renderFormats.length = 0;
Expand Down Expand Up @@ -453,6 +483,15 @@ class CameraFrame extends Script {
dstGrading.tint.copy(grading.tint);
}

// colorLUT
const dstColorLUT = cf.colorLUT;
if (colorLUT.texture?.resource) {
dstColorLUT.texture = colorLUT.texture.resource;
dstColorLUT.intensity = colorLUT.intensity;
} else {
dstColorLUT.texture = null;
}

// vignette
const dstVignette = cf.vignette;
dstVignette.intensity = vignette.enabled ? vignette.intensity : 0;
Expand Down
22 changes: 22 additions & 0 deletions src/extras/render-passes/camera-frame.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CameraFrameOptions, RenderPassCameraFrame } from './render-pass-camera-
/**
* @import { AppBase } from '../../framework/app-base.js'
* @import { CameraComponent } from '../../framework/components/camera/component.js'
* @import { Texture } from '../../platform/graphics/texture.js'
*/

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

/**
* @typedef {Object} ColorLUT
* Properties related to the color lookup table (LUT) effect, a postprocessing technique used to
* apply a color transformation to the image.
* @property {Texture|null} texture - The texture of the color LUT effect. Defaults to null.
* @property {number} intensity - The intensity of the color LUT effect. Defaults to 1.
*/

/**
* @typedef {Object} Vignette
* Properties related to the vignette effect, a postprocessing technique that darkens the image
Expand Down Expand Up @@ -224,6 +233,16 @@ class CameraFrame {
tint: new Color(1, 1, 1, 1)
};

/**
* Color LUT settings.
*
* @type {ColorLUT}
*/
colorLUT = {
texture: null,
intensity: 1
};

/**
* Vignette settings.
*
Expand Down Expand Up @@ -437,6 +456,9 @@ class CameraFrame {
composePass.gradingTint = grading.tint;
}

composePass.colorLUT = this.colorLUT.texture;
composePass.colorLUTIntensity = this.colorLUT.intensity;

composePass.vignetteEnabled = vignette.intensity > 0;
if (composePass.vignetteEnabled) {
composePass.vignetteInner = vignette.inner;
Expand Down
37 changes: 37 additions & 0 deletions src/extras/render-passes/render-pass-compose.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { ShaderUtils } from '../../scene/shader-lib/shader-utils.js';
import { composeChunksGLSL } from '../../scene/shader-lib/glsl/collections/compose-chunks-glsl.js';
import { composeChunksWGSL } from '../../scene/shader-lib/wgsl/collections/compose-chunks-wgsl.js';

/**
* @import { Texture } from '../../platform/graphics/texture.js';
*/

/**
* Render pass implementation of the final post-processing composition.
*
Expand Down Expand Up @@ -63,6 +67,13 @@ class RenderPassCompose extends RenderPassShaderQuad {

_gammaCorrection = GAMMA_SRGB;

/**
* @type {Texture|null}
*/
_colorLUT = null;

colorLUTIntensity = 1;

_key = '';

_debug = null;
Expand All @@ -88,6 +99,9 @@ class RenderPassCompose extends RenderPassShaderQuad {
this.sceneTextureInvResId = scope.resolve('sceneTextureInvRes');
this.sceneTextureInvResValue = new Float32Array(2);
this.sharpnessId = scope.resolve('sharpness');
this.colorLUTId = scope.resolve('colorLUT');
this.colorLUTParams = new Float32Array(4);
this.colorLUTParamsId = scope.resolve('colorLUTParams');
}

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

set colorLUT(value) {
if (this._colorLUT !== value) {
this._colorLUT = value;
this._shaderDirty = true;
}
}

get colorLUT() {
return this._colorLUT;
}

set bloomTexture(value) {
if (this._bloomTexture !== value) {
this._bloomTexture = value;
Expand Down Expand Up @@ -236,6 +261,7 @@ class RenderPassCompose extends RenderPassShaderQuad {
`-${this.blurTextureUpscale ? 'dofupscale' : ''}` +
`-${this.ssaoTexture ? 'ssao' : 'nossao'}` +
`-${this.gradingEnabled ? 'grading' : 'nograding'}` +
`-${this.colorLUT ? 'colorlut' : 'nocolorlut'}` +
`-${this.vignetteEnabled ? 'vignette' : 'novignette'}` +
`-${this.fringingEnabled ? 'fringing' : 'nofringing'}` +
`-${this.taaEnabled ? 'taa' : 'notaa'}` +
Expand All @@ -253,6 +279,7 @@ class RenderPassCompose extends RenderPassShaderQuad {
if (this.blurTextureUpscale) defines.set('DOF_UPSCALE', true);
if (this.ssaoTexture) defines.set('SSAO', true);
if (this.gradingEnabled) defines.set('GRADING', true);
if (this.colorLUT) defines.set('COLOR_LUT', true);
if (this.vignetteEnabled) defines.set('VIGNETTE', true);
if (this.fringingEnabled) defines.set('FRINGING', true);
if (this.taaEnabled) defines.set('TAA', true);
Expand Down Expand Up @@ -299,6 +326,16 @@ class RenderPassCompose extends RenderPassShaderQuad {
this.tintId.setValue([this.gradingTint.r, this.gradingTint.g, this.gradingTint.b]);
}

const lutTexture = this._colorLUT;
if (lutTexture) {
this.colorLUTParams[0] = lutTexture.width;
this.colorLUTParams[1] = lutTexture.height;
this.colorLUTParams[2] = lutTexture.height - 1.0;
this.colorLUTParams[3] = this.colorLUTIntensity;
this.colorLUTParamsId.setValue(this.colorLUTParams);
this.colorLUTId.setValue(lutTexture);
}

if (this._vignetteEnabled) {
this.vignetterParamsId.setValue([this.vignetteInner, this.vignetteOuter, this.vignetteCurvature, this.vignetteIntensity]);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export default /* glsl */`
#ifdef COLOR_LUT
uniform sampler2D colorLUT;
uniform vec4 colorLUTParams; // width, height, maxColor, intensity

vec3 applyColorLUT(vec3 color) {
vec3 c = clamp(color, 0.0, 1.0);

float width = colorLUTParams.x;
float height = colorLUTParams.y;
float maxColor = colorLUTParams.z;

// Calculate blue axis slice
float cell = c.b * maxColor;
float cell_l = floor(cell);
float cell_h = ceil(cell);

// Half-texel offsets
float half_px_x = 0.5 / width;
float half_px_y = 0.5 / height;

// Red and green offsets within a tile
float r_offset = half_px_x + c.r / height * (maxColor / height);
float g_offset = half_px_y + c.g * (maxColor / height);

// texture coordinates for the two blue slices
vec2 uv_l = vec2(cell_l / height + r_offset, g_offset);
vec2 uv_h = vec2(cell_h / height + r_offset, g_offset);

// Sample both and interpolate
vec3 color_l = texture2DLod(colorLUT, uv_l, 0.0).rgb;
vec3 color_h = texture2DLod(colorLUT, uv_h, 0.0).rgb;

vec3 lutColor = mix(color_l, color_h, fract(cell));
return mix(color, lutColor, colorLUTParams.w);
}
#endif
`;
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default /* glsl */`
#include "composeVignettePS"
#include "composeFringingPS"
#include "composeCasPS"
#include "composeColorLutPS"

void main() {
vec2 uv = uv0;
Expand Down Expand Up @@ -60,6 +61,11 @@ export default /* glsl */`
// Apply Tone Mapping
result = toneMap(result);

// Apply Color LUT after tone mapping, in LDR space
#ifdef COLOR_LUT
result = applyColorLUT(result);
#endif

// Apply Vignette
#ifdef VIGNETTE
result = applyVignette(result, uv);
Expand Down
4 changes: 3 additions & 1 deletion src/scene/shader-lib/glsl/collections/compose-chunks-glsl.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import composeGradingPS from '../chunks/render-pass/frag/compose/compose-grading
import composeVignettePS from '../chunks/render-pass/frag/compose/compose-vignette.js';
import composeFringingPS from '../chunks/render-pass/frag/compose/compose-fringing.js';
import composeCasPS from '../chunks/render-pass/frag/compose/compose-cas.js';
import composeColorLutPS from '../chunks/render-pass/frag/compose/compose-color-lut.js';

export const composeChunksGLSL = {
composePS,
Expand All @@ -15,5 +16,6 @@ export const composeChunksGLSL = {
composeGradingPS,
composeVignettePS,
composeFringingPS,
composeCasPS
composeCasPS,
composeColorLutPS
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export default /* wgsl */`
#ifdef COLOR_LUT
var colorLUT: texture_2d<f32>;
var colorLUTSampler: sampler;
uniform colorLUTParams: vec4f; // width, height, maxColor, intensity

fn applyColorLUT(color: vec3f) -> vec3f {
var c: vec3f = clamp(color, vec3f(0.0), vec3f(1.0));

let width: f32 = uniform.colorLUTParams.x;
let height: f32 = uniform.colorLUTParams.y;
let maxColor: f32 = uniform.colorLUTParams.z;

// Calculate blue axis slice
let cell: f32 = c.b * maxColor;
let cell_l: f32 = floor(cell);
let cell_h: f32 = ceil(cell);

// Half-texel offsets
let half_px_x: f32 = 0.5 / width;
let half_px_y: f32 = 0.5 / height;

// Red and green offsets within a tile
let r_offset: f32 = half_px_x + c.r / height * (maxColor / height);
let g_offset: f32 = half_px_y + c.g * (maxColor / height);

// texture coordinates for the two blue slices
let uv_l: vec2f = vec2f(cell_l / height + r_offset, g_offset);
let uv_h: vec2f = vec2f(cell_h / height + r_offset, g_offset);

// Sample both and interpolate
let color_l: vec3f = textureSampleLevel(colorLUT, colorLUTSampler, uv_l, 0.0).rgb;
let color_h: vec3f = textureSampleLevel(colorLUT, colorLUTSampler, uv_h, 0.0).rgb;

let lutColor: vec3f = mix(color_l, color_h, fract(cell));
return mix(color, lutColor, uniform.colorLUTParams.w);
}
#endif
`;
Loading