diff --git a/examples/assets/cube-luts/lut-blue.png b/examples/assets/cube-luts/lut-blue.png
new file mode 100644
index 00000000000..0d16c8febee
Binary files /dev/null and b/examples/assets/cube-luts/lut-blue.png differ
diff --git a/examples/src/examples/graphics/hdr.controls.mjs b/examples/src/examples/graphics/hdr.controls.mjs
index d47cfca31eb..55a350384fc 100644
--- a/examples/src/examples/graphics/hdr.controls.mjs
+++ b/examples/src/examples/graphics/hdr.controls.mjs
@@ -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,
@@ -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
+ })
)
);
};
diff --git a/examples/src/examples/graphics/hdr.example.mjs b/examples/src/examples/graphics/hdr.example.mjs
index 29a0f314312..46d7371a599 100644
--- a/examples/src/examples/graphics/hdr.example.mjs
+++ b/examples/src/examples/graphics/hdr.example.mjs
@@ -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 = {
@@ -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
@@ -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
});
});
diff --git a/scripts/esm/camera-frame.mjs b/scripts/esm/camera-frame.mjs
index c6c6d59c3b8..9584d66541f 100644
--- a/scripts/esm/camera-frame.mjs
+++ b/scripts/esm/camera-frame.mjs
@@ -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
@@ -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;
@@ -359,6 +383,12 @@ class CameraFrame extends Script {
*/
grading = new Grading();
+ /**
+ * @attribute
+ * @type {ColorLUT}
+ */
+ colorLUT = new ColorLUT();
+
/**
* @attribute
* @type {Vignette}
@@ -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;
@@ -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;
diff --git a/src/extras/render-passes/camera-frame.js b/src/extras/render-passes/camera-frame.js
index 92cb2ea9843..1cda470d2ad 100644
--- a/src/extras/render-passes/camera-frame.js
+++ b/src/extras/render-passes/camera-frame.js
@@ -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'
*/
/**
@@ -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
@@ -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.
*
@@ -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;
diff --git a/src/extras/render-passes/render-pass-compose.js b/src/extras/render-passes/render-pass-compose.js
index 6ba03370be4..4d94593e567 100644
--- a/src/extras/render-passes/render-pass-compose.js
+++ b/src/extras/render-passes/render-pass-compose.js
@@ -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.
*
@@ -63,6 +67,13 @@ class RenderPassCompose extends RenderPassShaderQuad {
_gammaCorrection = GAMMA_SRGB;
+ /**
+ * @type {Texture|null}
+ */
+ _colorLUT = null;
+
+ colorLUTIntensity = 1;
+
_key = '';
_debug = null;
@@ -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) {
@@ -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;
@@ -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'}` +
@@ -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);
@@ -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]);
}
diff --git a/src/scene/shader-lib/glsl/chunks/render-pass/frag/compose/compose-color-lut.js b/src/scene/shader-lib/glsl/chunks/render-pass/frag/compose/compose-color-lut.js
new file mode 100644
index 00000000000..c0ee7640cd3
--- /dev/null
+++ b/src/scene/shader-lib/glsl/chunks/render-pass/frag/compose/compose-color-lut.js
@@ -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
+`;
diff --git a/src/scene/shader-lib/glsl/chunks/render-pass/frag/compose/compose.js b/src/scene/shader-lib/glsl/chunks/render-pass/frag/compose/compose.js
index aaf18824115..989cb1932a5 100644
--- a/src/scene/shader-lib/glsl/chunks/render-pass/frag/compose/compose.js
+++ b/src/scene/shader-lib/glsl/chunks/render-pass/frag/compose/compose.js
@@ -13,6 +13,7 @@ export default /* glsl */`
#include "composeVignettePS"
#include "composeFringingPS"
#include "composeCasPS"
+ #include "composeColorLutPS"
void main() {
vec2 uv = uv0;
@@ -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);
diff --git a/src/scene/shader-lib/glsl/collections/compose-chunks-glsl.js b/src/scene/shader-lib/glsl/collections/compose-chunks-glsl.js
index f1ba473b422..fbf2e2e071a 100644
--- a/src/scene/shader-lib/glsl/collections/compose-chunks-glsl.js
+++ b/src/scene/shader-lib/glsl/collections/compose-chunks-glsl.js
@@ -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,
@@ -15,5 +16,6 @@ export const composeChunksGLSL = {
composeGradingPS,
composeVignettePS,
composeFringingPS,
- composeCasPS
+ composeCasPS,
+ composeColorLutPS
};
diff --git a/src/scene/shader-lib/wgsl/chunks/render-pass/frag/compose/compose-color-lut.js b/src/scene/shader-lib/wgsl/chunks/render-pass/frag/compose/compose-color-lut.js
new file mode 100644
index 00000000000..1dea54844b5
--- /dev/null
+++ b/src/scene/shader-lib/wgsl/chunks/render-pass/frag/compose/compose-color-lut.js
@@ -0,0 +1,39 @@
+export default /* wgsl */`
+ #ifdef COLOR_LUT
+ var colorLUT: texture_2d;
+ 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
+`;
diff --git a/src/scene/shader-lib/wgsl/chunks/render-pass/frag/compose/compose.js b/src/scene/shader-lib/wgsl/chunks/render-pass/frag/compose/compose.js
index a0ccc11c9b2..d238e5d329f 100644
--- a/src/scene/shader-lib/wgsl/chunks/render-pass/frag/compose/compose.js
+++ b/src/scene/shader-lib/wgsl/chunks/render-pass/frag/compose/compose.js
@@ -14,6 +14,7 @@ export default /* wgsl */`
#include "composeVignettePS"
#include "composeFringingPS"
#include "composeCasPS"
+ #include "composeColorLutPS"
@fragment
fn fragmentMain(input: FragmentInput) -> FragmentOutput {
@@ -61,6 +62,11 @@ export default /* wgsl */`
// 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);
diff --git a/src/scene/shader-lib/wgsl/collections/compose-chunks-wgsl.js b/src/scene/shader-lib/wgsl/collections/compose-chunks-wgsl.js
index 28fcd531dac..eea437535d5 100644
--- a/src/scene/shader-lib/wgsl/collections/compose-chunks-wgsl.js
+++ b/src/scene/shader-lib/wgsl/collections/compose-chunks-wgsl.js
@@ -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 composeChunksWGSL = {
composePS,
@@ -15,5 +16,6 @@ export const composeChunksWGSL = {
composeGradingPS,
composeVignettePS,
composeFringingPS,
- composeCasPS
+ composeCasPS,
+ composeColorLutPS
};