diff --git a/eslint.config.mjs b/eslint.config.mjs
index 42b1f2f2e92..b68415bc8f8 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -2,6 +2,12 @@ import playcanvasConfig from '@playcanvas/eslint-config';
import babelParser from '@babel/eslint-parser';
import globals from 'globals';
+// Extract or preserve existing JSDoc tags
+const jsdocRule = playcanvasConfig.find(
+ config => config.rules && config.rules['jsdoc/check-tag-names']
+);
+const existingTags = jsdocRule?.rules['jsdoc/check-tag-names'][1]?.definedTags || [];
+
export default [
...playcanvasConfig,
{
@@ -27,7 +33,14 @@ export default [
}
},
rules: {
- 'import/order': 'off'
+ 'import/order': 'off',
+ 'jsdoc/check-tag-names': [
+ 'error',
+ {
+ // custom mjs script tags to not error on, add them to those from parent config
+ definedTags: [...new Set([...existingTags, 'range', 'step', 'precision'])]
+ }
+ ]
}
},
{
diff --git a/examples/src/examples/animation/events.example.mjs b/examples/src/examples/animation/events.example.mjs
index 74f14bcb017..8d3c856d2ed 100644
--- a/examples/src/examples/animation/events.example.mjs
+++ b/examples/src/examples/animation/events.example.mjs
@@ -1,6 +1,5 @@
-import { deviceType, rootPath, fileImport } from 'examples/utils';
+import { deviceType, rootPath } from 'examples/utils';
import * as pc from 'playcanvas';
-const { CameraFrame } = await fileImport(`${rootPath}/static/assets/scripts/misc/camera-frame.mjs`);
const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
window.focus();
@@ -34,13 +33,11 @@ createOptions.componentSystems = [
pc.RenderComponentSystem,
pc.CameraComponentSystem,
pc.LightComponentSystem,
- pc.ScriptComponentSystem,
pc.AnimComponentSystem
];
createOptions.resourceHandlers = [
pc.TextureHandler,
pc.ContainerHandler,
- pc.ScriptHandler,
pc.AnimClipHandler,
pc.AnimStateGraphHandler
];
@@ -76,12 +73,12 @@ assetListLoader.load(() => {
// ------ Custom render passes set up ------
- cameraEntity.addComponent('script');
- const cameraFrame = cameraEntity.script.create(CameraFrame);
+ const cameraFrame = new pc.CameraFrame(app, cameraEntity.camera);
cameraFrame.rendering.toneMapping = pc.TONEMAP_NEUTRAL;
cameraFrame.rendering.samples = 4;
cameraFrame.bloom.enabled = true;
cameraFrame.bloom.intensity = 0.01;
+ cameraFrame.update();
// ------------------------------------------
diff --git a/examples/src/examples/camera/first-person.example.mjs b/examples/src/examples/camera/first-person.example.mjs
index 8dd6c65fde2..e8e7eee742f 100644
--- a/examples/src/examples/camera/first-person.example.mjs
+++ b/examples/src/examples/camera/first-person.example.mjs
@@ -1,9 +1,7 @@
// @config DESCRIPTION
(WASD) Move
(Space) Jump
(Mouse) Look
-import { deviceType, rootPath, fileImport } from 'examples/utils';
+import { deviceType, rootPath } from 'examples/utils';
import * as pc from 'playcanvas';
-const { CameraFrame } = await fileImport(`${rootPath}/static/assets/scripts/misc/camera-frame.mjs`);
-
const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
window.focus();
@@ -148,15 +146,12 @@ cameraEntity.setLocalPosition(0, 0.5, 0);
// ------ Custom render passes set up ------
-cameraEntity.addComponent('script');
-/** @type { CameraFrame } */
-const cameraFrame = cameraEntity.script.create(CameraFrame);
-
+const cameraFrame = new pc.CameraFrame(app, cameraEntity.camera);
cameraFrame.rendering.samples = 4;
cameraFrame.rendering.toneMapping = pc.TONEMAP_ACES2;
-
cameraFrame.bloom.enabled = true;
cameraFrame.bloom.intensity = 0.01;
+cameraFrame.update();
// ------------------------------------------
diff --git a/examples/src/examples/graphics/ambient-occlusion.example.mjs b/examples/src/examples/graphics/ambient-occlusion.example.mjs
index b01146cb31e..a3e59ca99ef 100644
--- a/examples/src/examples/graphics/ambient-occlusion.example.mjs
+++ b/examples/src/examples/graphics/ambient-occlusion.example.mjs
@@ -1,7 +1,6 @@
import { data } from 'examples/observer';
-import { deviceType, rootPath, fileImport } from 'examples/utils';
+import { deviceType, rootPath } from 'examples/utils';
import * as pc from 'playcanvas';
-const { CameraFrame } = await fileImport(`${rootPath}/static/assets/scripts/misc/camera-frame.mjs`);
const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
window.focus();
@@ -168,8 +167,7 @@ assetListLoader.load(() => {
// ------ Custom render passes set up ------
- /** @type { CameraFrame } */
- const cameraFrame = cameraEntity.script.create(CameraFrame);
+ const cameraFrame = new pc.CameraFrame(app, cameraEntity.camera);
cameraFrame.rendering.samples = 4;
cameraFrame.rendering.toneMapping = pc.TONEMAP_NEUTRAL;
@@ -183,6 +181,8 @@ assetListLoader.load(() => {
cameraFrame.ssao.samples = data.get('data.ssao.samples');
cameraFrame.ssao.minAngle = data.get('data.ssao.minAngle');
cameraFrame.ssao.scale = data.get('data.ssao.scale');
+
+ cameraFrame.update();
};
// apply UI changes
diff --git a/examples/src/examples/graphics/clustered-area-lights.example.mjs b/examples/src/examples/graphics/clustered-area-lights.example.mjs
index 5c1ab2dd92d..42af210d94a 100644
--- a/examples/src/examples/graphics/clustered-area-lights.example.mjs
+++ b/examples/src/examples/graphics/clustered-area-lights.example.mjs
@@ -1,7 +1,6 @@
import { data } from 'examples/observer';
-import { deviceType, rootPath, fileImport } from 'examples/utils';
+import { deviceType, rootPath } from 'examples/utils';
import * as pc from 'playcanvas';
-const { CameraFrame } = await fileImport(`${rootPath}/static/assets/scripts/misc/camera-frame.mjs`);
const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
window.focus();
@@ -228,11 +227,11 @@ assetListLoader.load(() => {
app.root.addChild(camera);
// custom render passes
- const cameraFrame = camera.script.create(CameraFrame);
+ const cameraFrame = new pc.CameraFrame(app, camera.camera);
cameraFrame.rendering.samples = 4;
- cameraFrame.bloom.enabled = true;
cameraFrame.bloom.intensity = 0.01;
- cameraFrame.bloom.lastMipLevel = 4;
+ cameraFrame.bloom.blurLevel = 4;
+ cameraFrame.update();
// if the device renders in HDR mode, disable tone mapping to output HDR values without any processing
cameraFrame.rendering.toneMapping = device.isHdr ? pc.TONEMAP_NONE : pc.TONEMAP_NEUTRAL;
diff --git a/examples/src/examples/graphics/dithered-transparency.example.mjs b/examples/src/examples/graphics/dithered-transparency.example.mjs
index ae3077b0d6a..3997f706c1c 100644
--- a/examples/src/examples/graphics/dithered-transparency.example.mjs
+++ b/examples/src/examples/graphics/dithered-transparency.example.mjs
@@ -1,7 +1,6 @@
import { data } from 'examples/observer';
-import { deviceType, rootPath, fileImport } from 'examples/utils';
+import { deviceType, rootPath } from 'examples/utils';
import * as pc from 'playcanvas';
-const { CameraFrame } = await fileImport(`${rootPath}/static/assets/scripts/misc/camera-frame.mjs`);
const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
window.focus();
@@ -159,15 +158,16 @@ assetListLoader.load(() => {
// ------ Custom render passes set up ------
- /** @type { CameraFrame } */
- const cameraFrame = cameraEntity.script.create(CameraFrame);
+ const cameraFrame = new pc.CameraFrame(app, cameraEntity.camera);
cameraFrame.rendering.toneMapping = pc.TONEMAP_ACES;
cameraFrame.rendering.sceneColorMap = true;
cameraFrame.taa.jitter = 1;
+ cameraFrame.update();
const applySettings = () => {
cameraFrame.taa.enabled = data.get('data.taa');
cameraFrame.rendering.sharpness = cameraFrame.taa.enabled ? 1 : 0;
+ cameraFrame.update();
};
// ------
diff --git a/examples/src/examples/graphics/portal.example.mjs b/examples/src/examples/graphics/portal.example.mjs
index 3b4256164c3..95802f84850 100644
--- a/examples/src/examples/graphics/portal.example.mjs
+++ b/examples/src/examples/graphics/portal.example.mjs
@@ -1,6 +1,5 @@
-import { deviceType, rootPath, fileImport } from 'examples/utils';
+import { deviceType, rootPath } from 'examples/utils';
import * as pc from 'playcanvas';
-const { CameraFrame } = await fileImport(`${rootPath}/static/assets/scripts/misc/camera-frame.mjs`);
const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
window.focus();
@@ -159,13 +158,11 @@ assetListLoader.load(() => {
// ------ Custom render passes set up ------
- camera.addComponent('script');
- /** @type { CameraFrame } */
- const cameraFrame = camera.script.create(CameraFrame);
-
+ const cameraFrame = new pc.CameraFrame(app, camera.camera);
cameraFrame.rendering.stencil = true;
cameraFrame.rendering.samples = 4;
cameraFrame.rendering.toneMapping = pc.TONEMAP_ACES2;
+ cameraFrame.update();
// ------------------------------------------
diff --git a/examples/src/examples/graphics/post-processing.controls.mjs b/examples/src/examples/graphics/post-processing.controls.mjs
index 4bac61b5ae5..da87e2cc68e 100644
--- a/examples/src/examples/graphics/post-processing.controls.mjs
+++ b/examples/src/examples/graphics/post-processing.controls.mjs
@@ -86,12 +86,12 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => {
),
jsx(
LabelGroup,
- { text: 'last mip level' },
+ { text: 'blur level' },
jsx(SliderInput, {
binding: new BindingTwoWay(),
- link: { observer, path: 'data.bloom.lastMipLevel' },
+ link: { observer, path: 'data.bloom.blurLevel' },
min: 1,
- max: 10,
+ max: 16,
precision: 0
})
)
diff --git a/examples/src/examples/graphics/post-processing.example.mjs b/examples/src/examples/graphics/post-processing.example.mjs
index 9b73ca371c4..fe0872eeaf6 100644
--- a/examples/src/examples/graphics/post-processing.example.mjs
+++ b/examples/src/examples/graphics/post-processing.example.mjs
@@ -1,7 +1,6 @@
import { data } from 'examples/observer';
-import { deviceType, rootPath, fileImport } from 'examples/utils';
+import { deviceType, rootPath } from 'examples/utils';
import * as pc from 'playcanvas';
-const { CameraFrame } = await fileImport(`${rootPath}/static/assets/scripts/misc/camera-frame.mjs`);
const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
window.focus();
@@ -216,9 +215,9 @@ assetListLoader.load(() => {
// ------ Custom render passes set up ------
- /** @type { CameraFrame } */
- const cameraFrame = cameraEntity.script.create(CameraFrame);
+ const cameraFrame = new pc.CameraFrame(app, cameraEntity.camera);
cameraFrame.rendering.sceneColorMap = true;
+ cameraFrame.update();
const applySettings = () => {
@@ -247,9 +246,8 @@ assetListLoader.load(() => {
cameraFrame.taa.jitter = data.get('data.taa.jitter');
// Bloom
- cameraFrame.bloom.enabled = data.get('data.bloom.enabled');
- cameraFrame.bloom.intensity = pc.math.lerp(0, 0.1, data.get('data.bloom.intensity') / 100);
- cameraFrame.bloom.lastMipLevel = data.get('data.bloom.lastMipLevel');
+ cameraFrame.bloom.intensity = data.get('data.bloom.enabled') ? pc.math.lerp(0, 0.1, data.get('data.bloom.intensity') / 100) : 0;
+ cameraFrame.bloom.blurLevel = data.get('data.bloom.blurLevel');
// grading
cameraFrame.grading.enabled = data.get('data.grading.enabled');
@@ -258,15 +256,16 @@ assetListLoader.load(() => {
cameraFrame.grading.contrast = data.get('data.grading.contrast');
// vignette
- cameraFrame.vignette.enabled = data.get('data.vignette.enabled');
cameraFrame.vignette.inner = data.get('data.vignette.inner');
cameraFrame.vignette.outer = data.get('data.vignette.outer');
cameraFrame.vignette.curvature = data.get('data.vignette.curvature');
- cameraFrame.vignette.intensity = data.get('data.vignette.intensity');
+ cameraFrame.vignette.intensity = data.get('data.vignette.enabled') ? data.get('data.vignette.intensity') : 0;
// fringing
- cameraFrame.fringing.enabled = data.get('data.fringing.enabled');
- cameraFrame.fringing.intensity = data.get('data.fringing.intensity');
+ cameraFrame.fringing.intensity = data.get('data.fringing.enabled') ? data.get('data.fringing.intensity') : 0;
+
+ // apply all settings
+ cameraFrame.update();
};
// apply UI changes
@@ -285,7 +284,7 @@ assetListLoader.load(() => {
bloom: {
enabled: true,
intensity: 5,
- lastMipLevel: 1
+ blurLevel: 16
},
grading: {
enabled: false,
diff --git a/examples/src/examples/graphics/taa.example.mjs b/examples/src/examples/graphics/taa.example.mjs
index df8c9e35c11..a794b78f6b0 100644
--- a/examples/src/examples/graphics/taa.example.mjs
+++ b/examples/src/examples/graphics/taa.example.mjs
@@ -1,7 +1,6 @@
import { data } from 'examples/observer';
-import { deviceType, rootPath, fileImport } from 'examples/utils';
+import { deviceType, rootPath } from 'examples/utils';
import * as pc from 'playcanvas';
-const { CameraFrame } = await fileImport(`${rootPath}/static/assets/scripts/misc/camera-frame.mjs`);
const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
window.focus();
@@ -118,20 +117,21 @@ assetListLoader.load(() => {
// ------ Custom render passes set up ------
- /** @type { CameraFrame } */
- const cameraFrame = cameraEntity.script.create(CameraFrame);
+ const cameraFrame = new pc.CameraFrame(app, cameraEntity.camera);
cameraFrame.rendering.toneMapping = pc.TONEMAP_ACES;
cameraFrame.bloom.intensity = 0.02;
+ cameraFrame.update();
// ------
const applySettings = () => {
- cameraFrame.bloom.enabled = data.get('data.scene.bloom');
+ cameraFrame.bloom.intensity = data.get('data.scene.bloom') ? 0.02 : 0;
cameraFrame.taa.enabled = data.get('data.taa.enabled');
cameraFrame.taa.jitter = data.get('data.taa.jitter');
cameraFrame.rendering.renderTargetScale = data.get('data.scene.scale');
cameraFrame.rendering.sharpness = data.get('data.scene.sharpness');
+ cameraFrame.update();
};
// apply UI changes
diff --git a/examples/assets/scripts/misc/camera-frame.mjs b/scripts/utils/camera-frame.mjs
similarity index 56%
rename from examples/assets/scripts/misc/camera-frame.mjs
rename to scripts/utils/camera-frame.mjs
index 622a1828e69..24497a1767e 100644
--- a/examples/assets/scripts/misc/camera-frame.mjs
+++ b/scripts/utils/camera-frame.mjs
@@ -1,11 +1,4 @@
-import {
- Script,
- Color,
- math,
- CameraFrameOptions,
- RenderPassCameraFrame,
- SSAOTYPE_NONE
-} from 'playcanvas';
+import { CameraFrame as EngineCameraFrame, Script, Color } from 'playcanvas';
/** @enum {number} */
const ToneMapping = {
@@ -154,11 +147,11 @@ class Bloom {
/**
* @attribute
- * @range [0, 12]
+ * @range [0, 16]
* @precision 0
* @step 0
*/
- lastMipLevel = 1;
+ blurLevel = 1;
}
/** @interface */
@@ -292,110 +285,91 @@ class CameraFrame extends Script {
*/
fringing = new Fringing();
- options = new CameraFrameOptions();
+ engineCameraFrame = new EngineCameraFrame(this.app, this.entity.camera);
initialize() {
- this.updateOptions();
- this.createRenderPass();
-
this.on('enable', () => {
- this.createRenderPass();
+ this.engineCameraFrame.enabled = true;
});
this.on('disable', () => {
- this.destroyRenderPass();
+ this.engineCameraFrame.enabled = false;
});
this.on('destroy', () => {
- this.destroyRenderPass();
+ this.engineCameraFrame.destroy();
});
}
- createRenderPass() {
- const cameraComponent = this.entity.camera;
- this.renderPassCamera = new RenderPassCameraFrame(this.app, cameraComponent, this.options);
- cameraComponent.renderPasses = [this.renderPassCamera];
- }
-
- destroyRenderPass() {
- const cameraComponent = this.entity.camera;
- cameraComponent.renderPasses?.forEach((renderPass) => {
- renderPass.destroy();
- });
- cameraComponent.renderPasses = [];
- cameraComponent.rendering = null;
-
- cameraComponent.jitter = 0;
- }
-
- updateOptions() {
-
- const { options, rendering, bloom, taa, ssao } = this;
- options.stencil = rendering.stencil;
- options.samples = rendering.samples;
- options.sceneColorMap = rendering.sceneColorMap;
- options.prepassEnabled = rendering.sceneDepthMap;
- options.bloomEnabled = bloom.enabled;
- options.taaEnabled = taa.enabled;
- options.ssaoType = ssao.type;
- options.ssaoBlurEnabled = ssao.blurEnabled;
- options.formats = [rendering.renderFormat, rendering.renderFormatFallback0, rendering.renderFormatFallback1];
- }
-
postUpdate(dt) {
- const cameraComponent = this.entity.camera;
- const { options, renderPassCamera, rendering, bloom, grading, vignette, fringing, taa, ssao } = this;
-
- // options that can cause the passes to be re-created
- this.updateOptions();
- renderPassCamera.update(options);
-
- // update parameters of individual render passes
- const { composePass, bloomPass, ssaoPass } = renderPassCamera;
-
- renderPassCamera.renderTargetScale = math.clamp(rendering.renderTargetScale, 0.1, 1);
- composePass.toneMapping = rendering.toneMapping;
- composePass.sharpness = rendering.sharpness;
-
- if (options.bloomEnabled && bloomPass) {
- composePass.bloomIntensity = bloom.intensity;
- bloomPass.lastMipLevel = bloom.lastMipLevel;
+ const cf = this.engineCameraFrame;
+ const { rendering, bloom, grading, vignette, fringing, taa, ssao } = this;
+
+ const dstRendering = cf.rendering;
+ dstRendering.renderFormats.length = 0;
+ dstRendering.renderFormats.push(rendering.renderFormat);
+ dstRendering.renderFormats.push(rendering.renderFormatFallback0);
+ dstRendering.renderFormats.push(rendering.renderFormatFallback1);
+ dstRendering.stencil = rendering.stencil;
+ dstRendering.renderTargetScale = rendering.renderTargetScale;
+ dstRendering.samples = rendering.samples;
+ dstRendering.sceneColorMap = rendering.sceneColorMap;
+ dstRendering.sceneDepthMap = rendering.sceneDepthMap;
+ dstRendering.toneMapping = rendering.toneMapping;
+ dstRendering.sharpness = rendering.sharpness;
+
+ // ssao
+ const dstSsao = cf.ssao;
+ dstSsao.type = ssao.type;
+ if (ssao.type !== SsaoType.NONE) {
+ dstSsao.intensity = ssao.intensity;
+ dstSsao.radius = ssao.radius;
+ dstSsao.samples = ssao.samples;
+ dstSsao.power = ssao.power;
+ dstSsao.minAngle = ssao.minAngle;
+ dstSsao.scale = ssao.scale;
}
- if (options.ssaoType !== SSAOTYPE_NONE) {
- ssaoPass.intensity = ssao.intensity;
- ssaoPass.power = ssao.power;
- ssaoPass.radius = ssao.radius;
- ssaoPass.sampleCount = ssao.samples;
- ssaoPass.minAngle = ssao.minAngle;
- ssaoPass.scale = ssao.scale;
+ // bloom
+ const dstBloom = cf.bloom;
+ dstBloom.intensity = bloom.enabled ? bloom.intensity : 0;
+ if (bloom.enabled) {
+ dstBloom.blurLevel = bloom.blurLevel;
}
- composePass.gradingEnabled = grading.enabled;
+ // grading
+ const dstGrading = cf.grading;
+ dstGrading.enabled = grading.enabled;
if (grading.enabled) {
- composePass.gradingSaturation = grading.saturation;
- composePass.gradingBrightness = grading.brightness;
- composePass.gradingContrast = grading.contrast;
- composePass.gradingTint = grading.tint;
+ dstGrading.brightness = grading.brightness;
+ dstGrading.contrast = grading.contrast;
+ dstGrading.saturation = grading.saturation;
+ dstGrading.tint.copy(grading.tint);
}
- composePass.vignetteEnabled = vignette.enabled;
+ // vignette
+ const dstVignette = cf.vignette;
+ dstVignette.intensity = vignette.enabled ? vignette.intensity : 0;
if (vignette.enabled) {
- composePass.vignetteInner = vignette.inner;
- composePass.vignetteOuter = vignette.outer;
- composePass.vignetteCurvature = vignette.curvature;
- composePass.vignetteIntensity = vignette.intensity;
+ dstVignette.inner = vignette.inner;
+ dstVignette.outer = vignette.outer;
+ dstVignette.curvature = vignette.curvature;
}
- composePass.fringingEnabled = fringing.enabled;
- if (fringing.enabled) {
- composePass.fringingIntensity = fringing.intensity;
+ // taa
+ const dstTaa = cf.taa;
+ dstTaa.enabled = taa.enabled;
+ if (taa.enabled) {
+ dstTaa.jitter = taa.jitter;
}
- // enable camera jitter if taa is enabled
- cameraComponent.jitter = taa.enabled ? taa.jitter : 0;
+ // fringing
+ const dstFringing = cf.fringing;
+ dstFringing.intensity = fringing.enabled ? fringing.intensity : 0;
+
+ cf.update();
}
}
diff --git a/src/extras/index.js b/src/extras/index.js
index c6df3f3165f..09b9c17594d 100644
--- a/src/extras/index.js
+++ b/src/extras/index.js
@@ -14,7 +14,7 @@ export { UsdzExporter } from './exporters/usdz-exporter.js';
export { GltfExporter } from './exporters/gltf-exporter.js';
// RENDER PASSES
-export { SSAOTYPE_NONE, SSAOTYPE_LIGHTING, SSAOTYPE_COMBINE } from './render-passes/render-pass-camera-frame.js';
+export { SSAOTYPE_NONE, SSAOTYPE_LIGHTING, SSAOTYPE_COMBINE } from './render-passes/constants.js';
export { RenderPassCameraFrame, CameraFrameOptions } from './render-passes/render-pass-camera-frame.js';
export { RenderPassCompose } from './render-passes/render-pass-compose.js';
export { RenderPassDepthAwareBlur } from './render-passes/render-pass-depth-aware-blur.js';
@@ -23,6 +23,7 @@ export { RenderPassUpsample } from './render-passes/render-pass-upsample.js';
export { RenderPassBloom } from './render-passes/render-pass-bloom.js';
export { RenderPassSsao } from './render-passes/render-pass-ssao.js';
export { RenderPassTAA } from './render-passes/render-pass-taa.js';
+export { CameraFrame } from './render-passes/camera-frame.js';
// GIZMOS
export {
diff --git a/src/extras/render-passes/camera-frame.js b/src/extras/render-passes/camera-frame.js
new file mode 100644
index 00000000000..eca69368f9f
--- /dev/null
+++ b/src/extras/render-passes/camera-frame.js
@@ -0,0 +1,388 @@
+import { Debug } from '../../core/debug.js';
+import { Color } from '../../core/math/color.js';
+import { math } from '../../core/math/math.js';
+import { PIXELFORMAT_111110F, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F } from '../../platform/graphics/constants.js';
+import { SSAOTYPE_NONE } from './constants.js';
+import { CameraFrameOptions, RenderPassCameraFrame } from './render-pass-camera-frame.js';
+
+/**
+ * @import { AppBase } from '../../framework/app-base.js'
+ * @import { CameraComponent } from '../../framework/components/camera/component.js'
+ */
+
+/**
+ * Properties related to scene rendering, encompassing settings that control the rendering resolution,
+ * pixel format, multi-sampling for anti-aliasing, tone-mapping and similar.
+ *
+ * @typedef {Object} Rendering
+ * @property {number[]} renderFormats - The preferred render formats of the frame buffer, in order of
+ * preference. First format from this list that is supported by the hardware is used. When none of
+ * the formats are supported, {@link PIXELFORMAT_RGBA8} is used, but this automatically disables
+ * bloom effect, which requires HDR format. The list can contain the following formats:
+ * {@link PIXELFORMAT_111110F}, {@link PIXELFORMAT_RGBA16F}, {@link PIXELFORMAT_RGBA32F} and {@link
+ * PIXELFORMAT_RGBA8}. Typically the default option should be used, which prefers the faster formats,
+ * but if higher dynamic range is needed, the list can be adjusted to prefer higher precision formats.
+ * Defaults to [{@link PIXELFORMAT_111110F}, {@link PIXELFORMAT_RGBA16F}, {@link PIXELFORMAT_RGBA32F}].
+ * @property {boolean} stencil - Whether the render buffer has a stencil buffer. Defaults to false.
+ * @property {number} renderTargetScale - The scale of the render target, 0.1-1 range. This allows the
+ * scene to be rendered to a lower resolution render target as an optimization. The post-processing
+ * is also applied at this lower resolution. The image is then up-scaled to the full resolution and
+ * any UI rendering that follows is applied at the full resolution. Defaults to 1 which represents
+ * full resolution rendering.
+ * @property {number} samples - The number of samples of the {@link RenderTarget} used for the scene
+ * rendering, in 1-4 range. Value of 1 disables multisample anti-aliasing, other values enable
+ * anti-aliasing, Typically set to 1 when TAA is used, even though both anti-aliasing options can be
+ * used together at a higher cost. Defaults to 1.
+ * @property {boolean} sceneColorMap - Whether rendering generates a scene color map. Defaults to false.
+ * @property {boolean} sceneDepthMap - Whether rendering generates a scene depth map. Defaults to false.
+ * @property {number} toneMapping - The tone mapping. Defaults to {@link ToneMapping.LINEAR}. Can be:
+ *
+ * - {@link TONEMAP_LINEAR}
+ * - {@link TONEMAP_FILMIC}
+ * - {@link TONEMAP_HEJL}
+ * - {@link TONEMAP_ACES}
+ * - {@link TONEMAP_ACES2}
+ * - {@link TONEMAP_NEUTRAL}
+ *
+ * @property {number} sharpness - The sharpening intensity, 0-1 range. This can be used to increase
+ * the sharpness of the rendered image. Often used to counteract the blurriness of the TAA effect,
+ * but also blurriness caused by rendering to a lower resolution render target by using
+ * rendering.renderTargetScale property. Defaults to 0.
+ */
+
+/**
+ * Properties related to the Screen Space Ambient Occlusion (SSAO) effect, a postprocessing technique
+ * that approximates ambient occlusion by calculating how exposed each point in the screen space is
+ * to ambient light, enhancing depth perception and adding subtle shadowing in crevices and between
+ * objects.
+ *
+ * @typedef {Object} Ssao
+ * @property {string} type - The type of the SSAO determines how it is applied in the rendering
+ * process. Defaults to {@link SSAOTYPE_NONE}. Can be:
+ *
+ * - {@link SSAOTYPE_NONE}
+ * - {@link SSAOTYPE_LIGHTING}
+ * - {@link SSAOTYPE_COMBINE}
+ *
+ * @property {boolean} blurEnabled - Whether the SSAO effect is blurred. Defaults to true.
+ * @property {number} intensity - The intensity of the SSAO effect, 0-1 range. Defaults to 0.5.
+ * @property {number} radius - The radius of the SSAO effect, 0-100 range. Defaults to 30.
+ * @property {number} samples - The number of samples of the SSAO effect, 1-64 range. Defaults to 12.
+ * @property {number} power - The power of the SSAO effect, 0.1-10 range. Defaults to 6.
+ * @property {number} minAngle - The minimum angle of the SSAO effect, 1-90 range. Defaults to 10.
+ * @property {number} scale - The scale of the SSAO effect, 0.5-1 range. Defaults to 1.
+ */
+
+/**
+ * Properties related to the HDR bloom effect, a postprocessing technique that simulates the natural
+ * glow of bright light sources by spreading their intensity beyond their boundaries, creating a soft
+ * and realistic blooming effect.
+ *
+ * @typedef {Object} Bloom
+ * @property {number} intensity - The intensity of the bloom effect, 0-0.1 range. Defaults to 0,
+ * making it disabled.
+ * @property {number} blurLevel - The number of iterations for blurring the bloom effect, with each
+ * level doubling the blur size. Once the blur size matches the dimensions of the render target,
+ * further blur passes are skipped. The default value is 16.
+ */
+
+/**
+ * Properties related to the color grading effect, a postprocessing technique used to adjust and the
+ * visual tone of an image. This effect modifies brightness, contrast, saturation, and overall color
+ * balance to achieve a specific aesthetic or mood.
+ *
+ * @typedef {Object} Grading
+ * @property {boolean} enabled - Whether grading is enabled. Defaults to false.
+ * @property {number} brightness - The brightness of the grading effect, 0-3 range. Defaults to 1.
+ * @property {number} contrast - The contrast of the grading effect, 0.5-1.5 range. Defaults to 1.
+ * @property {number} saturation - The saturation of the grading effect, 0-2 range. Defaults to 1.
+ * @property {Color} tint - The tint color of the grading effect. Defaults to white.
+ */
+
+/**
+ * Properties related to the vignette effect, a postprocessing technique that darkens the image
+ * edges, creating a gradual falloff in brightness from the center outward. The effect can be also
+ * reversed, making the center of the image darker than the edges, by specifying the outer distance
+ * smaller than the inner distance.
+ *
+ * @typedef {Object} Vignette
+ * @property {number} intensity - The intensity of the vignette effect, 0-1 range. Defaults to 0,
+ * making it disabled.
+ * @property {number} inner - The inner distance of the vignette effect measured from the center of
+ * the screen, 0-3 range. This is where the vignette effect starts. Value larger than 1 represents
+ * the value off screen, which allows more control. Defaults to 0.5, representing half the distance
+ * from center.
+ * @property {number} outer - The outer distance of the vignette effect measured from the center of
+ * the screen, 0-3 range. This is where the vignette reaches full intensity. Value larger than 1
+ * represents the value off screen, which allows more control. Defaults to 1, representing the full
+ * screen.
+ * @property {number} curvature - The curvature of the vignette effect, 0.01-10 range. The vignette
+ * is rendered using a rectangle with rounded corners, and this parameter controls the curvature of
+ * the corners. Value of 1 represents a circle. Smaller values make the corners more square, while
+ * larger values make them more rounded. Defaults to 0.5.
+ */
+
+/**
+ * Properties related to the fringing effect, a chromatic aberration phenomenon where the red, green,
+ * and blue color channels diverge increasingly with greater distance from the center of the screen.
+ *
+ * @typedef {Object} Fringing
+ * @property {number} intensity - The intensity of the fringing effect, 0-100 range. Defaults to 0,
+ * making it disabled.
+ */
+
+/**
+ * Properties related to temporal anti-aliasing (TAA), which is a technique used to reduce aliasing
+ * in the rendered image by blending multiple frames together over time.
+ *
+ * @typedef {Object} Taa
+ * @property {boolean} enabled - Whether Taa is enabled. Defaults to false.
+ * @property {number} jitter - The intensity of the camera jitter, 0-1 range. The larger the value,
+ * the more jitter is applied to the camera, making the anti-aliasing effect more pronounced. This
+ * also makes the image more blurry, and rendering.sharpness parameter can be used to counteract.
+ * Defaults to 1.
+ */
+
+/**
+ * Implementation of a simple to use camera rendering pass, which supports SSAO, Bloom and
+ * other rendering effects.
+ *
+ * @category Render Pass
+ */
+class CameraFrame {
+ /**
+ * Rendering settings.
+ *
+ * @type {Rendering}
+ */
+ rendering = {
+ renderFormats: [PIXELFORMAT_111110F, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F],
+ stencil: false,
+ renderTargetScale: 1.0,
+ samples: 1,
+ sceneColorMap: false,
+ sceneDepthMap: false,
+ toneMapping: 0,
+ sharpness: 0.0
+ };
+
+ /**
+ * SSAO settings.
+ *
+ * @type {Ssao}
+ */
+ ssao = {
+ type: SSAOTYPE_NONE,
+ blurEnabled: true,
+ intensity: 0.5,
+ radius: 30,
+ samples: 12,
+ power: 6,
+ minAngle: 10,
+ scale: 1
+ };
+
+ /**
+ * Bloom settings.
+ *
+ * @type {Bloom}
+ */
+ bloom = {
+ intensity: 0,
+ blurLevel: 16
+ };
+
+ /**
+ * Grading settings.
+ *
+ * @type {Grading}
+ */
+ grading = {
+ enabled: false,
+ brightness: 1,
+ contrast: 1,
+ saturation: 1,
+ tint: new Color(1, 1, 1, 1)
+ };
+
+ /**
+ * Vignette settings.
+ *
+ * @type {Vignette}
+ */
+ vignette = {
+ intensity: 0,
+ inner: 0.5,
+ outer: 1,
+ curvature: 0.5
+ };
+
+ /**
+ * Taa settings.
+ *
+ * @type {Taa}
+ */
+ taa = {
+ enabled: false,
+ jitter: 1
+ };
+
+ /**
+ * Fringing settings.
+ *
+ * @type {Fringing}
+ */
+ fringing = {
+ intensity: 0
+ };
+
+ options = new CameraFrameOptions();
+
+ /**
+ * @type {RenderPassCameraFrame|null}
+ * @private
+ */
+ renderPassCamera = null;
+
+ /**
+ * Creates a new CameraFrame instance.
+ *
+ * @param {AppBase} app - The application.
+ * @param {CameraComponent} cameraComponent - The camera component.
+ */
+ constructor(app, cameraComponent) {
+ this.app = app;
+ this.cameraComponent = cameraComponent;
+ Debug.assert(cameraComponent, 'CameraFrame: cameraComponent must be defined');
+
+ this.updateOptions();
+ this.enabled = true;
+ }
+
+ /**
+ * Destroys the camera frame, removing all render passes.
+ */
+ destroy() {
+ this.disable();
+ }
+
+ enable() {
+ if (!this.renderPassCamera) {
+ const cameraComponent = this.cameraComponent;
+ this.renderPassCamera = new RenderPassCameraFrame(this.app, cameraComponent, this.options);
+ cameraComponent.renderPasses = [this.renderPassCamera];
+ }
+ }
+
+ disable() {
+ if (this.renderPassCamera) {
+ const cameraComponent = this.cameraComponent;
+ cameraComponent.renderPasses?.forEach((renderPass) => {
+ renderPass.destroy();
+ });
+ cameraComponent.renderPasses = [];
+ cameraComponent.rendering = null;
+
+ cameraComponent.jitter = 0;
+ }
+ }
+
+ /**
+ * Sets the enabled state of the camera frame. This disabled the render passes, and releases
+ * any resources.
+ *
+ * @type {boolean}
+ */
+ set enabled(value) {
+ if (value) {
+ this.enable();
+ } else {
+ this.disable();
+ }
+ }
+
+ /**
+ * Gets the enabled state of the camera frame.
+ *
+ * @type {boolean}
+ */
+ get enabled() {
+ return this.renderPassCamera !== null;
+ }
+
+ updateOptions() {
+
+ const { options, rendering, bloom, taa, ssao } = this;
+ options.stencil = rendering.stencil;
+ options.samples = rendering.samples;
+ options.sceneColorMap = rendering.sceneColorMap;
+ options.prepassEnabled = rendering.sceneDepthMap;
+ options.bloomEnabled = bloom.intensity > 0;
+ options.taaEnabled = taa.enabled;
+ options.ssaoType = ssao.type;
+ options.ssaoBlurEnabled = ssao.blurEnabled;
+ options.formats = rendering.renderFormats.slice();
+ }
+
+ /**
+ * Applies any changes made to the properties of this instance.
+ */
+ update() {
+
+ if (!this.enabled) return;
+
+ const cameraComponent = this.cameraComponent;
+ const { options, renderPassCamera, rendering, bloom, grading, vignette, fringing, taa, ssao } = this;
+
+ // options that can cause the passes to be re-created
+ this.updateOptions();
+ renderPassCamera.update(options);
+
+ // update parameters of individual render passes
+ const { composePass, bloomPass, ssaoPass } = renderPassCamera;
+
+ renderPassCamera.renderTargetScale = math.clamp(rendering.renderTargetScale, 0.1, 1);
+ composePass.toneMapping = rendering.toneMapping;
+ composePass.sharpness = rendering.sharpness;
+
+ if (options.bloomEnabled && bloomPass) {
+ composePass.bloomIntensity = bloom.intensity;
+ bloomPass.blurLevel = bloom.blurLevel;
+ }
+
+ if (options.ssaoType !== SSAOTYPE_NONE) {
+ ssaoPass.intensity = ssao.intensity;
+ ssaoPass.power = ssao.power;
+ ssaoPass.radius = ssao.radius;
+ ssaoPass.sampleCount = ssao.samples;
+ ssaoPass.minAngle = ssao.minAngle;
+ ssaoPass.scale = ssao.scale;
+ }
+
+ composePass.gradingEnabled = grading.enabled;
+ if (grading.enabled) {
+ composePass.gradingSaturation = grading.saturation;
+ composePass.gradingBrightness = grading.brightness;
+ composePass.gradingContrast = grading.contrast;
+ composePass.gradingTint = grading.tint;
+ }
+
+ composePass.vignetteEnabled = vignette.intensity > 0;
+ if (composePass.vignetteEnabled) {
+ composePass.vignetteInner = vignette.inner;
+ composePass.vignetteOuter = vignette.outer;
+ composePass.vignetteCurvature = vignette.curvature;
+ composePass.vignetteIntensity = vignette.intensity;
+ }
+
+ composePass.fringingEnabled = fringing.intensity > 0;
+ if (composePass.fringingEnabled) {
+ composePass.fringingIntensity = fringing.intensity;
+ }
+
+ // enable camera jitter if taa is enabled
+ cameraComponent.jitter = taa.enabled ? taa.jitter : 0;
+ }
+}
+
+export { CameraFrame };
diff --git a/src/extras/render-passes/constants.js b/src/extras/render-passes/constants.js
new file mode 100644
index 00000000000..8626ddbe007
--- /dev/null
+++ b/src/extras/render-passes/constants.js
@@ -0,0 +1,24 @@
+/**
+ * SSAO is disabled.
+ *
+ * @type {string}
+ */
+export const SSAOTYPE_NONE = 'none';
+
+/**
+ * SSAO is applied during the lighting calculation stage, allowing it to blend seamlessly with scene
+ * lighting. This results in ambient occlusion being more pronounced in areas where direct light is
+ * obstructed, enhancing realism.
+ *
+ * @type {string}
+ */
+export const SSAOTYPE_LIGHTING = 'lighting';
+
+/**
+ * SSAO is applied as a standalone effect after the scene is rendered. This method uniformly
+ * overlays ambient occlusion across the image, disregarding direct lighting interactions. While
+ * this may sacrifice some realism, it can be advantageous for achieving specific artistic styles.
+ *
+ * @type {string}
+ */
+export const SSAOTYPE_COMBINE = 'combine';
diff --git a/src/extras/render-passes/render-pass-bloom.js b/src/extras/render-passes/render-pass-bloom.js
index 6f85693b7b3..ae91d86682b 100644
--- a/src/extras/render-passes/render-pass-bloom.js
+++ b/src/extras/render-passes/render-pass-bloom.js
@@ -7,6 +7,7 @@ import { FILTER_LINEAR, ADDRESS_CLAMP_TO_EDGE } from '../../platform/graphics/co
import { RenderPassDownsample } from './render-pass-downsample.js';
import { RenderPassUpsample } from './render-pass-upsample.js';
+import { math } from '../../core/math/math.js';
// based on https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom
/**
@@ -18,7 +19,7 @@ import { RenderPassUpsample } from './render-pass-upsample.js';
class RenderPassBloom extends RenderPass {
bloomTexture;
- lastMipLevel = 1;
+ blurLevel = 16;
bloomRenderTarget;
@@ -151,8 +152,8 @@ class RenderPassBloom extends RenderPass {
super.frameUpdate();
// create an appropriate amount of render passes
- let numPasses = this.calcMipLevels(this._sourceTexture.width, this._sourceTexture.height, 2 ** this.lastMipLevel);
- numPasses = Math.max(1, numPasses);
+ const maxNumPasses = this.calcMipLevels(this._sourceTexture.width, this._sourceTexture.height, 1);
+ const numPasses = math.clamp(maxNumPasses, 1, this.blurLevel);
if (this.renderTargets.length !== numPasses) {
diff --git a/src/extras/render-passes/render-pass-camera-frame.js b/src/extras/render-passes/render-pass-camera-frame.js
index 34ea9a27eac..7e539c4c860 100644
--- a/src/extras/render-passes/render-pass-camera-frame.js
+++ b/src/extras/render-passes/render-pass-camera-frame.js
@@ -11,10 +11,8 @@ import { RenderPassCompose } from './render-pass-compose.js';
import { RenderPassTAA } from './render-pass-taa.js';
import { RenderPassPrepass } from './render-pass-prepass.js';
import { RenderPassSsao } from './render-pass-ssao.js';
-
-export const SSAOTYPE_NONE = 'none';
-export const SSAOTYPE_LIGHTING = 'lighting';
-export const SSAOTYPE_COMBINE = 'combine';
+import { SSAOTYPE_COMBINE, SSAOTYPE_LIGHTING, SSAOTYPE_NONE } from './constants.js';
+import { Debug } from '../../core/debug.js';
class CameraFrameOptions {
formats;
@@ -82,6 +80,7 @@ class RenderPassCameraFrame extends RenderPass {
rt = null;
constructor(app, cameraComponent, options = {}) {
+ Debug.assert(app);
super(app.graphicsDevice);
this.app = app;
this.cameraComponent = cameraComponent;
@@ -211,8 +210,8 @@ class RenderPassCameraFrame extends RenderPass {
this.rt = new RenderTarget({
colorBuffer: this.sceneTexture,
- // depthBuffer: this.sceneDepth,
depth: true,
+ stencil: options.stencil,
samples: options.samples,
flipY: !!targetRenderTarget?.flipY // flipY is inherited from the target renderTarget
});