Repository: playcanvas/engine
Pull request #6702 (2024-06-14 13:28:11)

[BREAKING] Changes to the HDR rendering
This PR is marked as breaking, as it will affect projects that use the removed (and replaced) functionality. Note that the current implementation (removed functionality) of HDR was limited / not much documented and the it's not very likely it was being used in many cases.

## New API
```
// class storing few rendering parameters, allowing us to set those on the `Scene`,
// but also override them on a `CameraComponent`
class RenderingParams
    .gammaCorrection
    .toneMapping

Scene.rendering // exposes an instance of RenderingParams
CameraComponent.rendering // exposes an override on the camera

// in the future, additional properties will be added - for example fog settings and similar.
```

The main goals here:
- allow easy way to override camera parameters, that are also propagated to the shader generation, and each camera uses matching shaders using recently improved shader caching on MeshInstances
- this allows us to switch some cameras to HDR (linear, no tone mapping) while leaving others in Gamma space.

In the past this was done using multiple deprecated (see below) constants with not-clear meaning and implementation, which was only partially functional.

## Removed API
```
// constants:
    SHADER_FORWARDHDR
    SHADERPASS_FORWARD_HDR
    GAMMA_SRGBHDR
    GAMMA_SRGBFAST

// functions:
Layer.shaderPass 
```

## Deprecated API
```
Scene.toneMapping // now forwards to Scene.rendering.toneMapping
Scene.gammaCorrection // now forwards to Scene.rendering.gammaCorrection
```


Patch:
diff --git a/examples/src/examples/animation/locomotion.example.mjs b/examples/src/examples/animation/locomotion.example.mjs
index 75528b270..545910d25 100644
--- a/examples/src/examples/animation/locomotion.example.mjs
+++ b/examples/src/examples/animation/locomotion.example.mjs
@@ -85,7 +85,7 @@ assetListLoader.load(() => {
     app.scene.skyboxMip = 2;
     app.scene.skyboxIntensity = 0.7;
     app.scene.envAtlas = assets.helipad.resource;
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     // Create an Entity with a camera component
     const cameraEntity = new pc.Entity();
diff --git a/examples/src/examples/compute/histogram.example.mjs b/examples/src/examples/compute/histogram.example.mjs
index b8ff09c4d..0f4e55c7d 100644
--- a/examples/src/examples/compute/histogram.example.mjs
+++ b/examples/src/examples/compute/histogram.example.mjs
@@ -58,7 +58,7 @@ app.on('destroy', () => {
 const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
 assetListLoader.load(() => {
     // set up some general scene rendering properties
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     // setup skydome
     app.scene.skyboxMip = 2;
diff --git a/examples/src/examples/compute/particles.example.mjs b/examples/src/examples/compute/particles.example.mjs
index 74e606899..62e2575dc 100644
--- a/examples/src/examples/compute/particles.example.mjs
+++ b/examples/src/examples/compute/particles.example.mjs
@@ -51,7 +51,7 @@ app.on('destroy', () => {
 const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
 assetListLoader.load(() => {
     // set up some general scene rendering properties
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.skyboxMip = 2;
     app.scene.skyboxIntensity = 0.2;
     app.scene.envAtlas = assets.helipad.resource;
diff --git a/examples/src/examples/compute/texture-gen.example.mjs b/examples/src/examples/compute/texture-gen.example.mjs
index 2ae3864fc..8d21085e2 100644
--- a/examples/src/examples/compute/texture-gen.example.mjs
+++ b/examples/src/examples/compute/texture-gen.example.mjs
@@ -55,7 +55,7 @@ app.on('destroy', () => {
 const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
 assetListLoader.load(() => {
     // set up some general scene rendering properties
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     // setup skydome
     app.scene.skyboxMip = 1;
diff --git a/examples/src/examples/graphics/ambient-occlusion.example.mjs b/examples/src/examples/graphics/ambient-occlusion.example.mjs
index a7ea5a2ef..191ff0242 100644
--- a/examples/src/examples/graphics/ambient-occlusion.example.mjs
+++ b/examples/src/examples/graphics/ambient-occlusion.example.mjs
@@ -70,7 +70,7 @@ assetListLoader.load(() => {
     // setup skydome
     app.scene.envAtlas = assets.helipad.resource;
     app.scene.skyboxMip = 2;
-    app.scene.exposure = 5;
+    app.scene.exposure = 1.5;
 
     // get the instance of the laboratory
     const laboratoryEntity = assets.laboratory.resource.instantiateRenderEntity({
diff --git a/examples/src/examples/graphics/area-lights.example.mjs b/examples/src/examples/graphics/area-lights.example.mjs
index 113b5ab95..a83e6b27c 100644
--- a/examples/src/examples/graphics/area-lights.example.mjs
+++ b/examples/src/examples/graphics/area-lights.example.mjs
@@ -186,7 +186,7 @@ assetListLoader.load(() => {
     app.setAreaLightLuts(luts.LTC_MAT_1, luts.LTC_MAT_2);
 
     // set up some general scene rendering properties
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     // setup skydome
     app.scene.skyboxMip = 1; // use top mipmap level of cubemap (full resolution)
diff --git a/examples/src/examples/graphics/asset-viewer.example.mjs b/examples/src/examples/graphics/asset-viewer.example.mjs
index 0c4d3c8ee..5452ef37f 100644
--- a/examples/src/examples/graphics/asset-viewer.example.mjs
+++ b/examples/src/examples/graphics/asset-viewer.example.mjs
@@ -195,7 +195,7 @@ assetListLoader.load(() => {
     app.root.addChild(directionalLight);
 
     app.scene.envAtlas = assets.helipad.resource;
-    app.scene.toneMapping = pc.TONEMAP_NEUTRAL;
+    app.scene.rendering.toneMapping = pc.TONEMAP_NEUTRAL;
     app.scene.skyboxMip = 1;
     app.scene.skyboxRotation = new pc.Quat().setFromEulerAngles(0, 70, 0);
     app.scene.skyboxIntensity = 1.5;
diff --git a/examples/src/examples/graphics/clustered-area-lights.example.mjs b/examples/src/examples/graphics/clustered-area-lights.example.mjs
index 3fe6364bb..76a424658 100644
--- a/examples/src/examples/graphics/clustered-area-lights.example.mjs
+++ b/examples/src/examples/graphics/clustered-area-lights.example.mjs
@@ -62,7 +62,7 @@ assetListLoader.load(() => {
     app.start();
 
     // set up some general scene rendering properties
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     // enabled clustered lighting. This is a temporary API and will change in the future
     app.scene.clusteredLightingEnabled = true;
@@ -204,7 +204,7 @@ assetListLoader.load(() => {
     app.setAreaLightLuts(luts.LTC_MAT_1, luts.LTC_MAT_2);
 
     // set up some general scene rendering properties
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     // create ground plane
     const ground = createPrimitive('plane', new pc.Vec3(0, 0, 0), new pc.Vec3(45, 1, 45), assets);
diff --git a/examples/src/examples/graphics/clustered-omni-shadows.example.mjs b/examples/src/examples/graphics/clustered-omni-shadows.example.mjs
index a7dc539f4..a829aa973 100644
--- a/examples/src/examples/graphics/clustered-omni-shadows.example.mjs
+++ b/examples/src/examples/graphics/clustered-omni-shadows.example.mjs
@@ -69,7 +69,7 @@ assetListLoader.load(() => {
     app.start();
 
     // set up some general scene rendering properties
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     data.set('settings', {
         shadowAtlasResolution: 1300, // shadow map resolution storing all shadows
diff --git a/examples/src/examples/graphics/contact-hardening-shadows.example.mjs b/examples/src/examples/graphics/contact-hardening-shadows.example.mjs
index a9d1cf819..4ff969aa6 100644
--- a/examples/src/examples/graphics/contact-hardening-shadows.example.mjs
+++ b/examples/src/examples/graphics/contact-hardening-shadows.example.mjs
@@ -78,7 +78,7 @@ const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets
 assetListLoader.load(() => {
     app.start();
 
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.skyboxMip = 1;
     app.scene.ambientLight.set(0, 0, 0);
     app.scene.ambientLuminance = 0;
diff --git a/examples/src/examples/graphics/dispersion.example.mjs b/examples/src/examples/graphics/dispersion.example.mjs
index 1551d48ac..9644a39c3 100644
--- a/examples/src/examples/graphics/dispersion.example.mjs
+++ b/examples/src/examples/graphics/dispersion.example.mjs
@@ -59,7 +59,7 @@ assetListLoader.load(() => {
 
     // set skybox
     app.scene.envAtlas = assets.helipad.resource;
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.skyboxMip = 1;
 
     // get the instance of the cube it set up with render component and add it to scene
diff --git a/examples/src/examples/graphics/grab-pass.example.mjs b/examples/src/examples/graphics/grab-pass.example.mjs
index 017e4558a..583bdd481 100644
--- a/examples/src/examples/graphics/grab-pass.example.mjs
+++ b/examples/src/examples/graphics/grab-pass.example.mjs
@@ -56,7 +56,7 @@ assetListLoader.load(() => {
     app.scene.exposure = 2;
     app.scene.envAtlas = assets.helipad.resource;
 
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     // Depth layer is where the framebuffer is copied to a texture to be used in the following layers.
     // Move the depth layer to take place after World and Skydome layers, to capture both of them.
diff --git a/examples/src/examples/graphics/ground-fog.example.mjs b/examples/src/examples/graphics/ground-fog.example.mjs
index 5bf50523f..a1122965a 100644
--- a/examples/src/examples/graphics/ground-fog.example.mjs
+++ b/examples/src/examples/graphics/ground-fog.example.mjs
@@ -66,7 +66,7 @@ assetListLoader.load(() => {
     app.scene.skyboxMip = 3;
     app.scene.envAtlas = assets.helipad.resource;
     app.scene.skyboxRotation = new pc.Quat().setFromEulerAngles(0, -70, 0);
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     // disable skydome rendering
     const skyLayer = app.scene.layers.getLayerById(pc.LAYERID_SKYBOX);
diff --git a/examples/src/examples/graphics/hardware-instancing.example.mjs b/examples/src/examples/graphics/hardware-instancing.example.mjs
index 59bd96399..67e1e5028 100644
--- a/examples/src/examples/graphics/hardware-instancing.example.mjs
+++ b/examples/src/examples/graphics/hardware-instancing.example.mjs
@@ -52,7 +52,7 @@ assetListLoader.load(() => {
     app.scene.envAtlas = assets.helipad.resource;
 
     // set up some general scene rendering properties
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     app.scene.ambientLight = new pc.Color(0.1, 0.1, 0.1);
 
diff --git a/examples/src/examples/graphics/light-physical-units.example.mjs b/examples/src/examples/graphics/light-physical-units.example.mjs
index ee8990685..d0a58a777 100644
--- a/examples/src/examples/graphics/light-physical-units.example.mjs
+++ b/examples/src/examples/graphics/light-physical-units.example.mjs
@@ -62,7 +62,7 @@ const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets
 assetListLoader.load(() => {
     app.start();
 
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.skyboxMip = 1;
     app.scene.ambientLight.set(1, 0, 0);
     app.scene.ambientLuminance = 20000;
diff --git a/examples/src/examples/graphics/material-anisotropic.example.mjs b/examples/src/examples/graphics/material-anisotropic.example.mjs
index 5d4b790f1..d5ef9bc13 100644
--- a/examples/src/examples/graphics/material-anisotropic.example.mjs
+++ b/examples/src/examples/graphics/material-anisotropic.example.mjs
@@ -54,7 +54,7 @@ const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets
 assetListLoader.load(() => {
     app.start();
 
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.skyboxMip = 1;
     app.scene.envAtlas = assets.helipad.resource;
 
diff --git a/examples/src/examples/graphics/material-clear-coat.example.mjs b/examples/src/examples/graphics/material-clear-coat.example.mjs
index ecb647a11..dc0848c8e 100644
--- a/examples/src/examples/graphics/material-clear-coat.example.mjs
+++ b/examples/src/examples/graphics/material-clear-coat.example.mjs
@@ -51,7 +51,7 @@ const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets
 assetListLoader.load(() => {
     app.start();
 
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.envAtlas = assets.helipad.resource;
     app.scene.skyboxMip = 1;
 
diff --git a/examples/src/examples/graphics/material-physical.example.mjs b/examples/src/examples/graphics/material-physical.example.mjs
index 332832067..cb9e6ba05 100644
--- a/examples/src/examples/graphics/material-physical.example.mjs
+++ b/examples/src/examples/graphics/material-physical.example.mjs
@@ -48,7 +48,7 @@ assetListLoader.load(() => {
     app.start();
 
     app.scene.envAtlas = assets.helipad.resource;
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.skyboxMip = 1;
 
     // Create an entity with a camera component
diff --git a/examples/src/examples/graphics/material-translucent-specular.example.mjs b/examples/src/examples/graphics/material-translucent-specular.example.mjs
index 686be0f19..258cf06b2 100644
--- a/examples/src/examples/graphics/material-translucent-specular.example.mjs
+++ b/examples/src/examples/graphics/material-translucent-specular.example.mjs
@@ -52,7 +52,7 @@ const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets
 assetListLoader.load(() => {
     app.start();
 
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.envAtlas = assets.helipad.resource;
     app.scene.skyboxMip = 1;
     app.scene.skyboxIntensity = 1;
diff --git a/examples/src/examples/graphics/multi-render-targets.example.mjs b/examples/src/examples/graphics/multi-render-targets.example.mjs
index 98e2ba0e3..9b09e47a2 100644
--- a/examples/src/examples/graphics/multi-render-targets.example.mjs
+++ b/examples/src/examples/graphics/multi-render-targets.example.mjs
@@ -66,7 +66,7 @@ assetListLoader.load(() => {
     // setup skydome
     app.scene.envAtlas = assets.helipad.resource;
     app.scene.skyboxMip = 1;
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     // get existing layers
     const worldLayer = app.scene.layers.getLayerByName('World');
diff --git a/examples/src/examples/graphics/multi-view.example.mjs b/examples/src/examples/graphics/multi-view.example.mjs
index 5a0790216..efdd77cca 100644
--- a/examples/src/examples/graphics/multi-view.example.mjs
+++ b/examples/src/examples/graphics/multi-view.example.mjs
@@ -168,7 +168,7 @@ assetListLoader.load(() => {
 
     // set skybox - this DDS file was 'prefiltered' in the PlayCanvas Editor and then downloaded.
     app.scene.envAtlas = assets.helipad.resource;
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.skyboxMip = 1;
 
     // handle HUD changes - update the debug mode for the top and right cameras
diff --git a/examples/src/examples/graphics/normals-and-tangents.example.mjs b/examples/src/examples/graphics/normals-and-tangents.example.mjs
index da769002d..419749467 100644
--- a/examples/src/examples/graphics/normals-and-tangents.example.mjs
+++ b/examples/src/examples/graphics/normals-and-tangents.example.mjs
@@ -59,7 +59,7 @@ assetListLoader.load(() => {
 
     // Setup skydome
     app.scene.envAtlas = assets.helipad.resource;
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.skyboxRotation = new pc.Quat().setFromEulerAngles(0, 70, 0);
     app.scene.skyboxIntensity = 1.5;
 
diff --git a/examples/src/examples/graphics/paint-mesh.example.mjs b/examples/src/examples/graphics/paint-mesh.example.mjs
index 18974a060..0b9c4e72f 100644
--- a/examples/src/examples/graphics/paint-mesh.example.mjs
+++ b/examples/src/examples/graphics/paint-mesh.example.mjs
@@ -51,7 +51,7 @@ assetListLoader.load(() => {
     app.start();
 
     app.scene.setSkybox(assets.helipad.resources);
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.skyboxIntensity = 1;
     app.scene.skyboxMip = 2;
 
diff --git a/examples/src/examples/graphics/portal.example.mjs b/examples/src/examples/graphics/portal.example.mjs
index 08d037114..563b6e43e 100644
--- a/examples/src/examples/graphics/portal.example.mjs
+++ b/examples/src/examples/graphics/portal.example.mjs
@@ -57,7 +57,7 @@ assetListLoader.load(() => {
 
     // set skybox - this DDS file was 'prefiltered' in the PlayCanvas Editor and then downloaded.
     app.scene.envAtlas = assets.helipad.resource;
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.skyboxMip = 1;
     app.scene.skyboxIntensity = 0.7;
 
diff --git a/examples/src/examples/graphics/post-processing.example.mjs b/examples/src/examples/graphics/post-processing.example.mjs
index 7dba7145a..014572e00 100644
--- a/examples/src/examples/graphics/post-processing.example.mjs
+++ b/examples/src/examples/graphics/post-processing.example.mjs
@@ -319,7 +319,7 @@ assetListLoader.load(() => {
         },
         bloom: {
             enabled: currentOptions.bloomEnabled,
-            intensity: 10,
+            intensity: 20,
             lastMipLevel: 1
         },
         grading: {
diff --git a/examples/src/examples/graphics/reflection-cubemap.example.mjs b/examples/src/examples/graphics/reflection-cubemap.example.mjs
index 0c15dc3e4..4f6fad84b 100644
--- a/examples/src/examples/graphics/reflection-cubemap.example.mjs
+++ b/examples/src/examples/graphics/reflection-cubemap.example.mjs
@@ -53,7 +53,7 @@ assetListLoader.load(() => {
     app.start();
 
     // set up some general scene rendering properties
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     // setup skydome
     app.scene.envAtlas = assets.helipad.resource;
diff --git a/examples/src/examples/graphics/reflection-planar.example.mjs b/examples/src/examples/graphics/reflection-planar.example.mjs
index f86e97b77..51c8be093 100644
--- a/examples/src/examples/graphics/reflection-planar.example.mjs
+++ b/examples/src/examples/graphics/reflection-planar.example.mjs
@@ -50,7 +50,7 @@ assetListLoader.load(() => {
     app.start();
 
     // set up some general scene rendering properties
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     // setup skydome
     app.scene.envAtlas = assets.envatlas.resource;
diff --git a/examples/src/examples/graphics/render-asset.example.mjs b/examples/src/examples/graphics/render-asset.example.mjs
index f96ea1b8c..4b4f30e36 100644
--- a/examples/src/examples/graphics/render-asset.example.mjs
+++ b/examples/src/examples/graphics/render-asset.example.mjs
@@ -79,7 +79,7 @@ assetListLoader.load(() => {
 
     // set skybox
     app.scene.envAtlas = assets.helipad.resource;
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.skyboxMip = 1;
 
     // spin the meshes
diff --git a/examples/src/examples/graphics/render-to-texture.example.mjs b/examples/src/examples/graphics/render-to-texture.example.mjs
index 0de0a65ea..7d54fc9a0 100644
--- a/examples/src/examples/graphics/render-to-texture.example.mjs
+++ b/examples/src/examples/graphics/render-to-texture.example.mjs
@@ -257,7 +257,7 @@ assetListLoader.load(() => {
     app.scene.skyboxMip = 0;
     app.scene.envAtlas = assets.helipad.resource;
 
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     // update things each frame
     let time = 0;
diff --git a/examples/src/examples/graphics/shader-compile.example.mjs b/examples/src/examples/graphics/shader-compile.example.mjs
index 482e536bb..79264d3ee 100644
--- a/examples/src/examples/graphics/shader-compile.example.mjs
+++ b/examples/src/examples/graphics/shader-compile.example.mjs
@@ -117,7 +117,7 @@ assetListLoader.load(() => {
     app.setAreaLightLuts(luts.LTC_MAT_1, luts.LTC_MAT_2);
 
     // set up some general scene rendering properties
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     // setup skydome
     app.scene.skyboxMip = 1;
diff --git a/examples/src/examples/graphics/shadow-cascades.example.mjs b/examples/src/examples/graphics/shadow-cascades.example.mjs
index aca3d921a..88050ef8e 100644
--- a/examples/src/examples/graphics/shadow-cascades.example.mjs
+++ b/examples/src/examples/graphics/shadow-cascades.example.mjs
@@ -71,7 +71,7 @@ assetListLoader.load(() => {
     app.scene.skyboxMip = 3;
     app.scene.envAtlas = assets.helipad.resource;
     app.scene.skyboxRotation = new pc.Quat().setFromEulerAngles(0, -70, 0);
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     // instantiate the terrain
     /** @type {pc.Entity} */
diff --git a/examples/src/examples/graphics/sky.example.mjs b/examples/src/examples/graphics/sky.example.mjs
index a2115132d..e3215e37c 100644
--- a/examples/src/examples/graphics/sky.example.mjs
+++ b/examples/src/examples/graphics/sky.example.mjs
@@ -57,7 +57,7 @@ const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets
 assetListLoader.load(() => {
     app.start();
 
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     // add an instance of the statue
     const statueEntity = assets.statue.resource.instantiateRenderEntity();
diff --git a/examples/src/examples/graphics/texture-basis.example.mjs b/examples/src/examples/graphics/texture-basis.example.mjs
index 8a5ce9cea..7f7e0ba62 100644
--- a/examples/src/examples/graphics/texture-basis.example.mjs
+++ b/examples/src/examples/graphics/texture-basis.example.mjs
@@ -63,7 +63,7 @@ assetListLoader.load(() => {
     app.start();
 
     // Set skybox
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.skyboxMip = 1;
     app.scene.skyboxIntensity = 1.4;
     app.scene.envAtlas = assets.helipad.resource;
diff --git a/examples/src/examples/input/gamepad.example.mjs b/examples/src/examples/input/gamepad.example.mjs
index 8e8ca73c3..dfffb81c2 100644
--- a/examples/src/examples/input/gamepad.example.mjs
+++ b/examples/src/examples/input/gamepad.example.mjs
@@ -52,7 +52,7 @@ assetListLoader.load(() => {
 
     // set skybox
     app.scene.envAtlas = assets.helipad.resource;
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.exposure = 1.6;
     app.scene.skyboxMip = 1;
 
diff --git a/examples/src/examples/input/keyboard.example.mjs b/examples/src/examples/input/keyboard.example.mjs
index f33b0ba98..660c3ab03 100644
--- a/examples/src/examples/input/keyboard.example.mjs
+++ b/examples/src/examples/input/keyboard.example.mjs
@@ -49,7 +49,7 @@ assetListLoader.load(() => {
 
     // set skybox
     app.scene.envAtlas = assets.helipad.resource;
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.exposure = 1.6;
     app.scene.skyboxMip = 1;
 
diff --git a/examples/src/examples/input/mouse.example.mjs b/examples/src/examples/input/mouse.example.mjs
index 42d40cb91..7986669a4 100644
--- a/examples/src/examples/input/mouse.example.mjs
+++ b/examples/src/examples/input/mouse.example.mjs
@@ -49,7 +49,7 @@ assetListLoader.load(() => {
 
     // set skybox
     app.scene.envAtlas = assets.helipad.resource;
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.exposure = 1.6;
     app.scene.skyboxMip = 1;
 
diff --git a/examples/src/examples/loaders/gltf-export.example.mjs b/examples/src/examples/loaders/gltf-export.example.mjs
index 6a29c13b9..e039e9a68 100644
--- a/examples/src/examples/loaders/gltf-export.example.mjs
+++ b/examples/src/examples/loaders/gltf-export.example.mjs
@@ -141,7 +141,7 @@ assetListLoader.load(() => {
 
     // set skybox
     app.scene.envAtlas = assets.helipad.resource;
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.skyboxMip = 1;
     app.scene.exposure = 1.5;
 
diff --git a/examples/src/examples/loaders/gsplat-many.example.mjs b/examples/src/examples/loaders/gsplat-many.example.mjs
index 71fd8408e..51f65cbda 100644
--- a/examples/src/examples/loaders/gsplat-many.example.mjs
+++ b/examples/src/examples/loaders/gsplat-many.example.mjs
@@ -57,7 +57,7 @@ const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets
 assetListLoader.load(() => {
     app.start();
 
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     // get the instance of the gallery and set up with render component
     const galleryEntity = assets.gallery.resource.instantiateRenderEntity();
diff --git a/examples/src/examples/loaders/gsplat.example.mjs b/examples/src/examples/loaders/gsplat.example.mjs
index da003071b..b03420a4e 100644
--- a/examples/src/examples/loaders/gsplat.example.mjs
+++ b/examples/src/examples/loaders/gsplat.example.mjs
@@ -53,7 +53,7 @@ const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets
 assetListLoader.load(() => {
     app.start();
 
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
 
     // Create an Entity with a camera component
     const camera = new pc.Entity();
diff --git a/examples/src/examples/loaders/usdz-export.example.mjs b/examples/src/examples/loaders/usdz-export.example.mjs
index 237dcde7a..8a7aebe1e 100644
--- a/examples/src/examples/loaders/usdz-export.example.mjs
+++ b/examples/src/examples/loaders/usdz-export.example.mjs
@@ -75,7 +75,7 @@ assetListLoader.load(() => {
 
     // set skybox
     app.scene.envAtlas = assets.helipad.resource;
-    app.scene.toneMapping = pc.TONEMAP_ACES;
+    app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
     app.scene.skyboxMip = 1;
 
     // a link element, created in the html part of the examples.
diff --git a/package-lock.json b/package-lock.json
index f9eff7988..2817e2269 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
       "version": "1.71.0-dev",
       "license": "MIT",
       "dependencies": {
-        "@types/webxr": "^0.5.15",
+        "@types/webxr": "^0.5.16",
         "@webgpu/types": "^0.1.40"
       },
       "devDependencies": {
@@ -46,6 +46,9 @@
         "typedoc-plugin-mdn-links": "^3.1.22",
         "typescript": "^5.4.5",
         "xhr2": "^0.2.1"
+      },
+      "engines": {
+        "node": ">=18.0.0"
       }
     },
     "node_modules/@aashutoshrathi/word-wrap": {
@@ -2488,9 +2491,9 @@
       "dev": true
     },
     "node_modules/@types/webxr": {
-      "version": "0.5.15",
-      "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.15.tgz",
-      "integrity": "sha512-nC9116Gd4N+CqTxqo6gvCfhAMAzgRcfS8ZsciNodHq8uwW4JCVKwhagw8yN0XmC7mHrLnWqniJpoVEiR+72Drw=="
+      "version": "0.5.16",
+      "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.16.tgz",
+      "integrity": "sha512-0E0Cl84FECtzrB4qG19TNTqpunw0F1YF0QZZnFMF6pDw1kNKJtrlTKlVB34stGIsHbZsYQ7H0tNjPfZftkHHoA=="
     },
     "node_modules/@ungap/structured-clone": {
       "version": "1.2.0",
diff --git a/package.json b/package.json
index b8eb4c3ae..41766274a 100644
--- a/package.json
+++ b/package.json
@@ -98,7 +98,7 @@
     "README*.md"
   ],
   "dependencies": {
-    "@types/webxr": "^0.5.15",
+    "@types/webxr": "^0.5.16",
     "@webgpu/types": "^0.1.40"
   },
   "devDependencies": {
diff --git a/src/core/sort.js b/src/core/sort.js
index 062667ced..627891ff0 100644
--- a/src/core/sort.js
+++ b/src/core/sort.js
@@ -6,6 +6,15 @@
  */
 const cmpPriority = (a, b) => a.priority - b.priority;
 
+
+/**
+ * @param {{order: number}} a - First object with `order` property.
+ * @param {{order: number}} b - Second object with `order` property.
+ * @returns {number} A number indicating the relative position.
+ * @ignore
+ */
+const cmpStaticOrder = (a, b) => a.constructor.order - b.constructor.order;
+
 /**
  * @param {Array<{priority: number}>} arr - Array to be sorted in place where each element contains
  * an object with at least a priority property.
@@ -13,3 +22,11 @@ const cmpPriority = (a, b) => a.priority - b.priority;
  * @ignore
  */
 export const sortPriority = arr => arr.sort(cmpPriority);
+
+/**
+ * @param {Array<{order: number}>} arr - Array to be sorted in place where each element contains
+ * an object with a static `order` property.
+ * @returns {Array<{order: number}>} In place sorted array.
+ * @ignore
+ */
+export const sortStaticOrder = arr => arr.sort(cmpStaticOrder);
diff --git a/src/deprecated/deprecated.js b/src/deprecated/deprecated.js
index 6d16587e2..6c2e2f3d6 100644
--- a/src/deprecated/deprecated.js
+++ b/src/deprecated/deprecated.js
@@ -480,6 +480,28 @@ Object.defineProperty(Scene.prototype, 'defaultMaterial', {
     }
 });
 
+Object.defineProperty(Scene.prototype, 'toneMapping', {
+    set: function (value) {
+        Debug.deprecated('Scene#toneMapping is deprecated. Use Scene#rendering.toneMapping instead.');
+        this.rendering.toneMapping = value;
+    },
+    get: function () {
+        Debug.deprecated('Scene#toneMapping is deprecated. Use Scene#rendering.toneMapping instead.');
+        return this.rendering.toneMapping;
+    }
+});
+
+Object.defineProperty(Scene.prototype, 'gammaCorrection', {
+    set: function (value) {
+        Debug.deprecated('Scene#gammaCorrection is deprecated. Use Scene#rendering.gammaCorrection instead.');
+        this.rendering.gammaCorrection = value;
+    },
+    get: function () {
+        Debug.deprecated('Scene#gammaCorrection is deprecated. Use Scene#rendering.gammaCorrection instead.');
+        return this.rendering.gammaCorrection;
+    }
+});
+
 Object.defineProperty(LayerComposition.prototype, '_meshInstances', {
     get: function () {
         Debug.deprecated('pc.LayerComposition#_meshInstances is deprecated.');
diff --git a/src/extras/render-passes/render-pass-camera-frame.js b/src/extras/render-passes/render-pass-camera-frame.js
index 3385fa993..c42d313d4 100644
--- a/src/extras/render-passes/render-pass-camera-frame.js
+++ b/src/extras/render-passes/render-pass-camera-frame.js
@@ -1,4 +1,4 @@
-import { LAYERID_SKYBOX, LAYERID_IMMEDIATE, SHADERPASS_FORWARD_HDR } from '../../scene/constants.js';
+import { LAYERID_SKYBOX, LAYERID_IMMEDIATE, TONEMAP_LINEAR, GAMMA_NONE } from '../../scene/constants.js';
 import {
     ADDRESS_CLAMP_TO_EDGE,
     FILTER_LINEAR,
@@ -17,6 +17,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';
+import { RenderingParams } from '../../scene/renderer/rendering-params.js';
+import { Debug } from '../../core/debug.js';
 
 /**
  * Render pass implementation of a common camera frame rendering with integrated  post-processing
@@ -150,7 +152,19 @@ class RenderPassCameraFrame extends RenderPass {
         this.hdrFormat = device.getRenderableHdrFormat() || PIXELFORMAT_RGBA8;
 
         // camera renders in HDR mode (linear output, no tonemapping)
-        cameraComponent.setShaderPass(SHADERPASS_FORWARD_HDR);
+        if (!cameraComponent.rendering) {
+            const renderingParams = new RenderingParams();
+            renderingParams.gammaCorrection = GAMMA_NONE;
+            renderingParams.toneMapping = TONEMAP_LINEAR;
+            cameraComponent.rendering = renderingParams;
+        }
+
+        Debug.call(() => {
+            const renderingParams = cameraComponent.rendering;
+            if (renderingParams.gammaCorrection !== GAMMA_NONE || renderingParams.toneMapping !== TONEMAP_LINEAR) {
+                Debug.error('Camera rendering parameters are not set to HDR mode');
+            }
+        });
 
         // create a render target to render the scene into
         this.sceneTexture = new Texture(device, {
diff --git a/src/framework/app-base.js b/src/framework/app-base.js
index 25cc49e89..e51dc5bb6 100644
--- a/src/framework/app-base.js
+++ b/src/framework/app-base.js
@@ -129,7 +129,7 @@ class AppBase extends EventHandler {
      *
      * @callback MakeTickCallback
      * @param {number} [timestamp] - The timestamp supplied by requestAnimationFrame.
-     * @param {*} [frame] - XRFrame from requestAnimationFrame callback.
+     * @param {XRFrame} [frame] - XRFrame from requestAnimationFrame callback.
      * @returns {void}
      */
 
@@ -292,7 +292,7 @@ class AppBase extends EventHandler {
          * @type {Scene}
          * @example
          * // Set the tone mapping property of the application's scene
-         * this.app.scene.toneMapping = pc.TONEMAP_FILMIC;
+         * this.app.scene.rendering.toneMapping = pc.TONEMAP_FILMIC;
          */
         this.scene = new Scene(device);
         this._registerSceneImmediate(this.scene);
@@ -2036,7 +2036,7 @@ const makeTick = function (_app) {
     const application = _app;
     /**
      * @param {number} [timestamp] - The timestamp supplied by requestAnimationFrame.
-     * @param {*} [frame] - XRFrame from requestAnimationFrame callback.
+     * @param {XRFrame} [frame] - XRFrame from requestAnimationFrame callback.
      */
     return function (timestamp, frame) {
         if (!application.graphicsDevice)
diff --git a/src/framework/components/camera/component.js b/src/framework/components/camera/component.js
index 35eb3ee16..d2be8cd1b 100644
--- a/src/framework/components/camera/component.js
+++ b/src/framework/components/camera/component.js
@@ -146,7 +146,6 @@ class CameraComponent extends Component {
      * equivalent to {@link SHADERPASS_FORWARD}. Can be:
      *
      * - {@link SHADERPASS_FORWARD}
-     * - {@link SHADERPASS_FORWARD_HDR}
      * - {@link SHADERPASS_ALBEDO}
      * - {@link SHADERPASS_OPACITY}
      * - {@link SHADERPASS_WORLDNORMAL}
@@ -200,6 +199,25 @@ class CameraComponent extends Component {
         return this._camera.renderPasses;
     }
 
+    /**
+     * Sets the rendering parameters. If this is not null, the camera will use these rendering
+     * parameters instead of those specified in the scene's rendering parameters
+     * {@link Scene#rendering}.
+     */
+    set rendering(value) {
+        this._camera.renderingParams = value;
+    }
+
+    /**
+     * Gets a {@link RenderingParams} that defines rendering parameters, or null if those are not
+     * set.
+     *
+     * @type {import('../../../scene/renderer/rendering-params.js').RenderingParams|null}
+     */
+    get rendering() {
+        return this._camera.renderingParams;
+    }
+
     /**
      * Sets the camera aperture in f-stops. Default is 16. Higher value means less exposure.
      *
diff --git a/src/framework/components/component.js b/src/framework/components/component.js
index a034fa226..062c4e5ba 100644
--- a/src/framework/components/component.js
+++ b/src/framework/components/component.js
@@ -5,6 +5,15 @@ import { EventHandler } from '../../core/event-handler.js';
  * events each frame, and expose properties to the PlayCanvas Editor.
  */
 class Component extends EventHandler {
+    /**
+     * Component order. When an entity with multiple components gets enabled, this order specifies
+     * in which order the components get enabled. The lowest number gets enabled first.
+     *
+     * @type {number} - Component order number.
+     * @private
+     */
+    static order = 0;
+
     /**
      * The ComponentSystem used to create this Component.
      *
diff --git a/src/framework/components/rigid-body/component.js b/src/framework/components/rigid-body/component.js
index 226cc5a5a..2d8d9d579 100644
--- a/src/framework/components/rigid-body/component.js
+++ b/src/framework/components/rigid-body/component.js
@@ -114,6 +114,8 @@ class RigidBodyComponent extends Component {
      */
     static EVENT_TRIGGERLEAVE = 'triggerleave';
 
+    static order = -1;
+
     /** @private */
     _angularDamping = 0;
 
diff --git a/src/framework/components/system.js b/src/framework/components/system.js
index 506d68032..07679e438 100644
--- a/src/framework/components/system.js
+++ b/src/framework/components/system.js
@@ -67,15 +67,16 @@ class ComponentSystem extends EventHandler {
      * @ignore
      */
     removeComponent(entity) {
+        const id = this.id;
         const record = this.store[entity.getGuid()];
-        const component = entity.c[this.id];
+        const component = entity.c[id];
 
         this.fire('beforeremove', entity, component);
 
         delete this.store[entity.getGuid()];
 
-        entity[this.id] = undefined;
-        delete entity.c[this.id];
+        entity[id] = undefined;
+        delete entity.c[id];
 
         this.fire('remove', entity, record.data);
     }
diff --git a/src/framework/entity.js b/src/framework/entity.js
index de2ad5d2e..16a1ad7f4 100644
--- a/src/framework/entity.js
+++ b/src/framework/entity.js
@@ -1,5 +1,6 @@
 import { Debug } from '../core/debug.js';
 import { guid } from '../core/guid.js';
+import { sortStaticOrder } from '../core/sort.js';
 
 import { GraphNode } from '../scene/graph-node.js';
 
@@ -11,6 +12,12 @@ import { getApplication } from './globals.js';
  */
 const _enableList = [];
 
+/**
+ * @type {Array<import('./components/component.js').Component>}
+ * @ignore
+ */
+const _sortedArray = [];
+
 /**
  * The Entity is the core primitive of a PlayCanvas game. Generally speaking an object in your game
  * will consist of an {@link Entity}, and a set of {@link Component}s which are managed by their
@@ -503,30 +510,30 @@ class Entity extends GraphNode {
     _onHierarchyStateChanged(enabled) {
         super._onHierarchyStateChanged(enabled);
 
-        // enable / disable all the components
-        const components = this.c;
-        for (const type in components) {
-            if (components.hasOwnProperty(type)) {
-                const component = components[type];
-                if (component.enabled) {
-                    if (enabled) {
-                        component.onEnable();
-                    } else {
-                        component.onDisable();
-                    }
+        const components = this._getSortedComponents();
+        for (let i = 0; i < components.length; i++) {
+            const component = components[i];
+            if (component.enabled) {
+                if (enabled) {
+                    component.onEnable();
+                } else {
+                    component.onDisable();
                 }
             }
         }
+
+        components.length = 0;
     }
 
     /** @private */
     _onHierarchyStatePostChanged() {
         // post enable all the components
-        const components = this.c;
-        for (const type in components) {
-            if (components.hasOwnProperty(type))
-                components[type].onPostStateChange();
+        const components = this._getSortedComponents();
+        for (let i = 0; i < components.length; i++) {
+            components[i].onPostStateChange();
         }
+
+        components.length = 0;
     }
 
     /**
@@ -601,6 +608,26 @@ class Entity extends GraphNode {
         return clone;
     }
 
+    _getSortedComponents() {
+        Debug.assert(_sortedArray.length === 0);
+
+        const components = this.c;
+        let needSort = 0;
+        for (const type in components) {
+            if (components.hasOwnProperty(type)) {
+                const component = components[type];
+                needSort |= component.constructor.order !== 0;
+                _sortedArray.push(component);
+            }
+        }
+
+        if (needSort && _sortedArray.length > 1) {
+            sortStaticOrder(_sortedArray);
+        }
+
+        return _sortedArray;
+    }
+
     /**
      * @param {Object<string, Entity>} duplicatedIdsMap - A map of original entity GUIDs to cloned
      * entities.
diff --git a/src/framework/input/element-input.js b/src/framework/input/element-input.js
index 351290949..9dcb4ef81 100644
--- a/src/framework/input/element-input.js
+++ b/src/framework/input/element-input.js
@@ -313,7 +313,7 @@ class ElementSelectEvent extends ElementInputEvent {
     /**
      * Create an instance of a ElementSelectEvent.
      *
-     * @param {object} event - The XRInputSourceEvent that was originally raised.
+     * @param {XRInputSourceEvent} event - The XRInputSourceEvent that was originally raised.
      * @param {import('../components/element/component.js').ElementComponent} element - The
      * ElementComponent that this event was originally raised on.
      * @param {import('../components/camera/component.js').CameraComponent} camera - The
diff --git a/src/framework/lightmapper/bake-light-ambient.js b/src/framework/lightmapper/bake-light-ambient.js
index 50c8841a5..c3f627c03 100644
--- a/src/framework/lightmapper/bake-light-ambient.js
+++ b/src/framework/lightmapper/bake-light-ambient.js
@@ -45,7 +45,7 @@ class BakeLightAmbient extends BakeLight {
 
         // intensity of the virtual light depends on the sphere part used, and also needs to take into account
         // the fact N dot L used to bake it lowers total intensity
-        const gamma = this.scene.gammaCorrection ? 2.2 : 1;
+        const gamma = this.scene.rendering.gammaCorrection ? 2.2 : 1;
         const fullIntensity = 2 * Math.PI * this.scene.ambientBakeSpherePart;
         const linearIntensity = Math.pow(fullIntensity, gamma);
         this.light.intensity = Math.pow(linearIntensity / numVirtualLights, 1 / gamma);
diff --git a/src/framework/lightmapper/bake-light-simple.js b/src/framework/lightmapper/bake-light-simple.js
index d9e72ec29..c398bdbe2 100644
--- a/src/framework/lightmapper/bake-light-simple.js
+++ b/src/framework/lightmapper/bake-light-simple.js
@@ -34,7 +34,7 @@ class BakeLightSimple extends BakeLight {
         light._node.getWorldTransform();
 
         // divide intensity by number of virtual lights (in linear space)
-        const gamma = this.scene.gammaCorrection ? 2.2 : 1;
+        const gamma = this.scene.rendering.gammaCorrection ? 2.2 : 1;
         const linearIntensity = Math.pow(this.intensity, gamma);
         light.intensity = Math.pow(linearIntensity / numVirtualLights, 1 / gamma);
     }
diff --git a/src/framework/lightmapper/lightmapper.js b/src/framework/lightmapper/lightmapper.js
index 8f389ace9..135ac3cf2 100644
--- a/src/framework/lightmapper/lightmapper.js
+++ b/src/framework/lightmapper/lightmapper.js
@@ -27,12 +27,13 @@ import { shaderChunksLightmapper } from '../../scene/shader-lib/chunks/chunks-li
 
 import {
     BAKE_COLORDIR,
-    FOG_NONE,
+    FOG_NONE, GAMMA_NONE, TONEMAP_LINEAR,
     LIGHTTYPE_DIRECTIONAL, LIGHTTYPE_OMNI, LIGHTTYPE_SPOT,
     PROJECTION_ORTHOGRAPHIC, PROJECTION_PERSPECTIVE,
     SHADERDEF_DIRLM, SHADERDEF_LM, SHADERDEF_LMAMBIENT,
     MASK_BAKE, MASK_AFFECT_LIGHTMAPPED, MASK_AFFECT_DYNAMIC,
-    SHADOWUPDATE_REALTIME, SHADOWUPDATE_THISFRAME, SHADER_FORWARDHDR
+    SHADOWUPDATE_REALTIME, SHADOWUPDATE_THISFRAME,
+    SHADER_FORWARD
 } from '../../scene/constants.js';
 import { Camera } from '../../scene/camera.js';
 import { GraphNode } from '../../scene/graph-node.js';
@@ -46,6 +47,7 @@ import { LightmapFilters } from './lightmap-filters.js';
 import { BlendState } from '../../platform/graphics/blend-state.js';
 import { DepthState } from '../../platform/graphics/depth-state.js';
 import { RenderPassLightmapper } from './render-pass-lightmapper.js';
+import { RenderingParams } from '../../scene/renderer/rendering-params.js';
 
 const MAX_LIGHTMAP_SIZE = 2048;
 
@@ -161,6 +163,12 @@ class Lightmapper {
             camera.aspectRatio = 1;
             camera.node = new GraphNode();
             this.camera = camera;
+
+            // baking uses HDR (no gamma / tone mapping)
+            const rp = new RenderingParams();
+            rp.gammaCorrection = GAMMA_NONE;
+            rp.toneMapping = TONEMAP_LINEAR;
+            this.camera.renderingParams = rp;
         }
 
         // create light cluster structure
@@ -1035,7 +1043,7 @@ class Lightmapper {
                     shadowMapRendered = this.renderShadowMap(comp, shadowMapRendered, casters, bakeLight);
 
                     if (clusteredLightingEnabled) {
-                        this.worldClusters.update(clusterLights, this.scene.gammaCorrection, this.lightingParams);
+                        this.worldClusters.update(clusterLights, this.scene.rendering.gammaCorrection, this.lightingParams);
                     }
 
                     // Store original materials
@@ -1118,7 +1126,7 @@ class Lightmapper {
                             this.renderer._forwardTime = 0;
                             this.renderer._shadowMapTime = 0;
 
-                            this.renderer.renderForward(this.camera, rcv, lightArray, SHADER_FORWARDHDR);
+                            this.renderer.renderForward(this.camera, rcv, lightArray, SHADER_FORWARD);
 
                             device.updateEnd();
                         }
diff --git a/src/framework/lightmapper/render-pass-lightmapper.js b/src/framework/lightmapper/render-pass-lightmapper.js
index c780959c4..43dfa218b 100644
--- a/src/framework/lightmapper/render-pass-lightmapper.js
+++ b/src/framework/lightmapper/render-pass-lightmapper.js
@@ -1,6 +1,6 @@
 import { DebugGraphics } from '../../platform/graphics/debug-graphics.js';
 import { RenderPass } from '../../platform/graphics/render-pass.js';
-import { SHADER_FORWARDHDR } from '../../scene/constants.js';
+import { SHADER_FORWARD } from '../../scene/constants.js';
 
 /**
  * A render pass implementing rendering of mesh instance receivers for light-mapper.
@@ -34,7 +34,7 @@ class RenderPassLightmapper extends RenderPass {
 
         const { renderer, camera, receivers, renderTarget, worldClusters, lightArray } = this;
 
-        renderer.renderForwardLayer(camera, renderTarget, null, undefined, SHADER_FORWARDHDR, this.viewBindGroups, {
+        renderer.renderForwardLayer(camera, renderTarget, null, undefined, SHADER_FORWARD, this.viewBindGroups, {
             meshInstances: receivers,
             splitLights: lightArray,
             lightClusters: worldClusters
diff --git a/src/framework/xr/xr-anchor.js b/src/framework/xr/xr-anchor.js
index 124409020..2c16f31ff 100644
--- a/src/framework/xr/xr-anchor.js
+++ b/src/framework/xr/xr-anchor.js
@@ -127,7 +127,7 @@ class XrAnchor extends EventHandler {
     }
 
     /**
-     * @param {*} frame - XRFrame from requestAnimationFrame callback.
+     * @param {XRFrame} frame - XRFrame from requestAnimationFrame callback.
      * @ignore
      */
     update(frame) {
diff --git a/src/framework/xr/xr-anchors.js b/src/framework/xr/xr-anchors.js
index 834bac1f7..ba3a03548 100644
--- a/src/framework/xr/xr-anchors.js
+++ b/src/framework/xr/xr-anchors.js
@@ -382,7 +382,7 @@ class XrAnchors extends EventHandler {
     }
 
     /**
-     * @param {*} frame - XRFrame from requestAnimationFrame callback.
+     * @param {XRFrame} frame - XRFrame from requestAnimationFrame callback.
      * @ignore
      */
     update(frame) {
diff --git a/src/framework/xr/xr-hand.js b/src/framework/xr/xr-hand.js
index 98785c352..88a009f30 100644
--- a/src/framework/xr/xr-hand.js
+++ b/src/framework/xr/xr-hand.js
@@ -147,7 +147,7 @@ class XrHand extends EventHandler {
     }
 
     /**
-     * @param {*} frame - XRFrame from requestAnimationFrame callback.
+     * @param {XRFrame} frame - XRFrame from requestAnimationFrame callback.
      * @ignore
      */
     update(frame) {
diff --git a/src/framework/xr/xr-image-tracking.js b/src/framework/xr/xr-image-tracking.js
index f317f2cc2..1915282bf 100644
--- a/src/framework/xr/xr-image-tracking.js
+++ b/src/framework/xr/xr-image-tracking.js
@@ -157,7 +157,7 @@ class XrImageTracking extends EventHandler {
     }
 
     /**
-     * @param {*} frame - XRFrame from requestAnimationFrame callback.
+     * @param {XRFrame} frame - XRFrame from requestAnimationFrame callback.
      * @ignore
      */
     update(frame) {
diff --git a/src/framework/xr/xr-input-source.js b/src/framework/xr/xr-input-source.js
index 0c14e8a24..5d8bf37d5 100644
--- a/src/framework/xr/xr-input-source.js
+++ b/src/framework/xr/xr-input-source.js
@@ -299,8 +299,7 @@ class XrInputSource extends EventHandler {
      * Create a new XrInputSource instance.
      *
      * @param {import('./xr-manager.js').XrManager} manager - WebXR Manager.
-     * @param {*} xrInputSource - [XRInputSource](https://developer.mozilla.org/en-US/docs/Web/API/XRInputSource)
-     * object that is created by WebXR API.
+     * @param {XRInputSource} xrInputSource - A WebXR input source.
      * @ignore
      */
     constructor(manager, xrInputSource) {
@@ -328,7 +327,7 @@ class XrInputSource extends EventHandler {
     /**
      * XRInputSource object that is associated with this input source.
      *
-     * @type {object}
+     * @type {XRInputSource}
      */
     get inputSource() {
         return this._xrInputSource;
@@ -469,7 +468,7 @@ class XrInputSource extends EventHandler {
     }
 
     /**
-     * @param {*} frame - XRFrame from requestAnimationFrame callback.
+     * @param {XRFrame} frame - XRFrame from requestAnimationFrame callback.
      * @ignore
      */
     update(frame) {
diff --git a/src/framework/xr/xr-input.js b/src/framework/xr/xr-input.js
index f0494c3ae..38e24031b 100644
--- a/src/framework/xr/xr-input.js
+++ b/src/framework/xr/xr-input.js
@@ -303,7 +303,7 @@ class XrInput extends EventHandler {
     }
 
     /**
-     * @param {*} frame - XRFrame from requestAnimationFrame callback.
+     * @param {XRFrame} frame - XRFrame from requestAnimationFrame callback.
      * @ignore
      */
     update(frame) {
diff --git a/src/framework/xr/xr-joint.js b/src/framework/xr/xr-joint.js
index d9a223499..4d24f3e23 100644
--- a/src/framework/xr/xr-joint.js
+++ b/src/framework/xr/xr-joint.js
@@ -127,7 +127,7 @@ class XrJoint {
     }
 
     /**
-     * @param {*} pose - XRJointPose of this joint.
+     * @param {XRJointPose} pose - XRJointPose of this joint.
      * @ignore
      */
     update(pose) {
diff --git a/src/framework/xr/xr-light-estimation.js b/src/framework/xr/xr-light-estimation.js
index 1f7bdf672..d4a314235 100644
--- a/src/framework/xr/xr-light-estimation.js
+++ b/src/framework/xr/xr-light-estimation.js
@@ -191,7 +191,7 @@ class XrLightEstimation extends EventHandler {
     }
 
     /**
-     * @param {*} frame - XRFrame from requestAnimationFrame callback.
+     * @param {XRFrame} frame - XRFrame from requestAnimationFrame callback.
      * @ignore
      */
     update(frame) {
diff --git a/src/framework/xr/xr-manager.js b/src/framework/xr/xr-manager.js
index 81612be01..9f0e15015 100644
--- a/src/framework/xr/xr-manager.js
+++ b/src/framework/xr/xr-manager.js
@@ -849,7 +849,7 @@ class XrManager extends EventHandler {
     }
 
     /**
-     * @param {*} frame - XRFrame from requestAnimationFrame callback.
+     * @param {XRFrame} frame - XRFrame from requestAnimationFrame callback.
      *
      * @returns {boolean} True if update was successful, false otherwise.
      * @ignore
diff --git a/src/framework/xr/xr-plane-detection.js b/src/framework/xr/xr-plane-detection.js
index 5aafefa30..41a30f342 100644
--- a/src/framework/xr/xr-plane-detection.js
+++ b/src/framework/xr/xr-plane-detection.js
@@ -143,7 +143,7 @@ class XrPlaneDetection extends EventHandler {
     }
 
     /**
-     * @param {*} frame - XRFrame from requestAnimationFrame callback.
+     * @param {XRFrame} frame - XRFrame from requestAnimationFrame callback.
      * @ignore
      */
     update(frame) {
diff --git a/src/framework/xr/xr-plane.js b/src/framework/xr/xr-plane.js
index 033302420..e176fc6f0 100644
--- a/src/framework/xr/xr-plane.js
+++ b/src/framework/xr/xr-plane.js
@@ -104,7 +104,7 @@ class XrPlane extends EventHandler {
     }
 
     /**
-     * @param {*} frame - XRFrame from requestAnimationFrame callback.
+     * @param {XRFrame} frame - XRFrame from requestAnimationFrame callback.
      * @ignore
      */
     update(frame) {
diff --git a/src/framework/xr/xr-tracked-image.js b/src/framework/xr/xr-tracked-image.js
index e6a12917f..2744d2a5a 100644
--- a/src/framework/xr/xr-tracked-image.js
+++ b/src/framework/xr/xr-tracked-image.js
@@ -75,7 +75,7 @@ class XrTrackedImage extends EventHandler {
     _emulated = false;
 
     /**
-     * @type {*}
+     * @type {XRPose|null}
      * @ignore
      */
     _pose = null;
diff --git a/src/framework/xr/xr-views.js b/src/framework/xr/xr-views.js
index ecc1c93de..d55fd9dab 100644
--- a/src/framework/xr/xr-views.js
+++ b/src/framework/xr/xr-views.js
@@ -204,7 +204,7 @@ class XrViews extends EventHandler {
     }
 
     /**
-     * @param {*} frame - XRFrame from requestAnimationFrame callback.
+     * @param {XRFrame} frame - XRFrame from requestAnimationFrame callback.
      * @param {XRView} xrView - XRView from WebXR API.
      * @ignore
      */
diff --git a/src/scene/camera.js b/src/scene/camera.js
index 2c61b3605..d5100f3b9 100644
--- a/src/scene/camera.js
+++ b/src/scene/camera.js
@@ -41,6 +41,13 @@ class Camera {
      */
     renderPassDepthGrab = null;
 
+    /**
+     * The rendering parameters.
+     *
+     * @type {import('./renderer/rendering-params.js').RenderingParams|null}
+     */
+    renderingParams = null;
+
     /**
      * Render passes used to render this camera. If empty, the camera will render using the default
      * render passes.
diff --git a/src/scene/constants.js b/src/scene/constants.js
index 1d95e6ce8..68f9f8d27 100644
--- a/src/scene/constants.js
+++ b/src/scene/constants.js
@@ -578,24 +578,6 @@ export const GAMMA_NONE = 0;
  */
 export const GAMMA_SRGB = 1;
 
-/**
- * Apply sRGB (fast) gamma correction.
- *
- * @type {number}
- * @deprecated
- * @ignore
- * @category Graphics
- */
-export const GAMMA_SRGBFAST = 2; // deprecated
-
-/**
- * Apply sRGB (HDR) gamma correction.
- *
- * @type {number}
- * @category Graphics
- */
-export const GAMMA_SRGBHDR = 3;
-
 /**
  * Linear tonemapping.
  *
@@ -717,20 +699,14 @@ export const MASK_AFFECT_LIGHTMAPPED = 2;
 export const MASK_BAKE = 4;
 
 /**
- * Render shaded materials with gamma correction and tonemapping.
+ * Render shaded materials using forward rendering.
  *
  * @type {number}
  * @category Graphics
  */
 export const SHADER_FORWARD = 0;
 
-/**
- * Render shaded materials without gamma correction and tonemapping.
- *
- * @type {number}
- * @category Graphics
- */
-export const SHADER_FORWARDHDR = 1;
+export const SHADER_PREPASS_VELOCITY = 1;
 
 /**
  * Render RGBA-encoded depth value.
@@ -746,8 +722,6 @@ export const SHADER_PICK = 3;
 // shadow pass used by the shadow rendering code
 export const SHADER_SHADOW = 4;
 
-export const SHADER_PREPASS_VELOCITY = 5;
-
 /**
  * Shader that performs forward rendering.
  *
@@ -756,15 +730,6 @@ export const SHADER_PREPASS_VELOCITY = 5;
  */
 export const SHADERPASS_FORWARD = 'forward';
 
-/**
- * Shader that performs forward rendering in HDR mode (gamma correction and tonemapping are
- * disabled).
- *
- * @type {string}
- * @category Graphics
- */
-export const SHADERPASS_FORWARD_HDR = 'forward_hdr';
-
 /**
  * Shader used for debug rendering of albedo.
  *
diff --git a/src/scene/gsplat/gsplat-compressed-material.js b/src/scene/gsplat/gsplat-compressed-material.js
index c1b898cae..1dc5e25d8 100644
--- a/src/scene/gsplat/gsplat-compressed-material.js
+++ b/src/scene/gsplat/gsplat-compressed-material.js
@@ -1,6 +1,6 @@
 import { CULLFACE_NONE, SEMANTIC_ATTR13, SEMANTIC_POSITION } from "../../platform/graphics/constants.js";
 import { ShaderProcessorOptions } from "../../platform/graphics/shader-processor-options.js";
-import { BLEND_NONE, BLEND_NORMAL, DITHER_NONE, GAMMA_NONE, GAMMA_SRGBHDR, SHADER_FORWARDHDR, TONEMAP_LINEAR } from "../constants.js";
+import { BLEND_NONE, BLEND_NORMAL, DITHER_NONE, TONEMAP_LINEAR } from "../constants.js";
 import { Material } from "../materials/material.js";
 import { getProgramLibrary } from "../shader-lib/get-program-library.js";
 
@@ -27,11 +27,12 @@ varying vec4 color;
 #endif
 
 uniform vec2 viewport;
-uniform vec4 bufferWidths;
+uniform vec4 bufferWidths;                  // packed width, chunked width, numSplats
 uniform highp usampler2D splatOrder;
 uniform highp usampler2D packedTexture;
 uniform highp sampler2D chunkTexture;
 
+uint orderId;
 uint splatId;
 ivec2 splatUV;
 ivec2 chunkUV;
@@ -46,7 +47,7 @@ void calcUV() {
     int chunkWidth = int(bufferWidths.y);
 
     // sample order texture
-    uint orderId = vertex_id_attrib + uint(vertex_position.z);
+    orderId = vertex_id_attrib + uint(vertex_position.z);
     ivec2 orderUV = ivec2(
         int(orderId) % packedWidth,
         int(orderId) / packedWidth
@@ -197,7 +198,7 @@ vec4 evalSplat() {
     vec4 centerClip = matrix_projection * centerView;
 
     // cull behind camera
-    if (centerClip.z < -centerClip.w) {
+    if (centerClip.z < -centerClip.w || orderId >= uint(bufferWidths.z)) {
         return vec4(0.0, 0.0, 2.0, 1.0);
     }
 
@@ -335,12 +336,12 @@ const createGSplatCompressedMaterial = (options = {}) => {
     material.blendType = dither ? BLEND_NONE : BLEND_NORMAL;
     material.depthWrite = dither;
 
-    material.getShaderVariant = function (device, scene, defs, unused, pass, sortedLights, viewUniformFormat, viewBindGroupFormat) {
+    material.getShaderVariant = function (device, scene, defs, renderParams, pass, sortedLights, viewUniformFormat, viewBindGroupFormat) {
 
         const programOptions = {
             pass: pass,
-            gamma: (pass === SHADER_FORWARDHDR ? (scene.gammaCorrection ? GAMMA_SRGBHDR : GAMMA_NONE) : scene.gammaCorrection),
-            toneMapping: (pass === SHADER_FORWARDHDR ? TONEMAP_LINEAR : scene.toneMapping),
+            gamma: renderParams.gammaCorrection,
+            toneMapping: renderParams.toneMapping,
             vertex: options.vertex ?? splatMainVS,
             fragment: options.fragment ?? splatMainFS,
             dither: ditherEnum
diff --git a/src/scene/gsplat/gsplat-compressed.js b/src/scene/gsplat/gsplat-compressed.js
index 000b41fcf..cdd675936 100644
--- a/src/scene/gsplat/gsplat-compressed.js
+++ b/src/scene/gsplat/gsplat-compressed.js
@@ -110,7 +110,7 @@ class GSplatCompressed {
         const result = createGSplatCompressedMaterial(options);
         result.setParameter('packedTexture', this.packedTexture);
         result.setParameter('chunkTexture', this.chunkTexture);
-        result.setParameter('bufferWidths', new Float32Array([this.packedTexture.width, this.chunkTexture.width / 3, 0, 0]));
+        result.setParameter('bufferWidths', new Float32Array([this.packedTexture.width, this.chunkTexture.width / 3, this.numSplats, 0]));
         return result;
     }
 
diff --git a/src/scene/gsplat/gsplat-instance.js b/src/scene/gsplat/gsplat-instance.js
index f31f9f537..89b80762b 100644
--- a/src/scene/gsplat/gsplat-instance.js
+++ b/src/scene/gsplat/gsplat-instance.js
@@ -118,7 +118,9 @@ class GSplatInstance {
         this.meshInstance = new MeshInstance(this.mesh, this.material);
         this.meshInstance.setInstancing(indicesVB, true);
         this.meshInstance.gsplatInstance = this;
-        this.meshInstance.instancingCount = numSplatInstances;
+
+        // only start rendering the splat after we've received the splat order data
+        this.meshInstance.instancingCount = 0;
 
         // clone centers to allow multiple instances of sorter
         this.centers = new Float32Array(splat.centers);
diff --git a/src/scene/gsplat/gsplat-material.js b/src/scene/gsplat/gsplat-material.js
index 9a12632cb..31d2f9652 100644
--- a/src/scene/gsplat/gsplat-material.js
+++ b/src/scene/gsplat/gsplat-material.js
@@ -1,6 +1,6 @@
 import { CULLFACE_NONE } from "../../platform/graphics/constants.js";
 import { ShaderProcessorOptions } from "../../platform/graphics/shader-processor-options.js";
-import { BLEND_NONE, BLEND_NORMAL, DITHER_NONE, GAMMA_NONE, GAMMA_SRGBHDR, SHADER_FORWARDHDR, TONEMAP_LINEAR } from "../constants.js";
+import { BLEND_NONE, BLEND_NORMAL, DITHER_NONE } from "../constants.js";
 import { Material } from "../materials/material.js";
 import { getProgramLibrary } from "../shader-lib/get-program-library.js";
 import { gsplat } from "./shader-generator-gsplat.js";
@@ -44,12 +44,12 @@ const createGSplatMaterial = (options = {}) => {
     material.blendType = dither ? BLEND_NONE : BLEND_NORMAL;
     material.depthWrite = dither;
 
-    material.getShaderVariant = function (device, scene, defs, unused, pass, sortedLights, viewUniformFormat, viewBindGroupFormat) {
+    material.getShaderVariant = function (device, scene, defs, renderParams, pass, sortedLights, viewUniformFormat, viewBindGroupFormat) {
 
         const programOptions = {
             pass: pass,
-            gamma: (pass === SHADER_FORWARDHDR ? (scene.gammaCorrection ? GAMMA_SRGBHDR : GAMMA_NONE) : scene.gammaCorrection),
-            toneMapping: (pass === SHADER_FORWARDHDR ? TONEMAP_LINEAR : scene.toneMapping),
+            gamma: renderParams.gammaCorrection,
+            toneMapping: renderParams.toneMapping,
             vertex: options.vertex ?? splatMainVS,
             fragment: options.fragment ?? splatMainFS,
             dither: ditherEnum
diff --git a/src/scene/layer.js b/src/scene/layer.js
index b9931213f..560f210c4 100644
--- a/src/scene/layer.js
+++ b/src/scene/layer.js
@@ -4,7 +4,6 @@ import { hash32Fnv1a } from '../core/hash.js';
 import {
     LIGHTTYPE_DIRECTIONAL,
     LAYER_FX,
-    SHADER_FORWARD,
     SORTKEY_FORWARD,
     SORTMODE_BACK2FRONT, SORTMODE_CUSTOM, SORTMODE_FRONT2BACK, SORTMODE_MATERIALMESH, SORTMODE_NONE
 } from './constants.js';
@@ -250,21 +249,6 @@ class Layer {
             this.renderTarget = options.renderTarget;
         }
 
-        /**
-         * A type of shader to use during rendering. Possible values are:
-         *
-         * - {@link SHADER_FORWARD}
-         * - {@link SHADER_FORWARDHDR}
-         * - {@link SHADER_DEPTH}
-         * - Your own custom value. Should be in 19 - 31 range. Use {@link StandardMaterial#onUpdateShader}
-         * to apply shader modifications based on this value.
-         *
-         * Defaults to {@link SHADER_FORWARD}.
-         *
-         * @type {number}
-         */
-        this.shaderPass = options.shaderPass ?? SHADER_FORWARD;
-
         // clear flags
         /**
          * @type {boolean}
diff --git a/src/scene/materials/basic-material.js b/src/scene/materials/basic-material.js
index 5090328ec..859e237d6 100644
--- a/src/scene/materials/basic-material.js
+++ b/src/scene/materials/basic-material.js
@@ -84,7 +84,7 @@ class BasicMaterial extends Material {
         }
     }
 
-    getShaderVariant(device, scene, objDefs, unused, pass, sortedLights, viewUniformFormat, viewBindGroupFormat, vertexFormat) {
+    getShaderVariant(device, scene, objDefs, renderParams, pass, sortedLights, viewUniformFormat, viewBindGroupFormat, vertexFormat) {
 
         const options = {
             skin: objDefs && (objDefs & SHADERDEF_SKIN) !== 0,
diff --git a/src/scene/materials/lit-material-options-builder.js b/src/scene/materials/lit-material-options-builder.js
index c8ff4ea4f..1f74cc7ef 100644
--- a/src/scene/materials/lit-material-options-builder.js
+++ b/src/scene/materials/lit-material-options-builder.js
@@ -1,21 +1,16 @@
 import {
-    CUBEPROJ_NONE, GAMMA_SRGBHDR, GAMMA_NONE, LIGHTTYPE_DIRECTIONAL, LIGHTTYPE_OMNI, LIGHTTYPE_SPOT,
-    MASK_AFFECT_DYNAMIC, SHADER_FORWARDHDR, TONEMAP_LINEAR, SHADERDEF_INSTANCING, SHADERDEF_MORPH_NORMAL,
+    CUBEPROJ_NONE, GAMMA_NONE, LIGHTTYPE_DIRECTIONAL, LIGHTTYPE_OMNI, LIGHTTYPE_SPOT,
+    MASK_AFFECT_DYNAMIC, TONEMAP_LINEAR, SHADERDEF_INSTANCING, SHADERDEF_MORPH_NORMAL,
     SHADERDEF_MORPH_POSITION, SHADERDEF_MORPH_TEXTURE_BASED, SHADERDEF_SCREENSPACE, SHADERDEF_SKIN,
     SHADERDEF_NOSHADOW, SHADERDEF_TANGENTS, SPECULAR_BLINN, SPRITE_RENDERMODE_SIMPLE
 } from "../constants.js";
 
 class LitMaterialOptionsBuilder {
-    static update(litOptions, material, scene, objDefs, pass, sortedLights) {
+    static update(litOptions, material, scene, renderParams, objDefs, pass, sortedLights) {
         LitMaterialOptionsBuilder.updateSharedOptions(litOptions, material, scene, objDefs, pass);
         LitMaterialOptionsBuilder.updateMaterialOptions(litOptions, material);
-        LitMaterialOptionsBuilder.updateEnvOptions(litOptions, material, scene);
+        LitMaterialOptionsBuilder.updateEnvOptions(litOptions, material, scene, renderParams);
         LitMaterialOptionsBuilder.updateLightingOptions(litOptions, material, objDefs, sortedLights);
-
-        if (pass === SHADER_FORWARDHDR) {
-            litOptions.gamma = GAMMA_SRGBHDR;
-            litOptions.toneMap = TONEMAP_LINEAR;
-        }
     }
 
     static updateSharedOptions(litOptions, material, scene, objDefs, pass) {
@@ -95,10 +90,10 @@ class LitMaterialOptionsBuilder {
         litOptions.diffuseMapEnabled = material.hasDiffuseMap;
     }
 
-    static updateEnvOptions(litOptions, material, scene) {
+    static updateEnvOptions(litOptions, material, scene, renderParams) {
         litOptions.fog = material.useFog ? scene.fog : 'none';
-        litOptions.gamma = material.useGammaTonemap ? scene.gammaCorrection : GAMMA_NONE;
-        litOptions.toneMap = material.useGammaTonemap ? scene.toneMapping : -1;
+        litOptions.gamma = material.useGammaTonemap ? renderParams.gammaCorrection : GAMMA_NONE;
+        litOptions.toneMap = material.useGammaTonemap ? renderParams.toneMapping : TONEMAP_LINEAR;
 
         // source of reflections
         if (material.useSkybox && scene.envAtlas && scene.skybox) {
diff --git a/src/scene/materials/lit-material.js b/src/scene/materials/lit-material.js
index d926550f2..1f3847edd 100644
--- a/src/scene/materials/lit-material.js
+++ b/src/scene/materials/lit-material.js
@@ -90,11 +90,11 @@ class LitMaterial extends Material {
 
     hasClearCoatNormals = false;
 
-    getShaderVariant(device, scene, objDefs, unused, pass, sortedLights, viewUniformFormat, viewBindGroupFormat, vertexFormat) {
+    getShaderVariant(device, scene, objDefs, renderParams, pass, sortedLights, viewUniformFormat, viewBindGroupFormat, vertexFormat) {
         options.usedUvs = this.usedUvs.slice();
         options.shaderChunk = this.shaderChunk;
 
-        LitMaterialOptionsBuilder.update(options.litOptions, this, scene, objDefs, pass, sortedLights);
+        LitMaterialOptionsBuilder.update(options.litOptions, this, scene, renderParams, objDefs, pass, sortedLights);
         const processingOptions = new ShaderProcessorOptions(viewUniformFormat, viewBindGroupFormat, vertexFormat);
         const library = getProgramLibrary(device);
         library.register('lit', lit);
diff --git a/src/scene/materials/material.js b/src/scene/materials/material.js
index 2394fa89c..71e474de2 100644
--- a/src/scene/materials/material.js
+++ b/src/scene/materials/material.js
@@ -532,8 +532,7 @@ class Material {
     updateUniforms(device, scene) {
     }
 
-    // TODO: unused parameter should be removed, but the Editor still uses this function
-    getShaderVariant(device, scene, objDefs, unused, pass, sortedLights, viewUniformFormat, viewBindGroupFormat, vertexFormat) {
+    getShaderVariant(device, scene, objDefs, renderParams, pass, sortedLights, viewUniformFormat, viewBindGroupFormat, vertexFormat) {
 
         // generate shader variant - its the same shader, but with different processing options
         const processingOptions = new ShaderProcessorOptions(viewUniformFormat, viewBindGroupFormat, vertexFormat);
diff --git a/src/scene/materials/standard-material-options-builder.js b/src/scene/materials/standard-material-options-builder.js
index 03ce42a87..cd98e21d8 100644
--- a/src/scene/materials/standard-material-options-builder.js
+++ b/src/scene/materials/standard-material-options-builder.js
@@ -4,10 +4,10 @@ import {
 
 import {
     BLEND_NONE,
-    GAMMA_NONE, GAMMA_SRGBHDR,
+    GAMMA_NONE,
     LIGHTTYPE_DIRECTIONAL, LIGHTTYPE_OMNI, LIGHTTYPE_SPOT,
     MASK_AFFECT_DYNAMIC,
-    SHADER_FORWARDHDR, SHADER_PREPASS_VELOCITY,
+    SHADER_PREPASS_VELOCITY,
     SHADERDEF_DIRLM, SHADERDEF_INSTANCING, SHADERDEF_LM, SHADERDEF_MORPH_POSITION, SHADERDEF_MORPH_NORMAL, SHADERDEF_NOSHADOW, SHADERDEF_MORPH_TEXTURE_BASED,
     SHADERDEF_SCREENSPACE, SHADERDEF_SKIN, SHADERDEF_TANGENTS, SHADERDEF_UV0, SHADERDEF_UV1, SHADERDEF_VCOLOR, SHADERDEF_LMAMBIENT,
     TONEMAP_LINEAR,
@@ -49,14 +49,10 @@ class StandardMaterialOptionsBuilder {
         this._updateUVOptions(options, stdMat, objDefs, true);
     }
 
-    updateRef(options, scene, stdMat, objDefs, pass, sortedLights) {
+    updateRef(options, scene, renderParams, stdMat, objDefs, pass, sortedLights) {
         this._updateSharedOptions(options, scene, stdMat, objDefs, pass);
-        this._updateEnvOptions(options, stdMat, scene);
+        this._updateEnvOptions(options, stdMat, scene, renderParams);
         this._updateMaterialOptions(options, stdMat);
-        if (pass === SHADER_FORWARDHDR) {
-            if (options.litOptions.gamma) options.litOptions.gamma = GAMMA_SRGBHDR;
-            options.litOptions.toneMap = TONEMAP_LINEAR;
-        }
         options.litOptions.hasTangents = objDefs && ((objDefs & SHADERDEF_TANGENTS) !== 0);
         this._updateLightOptions(options, scene, stdMat, objDefs, sortedLights);
         this._updateUVOptions(options, stdMat, objDefs, false);
@@ -297,10 +293,10 @@ class StandardMaterialOptionsBuilder {
         options.litOptions.dispersion = stdMat.dispersion > 0;
     }
 
-    _updateEnvOptions(options, stdMat, scene) {
+    _updateEnvOptions(options, stdMat, scene, renderParams) {
         options.litOptions.fog = stdMat.useFog ? scene.fog : 'none';
-        options.litOptions.gamma = stdMat.useGammaTonemap ? scene.gammaCorrection : GAMMA_NONE;
-        options.litOptions.toneMap = stdMat.useGammaTonemap ? scene.toneMapping : -1;
+        options.litOptions.gamma = stdMat.useGammaTonemap ? renderParams.gammaCorrection : GAMMA_NONE;
+        options.litOptions.toneMap = stdMat.useGammaTonemap ? renderParams.toneMapping : TONEMAP_LINEAR;
 
         const isPhong = stdMat.shadingModel === SPECULAR_PHONG;
 
diff --git a/src/scene/materials/standard-material.js b/src/scene/materials/standard-material.js
index d3dededee..5426e361c 100644
--- a/src/scene/materials/standard-material.js
+++ b/src/scene/materials/standard-material.js
@@ -884,7 +884,7 @@ class StandardMaterial extends Material {
         this._processParameters('_activeLightingParams');
     }
 
-    getShaderVariant(device, scene, objDefs, unused, pass, sortedLights, viewUniformFormat, viewBindGroupFormat, vertexFormat) {
+    getShaderVariant(device, scene, objDefs, renderParams, pass, sortedLights, viewUniformFormat, viewBindGroupFormat, vertexFormat) {
 
         // update prefiltered lighting data
         this.updateEnvUniforms(device, scene);
@@ -897,7 +897,7 @@ class StandardMaterial extends Material {
         if (minimalOptions)
             this.shaderOptBuilder.updateMinRef(options, scene, this, objDefs, pass, sortedLights);
         else
-            this.shaderOptBuilder.updateRef(options, scene, this, objDefs, pass, sortedLights);
+            this.shaderOptBuilder.updateRef(options, scene, renderParams, this, objDefs, pass, sortedLights);
 
         // execute user callback to modify the options
         if (this.onUpdateShader) {
@@ -1096,7 +1096,7 @@ function _defineColor(name, defaultValue) {
     defineUniform(name, (material, device, scene) => {
         const uniform = material._allocUniform(name, () => new Float32Array(3));
         const color = material[name];
-        const gamma = material.useGammaTonemap && scene.gammaCorrection;
+        const gamma = material.useGammaTonemap && scene.rendering.gammaCorrection;
 
         if (gamma) {
             uniform[0] = Math.pow(color.r, 2.2);
diff --git a/src/scene/mesh-instance.js b/src/scene/mesh-instance.js
index 9541b4c7a..a5c37fc4d 100644
--- a/src/scene/mesh-instance.js
+++ b/src/scene/mesh-instance.js
@@ -31,7 +31,7 @@ const _tempSphere = new BoundingSphere();
 const _meshSet = new Set();
 
 // internal array used to evaluate the hash for the shader instance
-const lookupHashes = new Uint32Array(3);
+const lookupHashes = new Uint32Array(4);
 
 /**
  * Internal data structure used to store data used by hardware instancing.
@@ -514,6 +514,8 @@ class MeshInstance {
      * @param {number} shaderPass - The shader pass index.
      * @param {number} lightHash - The hash value of the lights that are affecting this mesh instance.
      * @param {import('./scene.js').Scene} scene - The scene.
+     * @param {import('./renderer/rendering-params.js').RenderingParams} renderParams - The rendering
+     * parameters.
      * @param {import('../platform/graphics/uniform-buffer-format.js').UniformBufferFormat} [viewUniformFormat] - The
      * format of the view uniform buffer.
      * @param {import('../platform/graphics/bind-group-format.js').BindGroupFormat} [viewBindGroupFormat] - The
@@ -522,7 +524,7 @@ class MeshInstance {
      * @returns {ShaderInstance} - the shader instance.
      * @ignore
      */
-    getShaderInstance(shaderPass, lightHash, scene, viewUniformFormat, viewBindGroupFormat, sortedLights) {
+    getShaderInstance(shaderPass, lightHash, scene, renderParams, viewUniformFormat, viewBindGroupFormat, sortedLights) {
 
         const shaderDefs = this._shaderDefs;
 
@@ -530,6 +532,7 @@ class MeshInstance {
         lookupHashes[0] = shaderPass;
         lookupHashes[1] = lightHash;
         lookupHashes[2] = shaderDefs;
+        lookupHashes[3] = renderParams.hash;
         const hash = hash32Fnv1a(lookupHashes);
 
         // look up the cache
@@ -551,7 +554,7 @@ class MeshInstance {
                 // marker to allow us to see the source node for shader alloc
                 DebugGraphics.pushGpuMarker(this.mesh.device, `Node: ${this.node.name}`);
 
-                const shader = mat.getShaderVariant(this.mesh.device, scene, shaderDefs, null, shaderPass, sortedLights,
+                const shader = mat.getShaderVariant(this.mesh.device, scene, shaderDefs, renderParams, shaderPass, sortedLights,
                                                     viewUniformFormat, viewBindGroupFormat, this._mesh.vertexBuffer?.format);
 
                 DebugGraphics.popGpuMarker(this.mesh.device);
diff --git a/src/scene/particle-system/particle-emitter.js b/src/scene/particle-system/particle-emitter.js
index 160683162..2bfe3251c 100644
--- a/src/scene/particle-system/particle-emitter.js
+++ b/src/scene/particle-system/particle-emitter.js
@@ -816,7 +816,7 @@ class ParticleEmitter {
             this.normalOption = hasNormal ? 2 : 1;
         }
         // getShaderVariant is also called by pc.Scene when all shaders need to be updated
-        this.material.getShaderVariant = function (dev, sc, defs, unused, pass, sortedLights, viewUniformFormat, viewBindGroupFormat) {
+        this.material.getShaderVariant = function (dev, sc, defs, renderParams, pass, sortedLights, viewUniformFormat, viewBindGroupFormat) {
 
             // The app works like this:
             // 1. Emitter init
@@ -838,6 +838,9 @@ class ParticleEmitter {
             const inTools = this.emitter.inTools;
             const processingOptions = new ShaderProcessorOptions(viewUniformFormat, viewBindGroupFormat);
 
+            // renderParams and other parametesr should be passed in, but they are not so work around it
+            renderParams = renderParams ?? this.emitter.camera?.renderingParams ?? this.emitter.scene?.rendering;
+
             const shader = programLib.getProgram('particle', {
                 pass: SHADER_FORWARD,
                 useCpu: this.emitter.useCpu,
@@ -847,8 +850,8 @@ class ParticleEmitter {
                 alignToMotion: this.emitter.alignToMotion,
                 soft: this.emitter.depthSoftening,
                 mesh: this.emitter.useMesh,
-                gamma: this.emitter.scene ? this.emitter.scene.gammaCorrection : 0,
-                toneMap: this.emitter.scene ? this.emitter.scene.toneMapping : 0,
+                gamma: renderParams?.gammaCorrection ?? 0,
+                toneMap: renderParams?.toneMapping ?? 0,
                 fog: (this.emitter.scene && !this.emitter.noFog) ? this.emitter.scene.fog : 'none',
                 wrap: this.emitter.wrap && this.emitter.wrapBounds,
                 localSpace: this.emitter.localSpace,
diff --git a/src/scene/renderer/forward-renderer.js b/src/scene/renderer/forward-renderer.js
index c9fb4c4f6..fa6bdbbc7 100644
--- a/src/scene/renderer/forward-renderer.js
+++ b/src/scene/renderer/forward-renderer.js
@@ -147,7 +147,7 @@ class ForwardRenderer extends Renderer {
         this.ambientColor[0] = scene.ambientLight.r;
         this.ambientColor[1] = scene.ambientLight.g;
         this.ambientColor[2] = scene.ambientLight.b;
-        if (scene.gammaCorrection) {
+        if (scene.rendering.gammaCorrection) {
             for (let i = 0; i < 3; i++) {
                 this.ambientColor[i] = Math.pow(this.ambientColor[i], 2.2);
             }
@@ -228,7 +228,7 @@ class ForwardRenderer extends Renderer {
                 this._resolveLight(scope, cnt);
             }
 
-            this.lightColorId[cnt].setValue(scene.gammaCorrection ? directional._linearFinalColor : directional._finalColor);
+            this.lightColorId[cnt].setValue(scene.rendering.gammaCorrection ? directional._linearFinalColor : directional._finalColor);
 
             // Directional lights shine down the negative Y axis
             wtm.getY(directional._direction).mulScalar(-1);
@@ -303,7 +303,7 @@ class ForwardRenderer extends Renderer {
         }
 
         this.lightRadiusId[cnt].setValue(omni.attenuationEnd);
-        this.lightColorId[cnt].setValue(scene.gammaCorrection ? omni._linearFinalColor : omni._finalColor);
+        this.lightColorId[cnt].setValue(scene.render.gammaCorrection ? omni._linearFinalColor : omni._finalColor);
         wtm.getTranslation(omni._position);
         this.lightPos[cnt][0] = omni._position.x;
         this.lightPos[cnt][1] = omni._position.y;
@@ -359,7 +359,7 @@ class ForwardRenderer extends Renderer {
         this.lightInAngleId[cnt].setValue(spot._innerConeAngleCos);
         this.lightOutAngleId[cnt].setValue(spot._outerConeAngleCos);
         this.lightRadiusId[cnt].setValue(spot.attenuationEnd);
-        this.lightColorId[cnt].setValue(scene.gammaCorrection ? spot._linearFinalColor : spot._finalColor);
+        this.lightColorId[cnt].setValue(scene.rendering.gammaCorrection ? spot._linearFinalColor : spot._finalColor);
         wtm.getTranslation(spot._position);
         this.lightPos[cnt][0] = spot._position.x;
         this.lightPos[cnt][1] = spot._position.y;
@@ -461,6 +461,9 @@ class ForwardRenderer extends Renderer {
     // execute first pass over draw calls, in order to update materials / shaders
     renderForwardPrepareMaterials(camera, drawCalls, sortedLights, layer, pass) {
 
+        // rendering params from the scene, or overridden by the camera
+        const renderParams = camera.renderingParams ?? this.scene.rendering;
+
         const addCall = (drawCall, shaderInstance, isNewMaterial, lightMaskChanged) => {
             _drawCallList.drawCalls.push(drawCall);
             _drawCallList.shaderInstances.push(shaderInstance);
@@ -516,7 +519,7 @@ class ForwardRenderer extends Renderer {
                 }
             }
 
-            const shaderInstance = drawCall.getShaderInstance(pass, lightHash, scene, this.viewUniformFormat, this.viewBindGroupFormat, sortedLights);
+            const shaderInstance = drawCall.getShaderInstance(pass, lightHash, scene, renderParams, this.viewUniformFormat, this.viewBindGroupFormat, sortedLights);
 
             addCall(drawCall, shaderInstance, material !== prevMaterial, !prevMaterial || lightMask !== prevLightMask);
 
@@ -777,7 +780,7 @@ class ForwardRenderer extends Renderer {
             this.fogColor[0] = scene.fogColor.r;
             this.fogColor[1] = scene.fogColor.g;
             this.fogColor[2] = scene.fogColor.b;
-            if (scene.gammaCorrection) {
+            if (scene.rendering.gammaCorrection) {
                 for (let i = 0; i < 3; i++) {
                     this.fogColor[i] = Math.pow(this.fogColor[i], 2.2);
                 }
diff --git a/src/scene/renderer/render-pass-forward.js b/src/scene/renderer/render-pass-forward.js
index 6ec090b3d..fab11bbdc 100644
--- a/src/scene/renderer/render-pass-forward.js
+++ b/src/scene/renderer/render-pass-forward.js
@@ -7,6 +7,7 @@ import { BlendState } from "../../platform/graphics/blend-state.js";
 import { DebugGraphics } from "../../platform/graphics/debug-graphics.js";
 import { RenderPass } from "../../platform/graphics/render-pass.js";
 import { RenderAction } from "../composition/render-action.js";
+import { SHADER_FORWARD } from "../constants.js";
 
 /**
  * A render pass used render a set of layers using a camera.
@@ -264,8 +265,8 @@ class RenderPassForward extends RenderPass {
                 lightClusters: renderAction.lightClusters
             };
 
-            // shader pass - use setting from camera if available, otherwise use layer setting
-            const shaderPass = camera.camera.shaderPassInfo?.index ?? layer.shaderPass;
+            // shader pass - use setting from camera if available, otherwise forward
+            const shaderPass = camera.camera.shaderPassInfo?.index ?? SHADER_FORWARD;
 
             // if this is not a first render action to the render target, or if the render target was not
             // fully cleared on pass start, we need to execute clears here
diff --git a/src/scene/renderer/render-pass-update-clustered.js b/src/scene/renderer/render-pass-update-clustered.js
index 9556c02ba..42b1ae8f2 100644
--- a/src/scene/renderer/render-pass-update-clustered.js
+++ b/src/scene/renderer/render-pass-update-clustered.js
@@ -51,7 +51,7 @@ class RenderPassUpdateClustered extends RenderPass {
 
         const { renderer } = this;
         const { scene } = renderer;
-        renderer.worldClustersAllocator.update(this.frameGraph.renderPasses, scene.gammaCorrection, scene.lighting);
+        renderer.worldClustersAllocator.update(this.frameGraph.renderPasses, scene.rendering.gammaCorrection, scene.lighting);
 
         // #if _PROFILER
         renderer._lightClustersTime += now() - startTime;
diff --git a/src/scene/renderer/rendering-params.js b/src/scene/renderer/rendering-params.js
new file mode 100644
index 000000000..a976de9c5
--- /dev/null
+++ b/src/scene/renderer/rendering-params.js
@@ -0,0 +1,89 @@
+import { hashCode } from '../../core/hash.js';
+import { GAMMA_SRGB, TONEMAP_LINEAR } from '../constants.js';
+
+/**
+ * Rendering parameters, allow configuration of the rendering parameters.
+ *
+ * @category Graphics
+ */
+class RenderingParams {
+    /** @private */
+    _gammaCorrection = GAMMA_SRGB;
+
+    /** @private */
+    _toneMapping = TONEMAP_LINEAR;
+
+    /**
+     * The hash of the rendering parameters, or undefined if the hash has not been computed yet.
+     *
+     * @type {number|undefined}
+     * @private
+     */
+    _hash;
+
+    /**
+     * The hash of the rendering parameters.
+     *
+     * @type {number}
+     * @ignore
+     */
+    get hash() {
+        if (this._hash === undefined) {
+            const key = `${this.gammaCorrection}_${this.toneMapping}`;
+            this._hash = hashCode(key);
+        }
+        return this._hash;
+    }
+
+    markDirty() {
+        this._hash = undefined;
+    }
+
+    /**
+     * The gamma correction to apply when rendering the scene. Can be:
+     *
+     * - {@link GAMMA_NONE}
+     * - {@link GAMMA_SRGB}
+     *
+     * Defaults to {@link GAMMA_SRGB}.
+     *
+     * @type {number}
+     */
+    set gammaCorrection(value) {
+        if (this._gammaCorrection !== value) {
+            this._gammaCorrection = value;
+            this.markDirty();
+        }
+    }
+
+    get gammaCorrection() {
+        return this._gammaCorrection;
+    }
+
+    /**
+     * The tonemapping transform to apply to the rendered color buffer. Can be:
+     *
+     * - {@link TONEMAP_LINEAR}
+     * - {@link TONEMAP_FILMIC}
+     * - {@link TONEMAP_HEJL}
+     * - {@link TONEMAP_ACES}
+     * - {@link TONEMAP_ACES2}
+     * - {@link TONEMAP_NEUTRAL}
+     *
+     * Defaults to {@link TONEMAP_LINEAR}.
+     *
+     * @type {number}
+     */
+    set toneMapping(value) {
+        if (this._toneMapping !== value) {
+            this._toneMapping = value;
+            this.markDirty();
+        }
+    }
+
+    get toneMapping() {
+        return this._toneMapping;
+    }
+}
+
+export { RenderingParams };
diff --git a/src/scene/renderer/shadow-renderer.js b/src/scene/renderer/shadow-renderer.js
index 2f5f7512e..30836fd58 100644
--- a/src/scene/renderer/shadow-renderer.js
+++ b/src/scene/renderer/shadow-renderer.js
@@ -24,6 +24,7 @@ import { LightCamera } from './light-camera.js';
 import { UniformBufferFormat, UniformFormat } from '../../platform/graphics/uniform-buffer-format.js';
 import { BindUniformBufferFormat, BindGroupFormat } from '../../platform/graphics/bind-group-format.js';
 import { BlendState } from '../../platform/graphics/blend-state.js';
+import { RenderingParams } from './rendering-params.js';
 
 function gauss(x, sigma) {
     return Math.exp(-(x * x) / (2.0 * sigma * sigma));
@@ -106,6 +107,9 @@ class ShadowRenderer {
         this.blendStateWrite = new BlendState();
         this.blendStateNoWrite = new BlendState();
         this.blendStateNoWrite.setColorWrite(false, false, false, false);
+
+        // shadow rendering parameters
+        this.shadowRenderingParams = new RenderingParams();
     }
 
     // creates shadow camera for a light and sets up its constant properties
@@ -283,6 +287,7 @@ class ShadowRenderer {
         const scene = renderer.scene;
         const passFlags = 1 << SHADER_SHADOW;
         const shadowPass = this.getShadowPass(light);
+        const renderParams = this.shadowRenderingParams;
 
         // Render
         const count = visibleCasters.length;
@@ -313,7 +318,7 @@ class ShadowRenderer {
                 meshInstance.setParameters(device, passFlags);
             }
 
-            const shaderInstance = meshInstance.getShaderInstance(shadowPass, 0, scene, this.viewUniformFormat, this.viewBindGroupFormat);
+            const shaderInstance = meshInstance.getShaderInstance(shadowPass, 0, scene, renderParams, this.viewUniformFormat, this.viewBindGroupFormat);
             const shadowShader = shaderInstance.shader;
             Debug.assert(shadowShader, `no shader for pass ${shadowPass}`, material);
 
diff --git a/src/scene/scene.js b/src/scene/scene.js
index 464542ecd..66a1d268e 100644
--- a/src/scene/scene.js
+++ b/src/scene/scene.js
@@ -9,11 +9,12 @@ import { Mat4 } from '../core/math/mat4.js';
 
 import { PIXELFORMAT_RGBA8, ADDRESS_CLAMP_TO_EDGE, FILTER_LINEAR } from '../platform/graphics/constants.js';
 
-import { BAKE_COLORDIR, FOG_NONE, GAMMA_SRGB, LAYERID_IMMEDIATE } from './constants.js';
+import { BAKE_COLORDIR, FOG_NONE, LAYERID_IMMEDIATE } from './constants.js';
 import { LightingParams } from './lighting/lighting-params.js';
 import { Sky } from './skybox/sky.js';
 import { Immediate } from './immediate/immediate.js';
 import { EnvLighting } from './graphics/env-lighting.js';
+import { RenderingParams } from './renderer/rendering-params.js';
 
 /**
  * A scene is graphical representation of an environment. It manages the scene hierarchy, all
@@ -39,7 +40,7 @@ class Scene extends EventHandler {
      *                 layer.onDisable = myOnDisableFunction;
      *                 break;
      *             case 'MyOtherLayer':
-     *                 layer.shaderPass = myShaderPass;
+     *                 layer.clearColorBuffer = true;
      *                 break;
      *         }
      *     }
@@ -214,6 +215,13 @@ class Scene extends EventHandler {
      */
     _skyboxCubeMap = null;
 
+    /**
+     * The rendering parameters.
+     *
+     * @private
+     */
+    _renderingParams = new RenderingParams();
+
     /**
      * Create a new Scene instance.
      *
@@ -237,9 +245,6 @@ class Scene extends EventHandler {
 
         this._fog = FOG_NONE;
 
-        this._gammaCorrection = GAMMA_SRGB;
-        this._toneMapping = 0;
-
         /**
          * Array of 6 prefiltered lighting data cubemaps.
          *
@@ -446,32 +451,6 @@ class Scene extends EventHandler {
         return this._fog;
     }
 
-    /**
-     * Sets the gamma correction to apply when rendering the scene. Can be:
-     *
-     * - {@link GAMMA_NONE}
-     * - {@link GAMMA_SRGB}
-     *
-     * Defaults to {@link GAMMA_SRGB}.
-     *
-     * @type {number}
-     */
-    set gammaCorrection(value) {
-        if (value !== this._gammaCorrection) {
-            this._gammaCorrection = value;
-            this.updateShaders = true;
-        }
-    }
-
-    /**
-     * Gets the gamma correction to apply when rendering the scene.
-     *
-     * @type {number}
-     */
-    get gammaCorrection() {
-        return this._gammaCorrection;
-    }
-
     /**
      * Sets the {@link LayerComposition} that defines rendering order of this scene.
      *
@@ -505,6 +484,15 @@ class Scene extends EventHandler {
         return this._lightingParams;
     }
 
+    /**
+     * A {@link RenderingParams} that defines rendering parameters.
+     *
+     * @type {RenderingParams}
+     */
+    get rendering() {
+        return this._renderingParams;
+    }
+
     /**
      * Sets the range parameter of the bilateral filter. It's used when {@link Scene#lightmapFilterEnabled}
      * is enabled. Larger value applies more widespread blur. This needs to be a positive non-zero
@@ -707,36 +695,6 @@ class Scene extends EventHandler {
         return this._skyboxRotation;
     }
 
-    /**
-     * Sets the tonemapping transform to apply when writing fragments to the frame buffer. Can be:
-     *
-     * - {@link TONEMAP_LINEAR}
-     * - {@link TONEMAP_FILMIC}
-     * - {@link TONEMAP_HEJL}
-     * - {@link TONEMAP_ACES}
-     * - {@link TONEMAP_ACES2}
-     * - {@link TONEMAP_NEUTRAL}
-     *
-     * Defaults to {@link TONEMAP_LINEAR}.
-     *
-     * @type {number}
-     */
-    set toneMapping(value) {
-        if (value !== this._toneMapping) {
-            this._toneMapping = value;
-            this.updateShaders = true;
-        }
-    }
-
-    /**
-     * Gets the tonemapping transform to apply when writing fragments to the frame buffer.
-     *
-     * @type {number}
-     */
-    get toneMapping() {
-        return this._toneMapping;
-    }
-
     destroy() {
         this._resetSkyMesh();
         this.root = null;
@@ -771,8 +729,8 @@ class Scene extends EventHandler {
         this.fogStart = render.fog_start;
         this.fogEnd = render.fog_end;
         this.fogDensity = render.fog_density;
-        this._gammaCorrection = render.gamma_correction;
-        this._toneMapping = render.tonemapping;
+        this._renderingParams.gammaCorrection = render.gamma_correction;
+        this._renderingParams.toneMapping = render.tonemapping;
         this.lightmapSizeMultiplier = render.lightmapSizeMultiplier;
         this.lightmapMaxResolution = render.lightmapMaxResolution;
         this.lightmapMode = render.lightmapMode;
diff --git a/src/scene/shader-lib/chunks/common/frag/gamma2_2.js b/src/scene/shader-lib/chunks/common/frag/gamma2_2.js
index 03e79e10d..fe7708f5b 100644
--- a/src/scene/shader-lib/chunks/common/frag/gamma2_2.js
+++ b/src/scene/shader-lib/chunks/common/frag/gamma2_2.js
@@ -12,10 +12,6 @@ vec4 gammaCorrectInput(vec4 color) {
 }
 
 vec3 gammaCorrectOutput(vec3 color) {
-#ifdef HDR
-    return color;
-#else
     return pow(color + 0.0000001, vec3(1.0 / 2.2));
-#endif
 }
 `;
diff --git a/src/scene/shader-lib/chunks/lit/frag/end.js b/src/scene/shader-lib/chunks/lit/frag/end.js
index 2543245fd..2740295c1 100644
--- a/src/scene/shader-lib/chunks/lit/frag/end.js
+++ b/src/scene/shader-lib/chunks/lit/frag/end.js
@@ -3,9 +3,6 @@ export default /* glsl */`
 
     gl_FragColor.rgb += litArgs_emission;
     gl_FragColor.rgb = addFog(gl_FragColor.rgb);
-
-    #ifndef HDR
     gl_FragColor.rgb = toneMap(gl_FragColor.rgb);
     gl_FragColor.rgb = gammaCorrectOutput(gl_FragColor.rgb);
-    #endif
 `;
diff --git a/src/scene/shader-lib/program-library.js b/src/scene/shader-lib/program-library.js
index 86db8db4a..8e9d40e6f 100644
--- a/src/scene/shader-lib/program-library.js
+++ b/src/scene/shader-lib/program-library.js
@@ -7,6 +7,7 @@ import { Shader } from '../../platform/graphics/shader.js';
 import { SHADER_FORWARD, SHADER_DEPTH, SHADER_PICK, SHADER_SHADOW, SHADER_PREPASS_VELOCITY } from '../constants.js';
 import { ShaderPass } from '../shader-pass.js';
 import { StandardMaterialOptions } from '../materials/standard-material-options.js';
+import { RenderingParams } from '../renderer/rendering-params.js';
 
 /**
  * A class responsible for creation and caching of required shaders.
@@ -48,8 +49,9 @@ class ProgramLibrary {
         this._defaultStdMatOption = new StandardMaterialOptions();
         this._defaultStdMatOptionMin = new StandardMaterialOptions();
 
+        const defaultRenderParams = new RenderingParams();
         standardMaterial.shaderOptBuilder.updateRef(
-            this._defaultStdMatOption, {}, standardMaterial, null, [], SHADER_FORWARD, null);
+            this._defaultStdMatOption, {}, defaultRenderParams, standardMaterial, null, [], SHADER_FORWARD, null);
         standardMaterial.shaderOptBuilder.updateMinRef(
             this._defaultStdMatOptionMin, {}, standardMaterial, null, SHADER_SHADOW, null);
 
diff --git a/src/scene/shader-lib/programs/shader-generator.js b/src/scene/shader-lib/programs/shader-generator.js
index 36adc18d5..0178f4056 100644
--- a/src/scene/shader-lib/programs/shader-generator.js
+++ b/src/scene/shader-lib/programs/shader-generator.js
@@ -1,5 +1,5 @@
 import {
-    GAMMA_SRGB, GAMMA_SRGBFAST, GAMMA_SRGBHDR,
+    GAMMA_SRGB,
     TONEMAP_ACES, TONEMAP_ACES2, TONEMAP_FILMIC, TONEMAP_HEJL, TONEMAP_NEUTRAL, TONEMAP_LINEAR
 } from '../../constants.js';
 import { shaderChunks } from '../chunks/chunks.js';
@@ -29,10 +29,8 @@ class ShaderGenerator {
     }
 
     static gammaCode(value, chunks = shaderChunks) {
-        if (value === GAMMA_SRGB || value === GAMMA_SRGBFAST) {
+        if (value === GAMMA_SRGB) {
             return chunks.gamma2_2PS ?? shaderChunks.gamma2_2PS;
-        } else if (value === GAMMA_SRGBHDR) {
-            return "#define HDR\n" + (chunks.gamma2_2PS ?? shaderChunks.gamma2_2PS);
         }
         return chunks.gamma1_0PS ?? shaderChunks.gamma1_0PS;
     }
diff --git a/src/scene/shader-lib/programs/standard.js b/src/scene/shader-lib/programs/standard.js
index a3edf677b..659d496a3 100644
--- a/src/scene/shader-lib/programs/standard.js
+++ b/src/scene/shader-lib/programs/standard.js
@@ -2,7 +2,7 @@ import { Debug } from '../../../core/debug.js';
 
 import {
     BLEND_NONE, DITHER_BAYER8, DITHER_NONE, FRESNEL_SCHLICK,
-    SHADER_FORWARD, SHADER_FORWARDHDR,
+    SHADER_FORWARD,
     SPECULAR_PHONG,
     SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED
 } from '../../constants.js';
@@ -61,7 +61,7 @@ class ShaderGeneratorStandard extends ShaderGenerator {
     _getUvSourceExpression(transformPropName, uVPropName, options) {
         const transformId = options[transformPropName];
         const uvChannel = options[uVPropName];
-        const isMainPass = options.litOptions.pass === SHADER_FORWARD || options.litOptions.pass === SHADER_FORWARDHDR;
+        const isMainPass = options.litOptions.pass === SHADER_FORWARD;
 
         let expression;
         if (isMainPass && options.litOptions.nineSlicedMode === SPRITE_RENDERMODE_SLICED) {
diff --git a/src/scene/shader-pass.js b/src/scene/shader-pass.js
index 8f2adab5e..63d68ffbe 100644
--- a/src/scene/shader-pass.js
+++ b/src/scene/shader-pass.js
@@ -1,6 +1,6 @@
 import { Debug } from '../core/debug.js';
 import {
-    SHADER_FORWARD, SHADER_FORWARDHDR, SHADER_DEPTH, SHADER_PICK, SHADER_SHADOW, SHADER_PREPASS_VELOCITY
+    SHADER_FORWARD, SHADER_DEPTH, SHADER_PICK, SHADER_SHADOW, SHADER_PREPASS_VELOCITY
 } from './constants.js';
 
 import { DeviceCache } from '../platform/graphics/device-cache.js';
@@ -103,11 +103,10 @@ class ShaderPass {
 
         // add default passes in the required order, to match the constants
         add('forward', SHADER_FORWARD, { isForward: true });
-        add('forward_hdr', SHADER_FORWARDHDR, { isForward: true });
+        add('prepass', SHADER_PREPASS_VELOCITY);
         add('depth', SHADER_DEPTH);
         add('pick', SHADER_PICK);
         add('shadow', SHADER_SHADOW);
-        add('prepass', SHADER_PREPASS_VELOCITY);
     }
 
     /**
diff --git a/src/scene/skybox/sky-mesh.js b/src/scene/skybox/sky-mesh.js
index d66ab71db..78f8714a5 100644
--- a/src/scene/skybox/sky-mesh.js
+++ b/src/scene/skybox/sky-mesh.js
@@ -1,7 +1,7 @@
 import { CULLFACE_FRONT } from '../../platform/graphics/constants.js';
 import { ShaderProcessorOptions } from '../../platform/graphics/shader-processor-options.js';
 
-import { GAMMA_NONE, LAYERID_SKYBOX, SHADER_FORWARDHDR, TONEMAP_LINEAR } from '../constants.js';
+import { LAYERID_SKYBOX } from '../constants.js';
 import { Material } from '../materials/material.js';
 import { MeshInstance } from '../mesh-instance.js';
 import { getProgramLibrary } from '../shader-lib/get-program-library.js';
@@ -33,13 +33,13 @@ class SkyMesh {
         const material = new Material();
         material.name = 'SkyMaterial';
 
-        material.getShaderVariant = function (dev, sc, defs, unused, pass, sortedLights, viewUniformFormat, viewBindGroupFormat) {
+        material.getShaderVariant = function (dev, sc, defs, renderParams, pass, sortedLights, viewUniformFormat, viewBindGroupFormat) {
 
             const options = {
                 pass: pass,
                 encoding: texture.encoding,
-                gamma: (pass === SHADER_FORWARDHDR ? GAMMA_NONE : scene.gammaCorrection),
-                toneMapping: (pass === SHADER_FORWARDHDR ? TONEMAP_LINEAR : scene.toneMapping),
+                gamma: renderParams.gammaCorrection,
+                toneMapping: renderParams.toneMapping,
                 skymesh: type
             };
 


Test patch:
diff --git a/src/framework/xr/xr-hit-test-source.js b/src/framework/xr/xr-hit-test-source.js
index 53d6bdfb5..23a95177e 100644
--- a/src/framework/xr/xr-hit-test-source.js
+++ b/src/framework/xr/xr-hit-test-source.js
@@ -89,7 +89,7 @@ class XrHitTestSource extends EventHandler {
      * Create a new XrHitTestSource instance.
      *
      * @param {import('./xr-manager.js').XrManager} manager - WebXR Manager.
-     * @param {*} xrHitTestSource - XRHitTestSource object that is created by WebXR API.
+     * @param {XRHitTestSource} xrHitTestSource - XRHitTestSource object that is created by WebXR API.
      * @param {boolean} transient - True if XRHitTestSource created for input source profile.
      * @param {null|import('./xr-input-source.js').XrInputSource} inputSource - Input Source for which hit test is created for, or null.
      * @ignore
@@ -127,7 +127,7 @@ class XrHitTestSource extends EventHandler {
     }
 
     /**
-     * @param {*} frame - XRFrame from requestAnimationFrame callback.
+     * @param {XRFrame} frame - XRFrame from requestAnimationFrame callback.
      * @ignore
      */
     update(frame) {
diff --git a/src/framework/xr/xr-hit-test.js b/src/framework/xr/xr-hit-test.js
index e775c69a5..ca39d5f47 100644
--- a/src/framework/xr/xr-hit-test.js
+++ b/src/framework/xr/xr-hit-test.js
@@ -337,7 +337,7 @@ class XrHitTest extends EventHandler {
     }
 
     /**
-     * @param {*} frame - XRFrame from requestAnimationFrame callback.
+     * @param {XRFrame} frame - XRFrame from requestAnimationFrame callback.
      * @ignore
      */
     update(frame) {
diff --git a/test/framework/entity.test.mjs b/test/framework/entity.test.mjs
index 51dd33c1e..1cd638507 100644
--- a/test/framework/entity.test.mjs
+++ b/test/framework/entity.test.mjs
@@ -32,6 +32,7 @@ import { DummyComponentSystem } from './test-component/system.mjs';
 import { HTMLCanvasElement } from '@playcanvas/canvas-mock';
 
 import { expect } from 'chai';
+import { stub } from 'sinon';
 
 describe('Entity', function () {
 
@@ -121,6 +122,68 @@ describe('Entity', function () {
             });
         }
 
+        it('respects components order on disable', function () {
+            const entity = new Entity();
+            entity.enabled = true;
+
+            entity.addComponent('collision');
+            entity.addComponent('rigidbody');
+
+            const colOnDisable = stub();
+            const rbOnDisable = stub();
+            let disableOrder = 0;
+
+            entity.collision.onDisable = colOnDisable;
+            entity.rigidbody.onDisable = rbOnDisable;
+
+            colOnDisable.onFirstCall().callsFake(() => {
+                disableOrder = 2;
+            });
+            rbOnDisable.onFirstCall().callsFake(() => {
+                disableOrder = 1;
+            });
+
+            entity.enabled = false;
+
+            expect(disableOrder).to.equal(2);
+
+            entity.destroy();
+        });
+
+        it('respects components order on enable', function () {
+            const entity = new Entity('Child');
+            const parent = new Entity('Parent');
+
+            parent.addChild(entity);
+            parent._enabled = true;
+            parent._enabledInHierarchy = true;
+
+            entity.addComponent('collision');
+            entity.addComponent('rigidbody');
+
+            entity.enabled = false;
+
+            const rbOnEnable = stub();
+            const colOnEnable = stub();
+            let enableOrder = 0;
+
+            entity.collision.onEnable = colOnEnable;
+            entity.rigidbody.onEnable = rbOnEnable;
+
+            colOnEnable.onFirstCall().callsFake(() => {
+                enableOrder = 2;
+            });
+            rbOnEnable.onFirstCall().callsFake(() => {
+                enableOrder = 1;
+            });
+
+            entity.enabled = true;
+
+            expect(enableOrder).to.equal(2);
+
+            parent.destroy();
+        });
+
     });
 
     const createSubtree = () => {