From 0ab8933ccd3c125faaa57eea31751b736bfb8361 Mon Sep 17 00:00:00 2001 From: Martin Valigursky <59932779+mvaligursky@users.noreply.github.com> Date: Tue, 23 Jul 2024 12:45:53 +0100 Subject: [PATCH] Support for sRGB framebuffer on WebGPU (#6838) * Support for sRGB framebuffer on WebGPU * lint * examples lint --------- Co-authored-by: Martin Valigursky --- .../examples/graphics/area-lights.example.mjs | 8 +- .../material-transparency.example.mjs | 153 ++++++++++++++++++ .../graphics/particles-anim-index.example.mjs | 2 +- .../particles-random-sprites.example.mjs | 4 +- .../graphics/particles-snow.example.mjs | 2 +- .../graphics/particles-spark.example.mjs | 2 +- .../graphics/post-processing.example.mjs | 6 +- .../graphics/reflection-planar.shader.frag | 4 - .../examples/graphics/shader-burn.shader.frag | 4 - .../graphics/shader-compile.example.mjs | 9 +- .../examples/graphics/shader-toon.example.mjs | 13 +- .../examples/graphics/shader-toon.shader.frag | 7 +- .../graphics/shader-wobble.shader.frag | 4 - .../graphics/texture-array.example.mjs | 18 +-- .../graphics/texture-array.ground.frag | 2 +- .../graphics/texture-array.shader.frag | 2 +- src/extras/gizmo/axis-shapes.js | 29 ++-- .../render-passes/render-pass-compose.js | 21 ++- .../components/camera/post-effect-queue.js | 12 +- src/platform/graphics/constants.js | 44 ++++- .../graphics/graphics-device-create.js | 6 + src/platform/graphics/graphics-device.js | 5 +- src/platform/graphics/render-pass.js | 12 +- src/platform/graphics/render-target.js | 13 +- src/platform/graphics/webgl/webgl-texture.js | 9 +- src/platform/graphics/webgpu/constants.js | 3 +- .../graphics/webgpu/webgpu-graphics-device.js | 28 +++- .../graphics/webgpu/webgpu-render-target.js | 32 +++- src/scene/immediate/immediate.js | 2 +- src/scene/particle-system/particle-emitter.js | 34 ++-- src/scene/renderer/forward-renderer.js | 2 +- src/scene/renderer/render-pass-forward.js | 3 +- .../chunks/particle/frag/particle.js | 4 +- .../chunks/standard/frag/opacity-dither.js | 3 + src/scene/shader-lib/programs/particle.js | 12 +- 35 files changed, 374 insertions(+), 140 deletions(-) create mode 100644 examples/src/examples/graphics/material-transparency.example.mjs diff --git a/examples/src/examples/graphics/area-lights.example.mjs b/examples/src/examples/graphics/area-lights.example.mjs index a83e6b27c4b..26f45a9e100 100644 --- a/examples/src/examples/graphics/area-lights.example.mjs +++ b/examples/src/examples/graphics/area-lights.example.mjs @@ -56,14 +56,12 @@ assetListLoader.load(() => { * @param {string} primitiveType - The primitive type. * @param {pc.Vec3} position - The position. * @param {pc.Vec3} scale - The scale. - * @param {pc.Color} color - The color. * @param {any} assetManifest - The asset manifest. * @returns {pc.Entity} The returned entity. */ - function createPrimitive(primitiveType, position, scale, color, assetManifest) { + function createPrimitive(primitiveType, position, scale, assetManifest) { // create material of specified color const material = new pc.StandardMaterial(); - material.diffuse = color; material.gloss = 0.8; material.useMetalness = true; @@ -81,7 +79,7 @@ assetListLoader.load(() => { material.update(); // create primitive - const primitive = new pc.Entity(); + const primitive = new pc.Entity(primitiveType); primitive.addComponent('render', { type: primitiveType, material: material @@ -194,7 +192,7 @@ assetListLoader.load(() => { app.scene.envAtlas = assets.helipad.resource; // create ground plane - createPrimitive('plane', new pc.Vec3(0, 0, 0), new pc.Vec3(20, 20, 20), new pc.Color(0.3, 0.3, 0.3), assets); + createPrimitive('plane', new pc.Vec3(0, 0, 0), new pc.Vec3(20, 20, 20), assets); // get the instance of the statue and set up with render component const statue = assets.statue.resource.instantiateRenderEntity(); diff --git a/examples/src/examples/graphics/material-transparency.example.mjs b/examples/src/examples/graphics/material-transparency.example.mjs new file mode 100644 index 00000000000..9a45bd8b247 --- /dev/null +++ b/examples/src/examples/graphics/material-transparency.example.mjs @@ -0,0 +1,153 @@ +import * as pc from 'playcanvas'; +import { deviceType, rootPath } from 'examples/utils'; + +const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas')); +window.focus(); + +const assets = { + font: new pc.Asset('font', 'font', { url: rootPath + '/static/assets/fonts/arial.json' }) +}; + +const gfxOptions = { + deviceTypes: [deviceType], + glslangUrl: rootPath + '/static/lib/glslang/glslang.js', + twgslUrl: rootPath + '/static/lib/twgsl/twgsl.js', + + // disable anti-aliasing to make dithering more pronounced + antialias: false, + + // use sRGB for display format (only supported on WebGPU, fallbacks to LDR on WebGL2) + displayFormat: pc.DISPLAYFORMAT_LDR_SRGB +}; + +const device = await pc.createGraphicsDevice(canvas, gfxOptions); + +// make dithering more pronounced by rendering to lower resolution +device.maxPixelRatio = 1; + +const createOptions = new pc.AppOptions(); +createOptions.graphicsDevice = device; + +createOptions.componentSystems = [ + pc.RenderComponentSystem, + pc.CameraComponentSystem, + pc.LightComponentSystem, + pc.ElementComponentSystem +]; +createOptions.resourceHandlers = [ + pc.TextureHandler, + pc.FontHandler +]; + +const app = new pc.AppBase(canvas); +app.init(createOptions); + +// Set the canvas to fill the window and automatically change resolution to be the same as the canvas size +app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW); +app.setCanvasResolution(pc.RESOLUTION_AUTO); + +// Ensure canvas is resized when window changes size +const resize = () => app.resizeCanvas(); +window.addEventListener('resize', resize); +app.on('destroy', () => { + window.removeEventListener('resize', resize); +}); + +const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets); +assetListLoader.load(() => { + app.start(); + + app.scene.rendering.toneMapping = pc.TONEMAP_LINEAR; + + // Create an entity with a camera component + const camera = new pc.Entity(); + camera.addComponent('camera', { + clearColor: pc.Color.BLACK + }); + camera.translate(0, -0.5, 14); + camera.rotate(0, 0, 0); + app.root.addChild(camera); + + const NUM_SPHERES_X = 4; + const NUM_SPHERES_Z = 10; + + const ditherOptions = [ + pc.DITHER_NONE, + pc.DITHER_BAYER8, + pc.DITHER_BLUENOISE, + pc.DITHER_IGNNOISE + ]; + + /** + * @param {number} x - The x coordinate. + * @param {number} z - The z coordinate. + */ + const createSphere = function (x, z) { + const material = new pc.StandardMaterial(); + material.name = `material-${ditherOptions[x]}-${z}`; + material.emissive = new pc.Color(1, 0, 0); + material.specular = new pc.Color(1, 1, 1); + material.metalness = 0.0; + material.gloss = 0.5; + material.useMetalness = true; + + if (ditherOptions[x] === pc.DITHER_NONE) { + // alpha blending material + material.blendType = pc.BLEND_NORMAL; + } else { + // alpha dithering material + material.opacityDither = ditherOptions[x]; + } + + // we want the spheres to seem to fade out in a linear fashion, so we need to convert + // the perceived opacity value from sRGB to linear space + const perceivedOpacity = (z + 1) / NUM_SPHERES_Z; + const linearOpacity = Math.pow(perceivedOpacity, 2.2); + material.opacity = linearOpacity; + + material.update(); + + const sphere = new pc.Entity(`entity-${ditherOptions[x]}-${z}`); + sphere.addComponent('render', { + material: material, + type: 'sphere' + }); + sphere.setLocalPosition(1.5 * (x - (NUM_SPHERES_X - 1) * 0.5), z - (NUM_SPHERES_Z - 1) * 0.5, 0); + sphere.setLocalScale(0.9, 0.9, 0.9); + app.root.addChild(sphere); + }; + /** + * @param {pc.Asset} fontAsset - The font asset. + * @param {string} message - The message. + * @param {number} x - The x coordinate. + * @param {number} y - The y coordinate. + */ + const createText = function (fontAsset, message, x, y) { + // Create a text element-based entity + const text = new pc.Entity(); + text.addComponent('element', { + anchor: [0.5, 0.5, 0.5, 0.5], + fontAsset: fontAsset, + fontSize: 0.3, + pivot: [0.5, 0.5], + text: message, + type: pc.ELEMENTTYPE_TEXT + }); + text.setLocalPosition(x, y, 0); + app.root.addChild(text); + }; + + for (let i = 0; i < NUM_SPHERES_X; i++) { + for (let j = 0; j < NUM_SPHERES_Z; j++) { + createSphere(i, j); + } + } + + const y = (NUM_SPHERES_Z + 1) * -0.5; + createText(assets.font, 'Alpha\nBlend', NUM_SPHERES_X * -0.6, y); + createText(assets.font, 'Bayer8\nDither', NUM_SPHERES_X * -0.2, y); + createText(assets.font, 'Blue-noise\nDither', NUM_SPHERES_X * 0.2, y); + createText(assets.font, 'IGN-noise\nDither', NUM_SPHERES_X * 0.6, y); +}); + +export { app }; diff --git a/examples/src/examples/graphics/particles-anim-index.example.mjs b/examples/src/examples/graphics/particles-anim-index.example.mjs index 655812e01a9..fa90d71e2df 100644 --- a/examples/src/examples/graphics/particles-anim-index.example.mjs +++ b/examples/src/examples/graphics/particles-anim-index.example.mjs @@ -7,7 +7,7 @@ window.focus(); const assets = { particlesNumbers: new pc.Asset('particlesNumbers', 'texture', { url: rootPath + '/static/assets/textures/particles-numbers.png' - }) + }, { srgb: true }) }; const gfxOptions = { diff --git a/examples/src/examples/graphics/particles-random-sprites.example.mjs b/examples/src/examples/graphics/particles-random-sprites.example.mjs index 0e2acea7e63..9c14ca43529 100644 --- a/examples/src/examples/graphics/particles-random-sprites.example.mjs +++ b/examples/src/examples/graphics/particles-random-sprites.example.mjs @@ -7,10 +7,10 @@ window.focus(); const assets = { particlesCoinsTexture: new pc.Asset('particlesCoinsTexture', 'texture', { url: rootPath + '/static/assets/textures/particles-coins.png' - }), + }, { srgb: true }), particlesBonusTexture: new pc.Asset('particlesBonusTexture', 'texture', { url: rootPath + '/static/assets/textures/particles-bonus.png' - }) + }, { srgb: true }) }; const gfxOptions = { diff --git a/examples/src/examples/graphics/particles-snow.example.mjs b/examples/src/examples/graphics/particles-snow.example.mjs index 82c2532753e..88dbba0f81c 100644 --- a/examples/src/examples/graphics/particles-snow.example.mjs +++ b/examples/src/examples/graphics/particles-snow.example.mjs @@ -5,7 +5,7 @@ const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('applic window.focus(); const assets = { - snowflake: new pc.Asset('snowflake', 'texture', { url: rootPath + '/static/assets/textures/snowflake.png' }) + snowflake: new pc.Asset('snowflake', 'texture', { url: rootPath + '/static/assets/textures/snowflake.png' }, { srgb: true }) }; const gfxOptions = { diff --git a/examples/src/examples/graphics/particles-spark.example.mjs b/examples/src/examples/graphics/particles-spark.example.mjs index ec92ad73f61..2671743825f 100644 --- a/examples/src/examples/graphics/particles-spark.example.mjs +++ b/examples/src/examples/graphics/particles-spark.example.mjs @@ -5,7 +5,7 @@ const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('applic window.focus(); const assets = { - spark: new pc.Asset('spark', 'texture', { url: rootPath + '/static/assets/textures/spark.png' }) + spark: new pc.Asset('spark', 'texture', { url: rootPath + '/static/assets/textures/spark.png' }, { srgb: true }) }; const gfxOptions = { diff --git a/examples/src/examples/graphics/post-processing.example.mjs b/examples/src/examples/graphics/post-processing.example.mjs index 7dba7145acb..729a6bc62f0 100644 --- a/examples/src/examples/graphics/post-processing.example.mjs +++ b/examples/src/examples/graphics/post-processing.example.mjs @@ -189,7 +189,11 @@ assetListLoader.load(() => { const label = new pc.Entity(name); label.addComponent('element', { text: text, - color: new pc.Color(100, 50, 80), // very bright color to affect the bloom + + // very bright color to affect the bloom - this is not correct, as this is sRGB color that + // is valid only in 0..1 range, but UI does not expose emissive intensity currently + color: new pc.Color(18, 15, 5), + anchor: new pc.Vec4(x, y, 0.5, 0.5), fontAsset: assets.font, fontSize: 28, diff --git a/examples/src/examples/graphics/reflection-planar.shader.frag b/examples/src/examples/graphics/reflection-planar.shader.frag index 216e6e6c5be..db6ab1f6215 100644 --- a/examples/src/examples/graphics/reflection-planar.shader.frag +++ b/examples/src/examples/graphics/reflection-planar.shader.frag @@ -4,10 +4,6 @@ uniform vec4 uScreenSize; // reflection texture uniform sampler2D uDiffuseMap; -vec3 gammaCorrectOutput(vec3 color) { - return pow(color + 0.0000001, vec3(1.0 / 2.2)); -} - void main(void) { // sample reflection texture diff --git a/examples/src/examples/graphics/shader-burn.shader.frag b/examples/src/examples/graphics/shader-burn.shader.frag index d1072c9aa5c..76090dba935 100644 --- a/examples/src/examples/graphics/shader-burn.shader.frag +++ b/examples/src/examples/graphics/shader-burn.shader.frag @@ -7,10 +7,6 @@ uniform sampler2D uDiffuseMap; uniform sampler2D uHeightMap; uniform float uTime; -vec3 gammaCorrectOutput(vec3 color) { - return pow(color + 0.0000001, vec3(1.0 / 2.2)); -} - void main(void) { float height = texture2D(uHeightMap, vUv0).r; diff --git a/examples/src/examples/graphics/shader-compile.example.mjs b/examples/src/examples/graphics/shader-compile.example.mjs index 79264d3ee72..70ad5258303 100644 --- a/examples/src/examples/graphics/shader-compile.example.mjs +++ b/examples/src/examples/graphics/shader-compile.example.mjs @@ -61,15 +61,13 @@ assetListLoader.load(() => { * @param {string} primitiveType - The primitive type. * @param {pc.Vec3} position - The position. * @param {pc.Vec3} scale - The scale. - * @param {pc.Color} color - The color. * @param {any} assetManifest - The asset manifest. * @param {boolean} [id] - Prevent shader compilation caching. * @returns {pc.Entity} The entity. */ - function createPrimitive(primitiveType, position, scale, color, assetManifest, id = false) { + function createPrimitive(primitiveType, position, scale, assetManifest, id = false) { // create material of specified color const material = new pc.StandardMaterial(); - material.diffuse = color; material.gloss = 0.4; material.useMetalness = true; @@ -125,7 +123,7 @@ assetListLoader.load(() => { app.scene.envAtlas = assets.helipad.resource; // create ground plane - createPrimitive('plane', new pc.Vec3(0, 0, 0), new pc.Vec3(20, 20, 20), new pc.Color(0.3, 0.3, 0.3), assets); + createPrimitive('plane', new pc.Vec3(0, 0, 0), new pc.Vec3(20, 20, 20), assets); // Create the camera, which renders entities const camera = new pc.Entity(); @@ -142,8 +140,7 @@ assetListLoader.load(() => { for (let x = -10; x <= 10; x += 6) { for (let y = -10; y <= 10; y += 6) { const pos = new pc.Vec3(x, 0.6, y); - const color = new pc.Color(0.3 + Math.random() * 0.7, 0.3 + Math.random() * 0.7, 0.3 + Math.random() * 0.7); - createPrimitive('sphere', pos, new pc.Vec3(1, 1, 1), color, assets, true); + createPrimitive('sphere', pos, new pc.Vec3(1, 1, 1), assets, true); } } diff --git a/examples/src/examples/graphics/shader-toon.example.mjs b/examples/src/examples/graphics/shader-toon.example.mjs index a41fb9481a8..13a66f213bd 100644 --- a/examples/src/examples/graphics/shader-toon.example.mjs +++ b/examples/src/examples/graphics/shader-toon.example.mjs @@ -87,26 +87,17 @@ assetListLoader.load(() => { * Set the new material on all meshes in the model, and use original texture from the model on the new material * @type {pc.Texture | null} */ - let originalTexture = null; /** @type {Array} */ const renders = entity.findComponents('render'); renders.forEach((render) => { - const meshInstances = render.meshInstances; - for (let i = 0; i < meshInstances.length; i++) { - const meshInstance = meshInstances[i]; - if (!originalTexture) { - /** @type {pc.StandardMaterial} */ - const originalMaterial = meshInstance.material; - originalTexture = originalMaterial.diffuseMap; - } + render.meshInstances.forEach((meshInstance) => { meshInstance.material = material; - } + }); }); // material parameters const lightPosArray = [light.getPosition().x, light.getPosition().y, light.getPosition().z]; material.setParameter('uLightPos', lightPosArray); - material.setParameter('uTexture', originalTexture); material.update(); // rotate the statue diff --git a/examples/src/examples/graphics/shader-toon.shader.frag b/examples/src/examples/graphics/shader-toon.shader.frag index 02a2951c960..c7b505e7038 100644 --- a/examples/src/examples/graphics/shader-toon.shader.frag +++ b/examples/src/examples/graphics/shader-toon.shader.frag @@ -1,13 +1,12 @@ precision mediump float; -uniform sampler2D uTexture; varying float vertOutTexCoord; varying vec2 texCoord; void main(void) { float v = vertOutTexCoord; v = float(int(v * 6.0)) / 6.0; - // vec4 color = texture2D (uTexture, texCoord); // try this to use the diffuse color. - vec4 color = vec4(0.5, 0.47, 0.43, 1.0); - gl_FragColor = color * vec4(v, v, v, 1.0); + vec3 linearColor = vec3(0.218, 0.190, 0.156) * v; + gl_FragColor.rgb = gammaCorrectOutput(linearColor.rgb); + gl_FragColor.a = 1.0; } diff --git a/examples/src/examples/graphics/shader-wobble.shader.frag b/examples/src/examples/graphics/shader-wobble.shader.frag index b2c80636a3b..c75ade018ac 100644 --- a/examples/src/examples/graphics/shader-wobble.shader.frag +++ b/examples/src/examples/graphics/shader-wobble.shader.frag @@ -5,10 +5,6 @@ uniform sampler2D uDiffuseMap; varying vec2 vUv0; -vec3 gammaCorrectOutput(vec3 color) { - return pow(color + 0.0000001, vec3(1.0 / 2.2)); -} - void main(void) { vec4 linearColor = texture2D(uDiffuseMap, vUv0); diff --git a/examples/src/examples/graphics/texture-array.example.mjs b/examples/src/examples/graphics/texture-array.example.mjs index eeb359d189c..4d0e2729334 100644 --- a/examples/src/examples/graphics/texture-array.example.mjs +++ b/examples/src/examples/graphics/texture-array.example.mjs @@ -46,18 +46,10 @@ function generateMipmaps(width, height) { } const assets = { - rockyTrail: new pc.Asset('rockyTrail', 'texture', { - url: rootPath + '/static/assets/textures/rocky_trail_diff_1k.jpg' - }), - rockBoulder: new pc.Asset('rockBoulder', 'texture', { - url: rootPath + '/static/assets/textures/rock_boulder_cracked_diff_1k.jpg' - }), - coastSand: new pc.Asset('coastSand', 'texture', { - url: rootPath + '/static/assets/textures/coast_sand_rocks_02_diff_1k.jpg' - }), - aerialRocks: new pc.Asset('aeralRocks', 'texture', { - url: rootPath + '/static/assets/textures/aerial_rocks_02_diff_1k.jpg' - }), + rockyTrail: new pc.Asset('rockyTrail', 'texture', { url: rootPath + '/static/assets/textures/rocky_trail_diff_1k.jpg' }, { srgb: true }), + rockBoulder: new pc.Asset('rockBoulder', 'texture', { url: rootPath + '/static/assets/textures/rock_boulder_cracked_diff_1k.jpg' }, { srgb: true }), + coastSand: new pc.Asset('coastSand', 'texture', { url: rootPath + '/static/assets/textures/coast_sand_rocks_02_diff_1k.jpg' }, { srgb: true }), + aerialRocks: new pc.Asset('aeralRocks', 'texture', { url: rootPath + '/static/assets/textures/aerial_rocks_02_diff_1k.jpg' }, { srgb: true }), script: new pc.Asset('script', 'script', { url: rootPath + '/static/scripts/camera/orbit-camera.js' }) }; @@ -112,7 +104,7 @@ assetListLoader.load(() => { const textureArrayOptions = { name: 'textureArrayImages', - format: pc.PIXELFORMAT_RGBA8, + format: pc.PIXELFORMAT_SRGBA8, width: 1024, height: 1024, arrayLength: 4, // array texture with 4 textures diff --git a/examples/src/examples/graphics/texture-array.ground.frag b/examples/src/examples/graphics/texture-array.ground.frag index 2bb687f0717..ab6c35dca1a 100644 --- a/examples/src/examples/graphics/texture-array.ground.frag +++ b/examples/src/examples/graphics/texture-array.ground.frag @@ -7,5 +7,5 @@ void main(void) { vec4 data = texture(uDiffuseMap, vec3(vUv0, step(vUv0.x, 0.5) + 2.0 * step(vUv0.y, 0.5))); data.rgb *= 0.8 * max(dot(worldNormal, vec3(0.1, 1.0, 0.5)), 0.0) + 0.5; // simple lighting - gl_FragColor = vec4(data.rgb, 1.0); + gl_FragColor = vec4(gammaCorrectOutput(data.rgb), 1.0); } diff --git a/examples/src/examples/graphics/texture-array.shader.frag b/examples/src/examples/graphics/texture-array.shader.frag index d6e97fd743e..a87daf26e6c 100644 --- a/examples/src/examples/graphics/texture-array.shader.frag +++ b/examples/src/examples/graphics/texture-array.shader.frag @@ -11,5 +11,5 @@ void main(void) vec4 data = texture(uDiffuseMap, vec3(vUv0, floor(index))); data.rgb *= 0.8 * max(dot(worldNormal, vec3(0.1, 1.0, 0.5)), 0.0) + 0.5; // simple lighting - gl_FragColor = vec4(data.rgb, 1.0); + gl_FragColor = vec4(gammaCorrectOutput(data.rgb), 1.0); } diff --git a/src/extras/gizmo/axis-shapes.js b/src/extras/gizmo/axis-shapes.js index 88df26998b7..e9733e4adc7 100644 --- a/src/extras/gizmo/axis-shapes.js +++ b/src/extras/gizmo/axis-shapes.js @@ -36,8 +36,13 @@ const GEOMETRIES = { torus: TorusGeometry }; -const SHADER = { - vert: /* glsl */` +const shaderDesc = { + uniqueName: 'axis-shape', + attributes: { + vertex_position: SEMANTIC_POSITION, + vertex_color: SEMANTIC_COLOR + }, + vertexCode: /* glsl */` attribute vec3 vertex_position; attribute vec4 vertex_color; varying vec4 vColor; @@ -51,16 +56,18 @@ const SHADER = { vZW = gl_Position.zw; // disable depth clipping // gl_Position.z = 0.0; - }`, - frag: /* glsl */` + } + `, + fragmentCode: /* glsl */` precision highp float; varying vec4 vColor; varying vec2 vZW; void main(void) { - gl_FragColor = vColor; + gl_FragColor = vec4(gammaCorrectOutput(decodeGamma(vColor)), vColor.w); // clamp depth in Z to [0, 1] range gl_FragDepth = max(0.0, min(1.0, (vZW.x / vZW.y + 1.0) * 0.5)); - }` + } + ` }; // temporary variables @@ -200,15 +207,7 @@ class AxisShape { } _addRenderMeshes(entity, meshes) { - const material = new ShaderMaterial({ - uniqueName: 'axis-shape', - vertexCode: SHADER.vert, - fragmentCode: SHADER.frag, - attributes: { - vertex_position: SEMANTIC_POSITION, - vertex_color: SEMANTIC_COLOR - } - }); + const material = new ShaderMaterial(shaderDesc); material.cull = this._cull; material.blendType = BLEND_NORMAL; material.update(); diff --git a/src/extras/render-passes/render-pass-compose.js b/src/extras/render-passes/render-pass-compose.js index 64a064893ae..8031adf2241 100644 --- a/src/extras/render-passes/render-pass-compose.js +++ b/src/extras/render-passes/render-pass-compose.js @@ -156,7 +156,9 @@ const fragmentShader = /* glsl */ ` result *= vignette(uv); #endif - result = gammaCorrectOutput(result); + #ifdef GAMMA_CORRECT_OUTPUT + result = gammaCorrectOutput(result); + #endif gl_FragColor = vec4(result, scene.a); } @@ -207,6 +209,8 @@ class RenderPassCompose extends RenderPassShaderQuad { _sharpness = 0.5; + _srgb = false; + _key = ''; constructor(graphicsDevice) { @@ -326,6 +330,15 @@ class RenderPassCompose extends RenderPassShaderQuad { frameUpdate() { + // detect if the render target is srgb vs execute manual srgb conversion + const rt = this.renderTarget ?? this.device.backBuffer; + const srgb = rt.isColorBufferSrgb(0); + if (this._srgb !== srgb) { + this._srgb = srgb; + this._shaderDirty = true; + } + + // need to rebuild shader if (this._shaderDirty) { this._shaderDirty = false; @@ -336,7 +349,8 @@ class RenderPassCompose extends RenderPassShaderQuad { `-${this.vignetteEnabled ? 'vignette' : 'novignette'}` + `-${this.fringingEnabled ? 'fringing' : 'nofringing'}` + `-${this.taaEnabled ? 'taa' : 'notaa'}` + - `-${this.isSharpnessEnabled ? 'cas' : 'nocas'}`; + `-${this.isSharpnessEnabled ? 'cas' : 'nocas'}` + + `-${this._srgb ? 'srgb' : 'linear'}`; if (this._key !== key) { this._key = key; @@ -348,7 +362,8 @@ class RenderPassCompose extends RenderPassShaderQuad { (this.vignetteEnabled ? `#define VIGNETTE\n` : '') + (this.fringingEnabled ? `#define FRINGING\n` : '') + (this.taaEnabled ? `#define TAA\n` : '') + - (this.isSharpnessEnabled ? `#define CAS\n` : ''); + (this.isSharpnessEnabled ? `#define CAS\n` : '') + + (this._srgb ? `` : '#define GAMMA_CORRECT_OUTPUT\n'); const fsChunks = shaderChunks.decodePS + diff --git a/src/framework/components/camera/post-effect-queue.js b/src/framework/components/camera/post-effect-queue.js index 21c334f9c16..ebf9c311b84 100644 --- a/src/framework/components/camera/post-effect-queue.js +++ b/src/framework/components/camera/post-effect-queue.js @@ -1,4 +1,4 @@ -import { ADDRESS_CLAMP_TO_EDGE, FILTER_NEAREST, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F, PIXELFORMAT_RGBA8 } from '../../../platform/graphics/constants.js'; +import { ADDRESS_CLAMP_TO_EDGE, FILTER_NEAREST, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F, PIXELFORMAT_RGBA8, PIXELFORMAT_SRGBA8 } from '../../../platform/graphics/constants.js'; import { DebugGraphics } from '../../../platform/graphics/debug-graphics.js'; import { RenderTarget } from '../../../platform/graphics/render-target.js'; import { Texture } from '../../../platform/graphics/texture.js'; @@ -109,7 +109,13 @@ class PostEffectQueue { _createOffscreenTarget(useDepth, hdr) { const device = this.app.graphicsDevice; - const format = hdr && device.getRenderableHdrFormat([PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F], true) || PIXELFORMAT_RGBA8; + + // use srgb LDR format if backbuffer is srgb + const outputRt = this.destinationRenderTarget ?? device.backBuffer; + const srgb = outputRt.isColorBufferSrgb(0); + + const format = (hdr && device.getRenderableHdrFormat([PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F], true)) ?? + (srgb ? PIXELFORMAT_SRGBA8 : PIXELFORMAT_RGBA8); const name = this.camera.entity.name + '-posteffect-' + this.effects.length; const colorBuffer = this._allocateColorBuffer(format, name); @@ -339,7 +345,7 @@ class PostEffectQueue { this._destroyOffscreenTarget(this._sourceTarget); - this.camera.renderTarget = null; + this.camera.renderTarget = this.destinationRenderTarget; this.camera.onPostprocessing = null; } } diff --git a/src/platform/graphics/constants.js b/src/platform/graphics/constants.js index b7573479215..2746bf2a61a 100644 --- a/src/platform/graphics/constants.js +++ b/src/platform/graphics/constants.js @@ -762,7 +762,8 @@ export const PIXELFORMAT_ATC_RGB = 29; export const PIXELFORMAT_ATC_RGBA = 30; /** - * 32-bit BGRA (8-bits for blue channel, 8 for green, 8 for red with 8-bit alpha). + * 32-bit BGRA (8-bits for blue channel, 8 for green, 8 for red with 8-bit alpha). This is an + * internal format used by the WebGPU's backbuffer only. * * @type {number} * @ignore @@ -1026,6 +1027,15 @@ export const PIXELFORMAT_ETC2_SRGBA = 62; */ export const PIXELFORMAT_ASTC_4x4_SRGB = 63; +/** + * 32-bit BGRA sRGB format. This is an internal format used by the WebGPU's backbuffer only. + * + * @type {number} + * @ignore + * @category Graphics + */ +export const PIXELFORMAT_SBGRA8 = 64; + /** * Information about pixel formats. * @@ -1062,6 +1072,7 @@ export const pixelFormatInfo = new Map([ [PIXELFORMAT_SRGB8, { name: 'SRGB8', size: 4, ldr: true, srgb: true }], [PIXELFORMAT_SRGBA8, { name: 'SRGBA8', size: 4, ldr: true, srgb: true }], [PIXELFORMAT_BGRA8, { name: 'BGRA8', size: 4, ldr: true }], + [PIXELFORMAT_SBGRA8, { name: 'SBGRA8', size: 4, ldr: true, srgb: true }], // compressed formats [PIXELFORMAT_DXT1, { name: 'DXT1', blockSize: 8, ldr: true, srgbFormat: PIXELFORMAT_DXT1_SRGB }], @@ -2181,6 +2192,37 @@ export const SHADERSTAGE_FRAGMENT = 2; */ export const SHADERSTAGE_COMPUTE = 4; +/** + * Display format for low dynamic range data. This is always supported; however, due to the cost, it + * does not implement linear alpha blending on the main framebuffer. Instead, alpha blending occurs + * in sRGB space. + * + * @type {string} + * @category Graphics + */ +export const DISPLAYFORMAT_LDR = 'ldr'; + +/** + * Display format for low dynamic range data in the sRGB color space. This format correctly + * implements linear alpha blending on the main framebuffer, with the alpha blending occurring in + * linear space. This is currently supported on WebGPU platform only. On unsupported platforms, it + * silently falls back to {@link DISPLAYFORMAT_LDR}. + * + * @type {string} + * @category Graphics + */ +export const DISPLAYFORMAT_LDR_SRGB = 'ldr_srgb'; + +/** + * Display format for high dynamic range data, using 16bit floating point values. + * Note: This is not implemented yet, but is added to indicate the intended API. + * + * @type {string} + * @category Graphics + * @ignore + */ +export const DISPLAYFORMAT_HDR = 'hdr'; + // indices of commonly used bind groups, sorted from the least commonly changing to avoid internal rebinding export const BINDGROUP_VIEW = 0; // view bind group, textures, samplers and uniforms export const BINDGROUP_MESH = 1; // mesh bind group - textures and samplers diff --git a/src/platform/graphics/graphics-device-create.js b/src/platform/graphics/graphics-device-create.js index 77cbe9e2764..f07172083c0 100644 --- a/src/platform/graphics/graphics-device-create.js +++ b/src/platform/graphics/graphics-device-create.js @@ -16,6 +16,12 @@ import { NullGraphicsDevice } from './null/null-graphics-device.js'; * Typically, you'd only specify {@link DEVICETYPE_WEBGPU}, or leave it empty. * @param {boolean} [options.antialias] - Boolean that indicates whether or not to perform * anti-aliasing if possible. Defaults to true. + * @param {string} [options.displayFormat] - The display format of the canvas. Defaults to + * {@link DISPLAYFORMAT_LDR}. Can be: + * + * - {@link DISPLAYFORMAT_LDR} + * - {@link DISPLAYFORMAT_LDR_SRGB} + * * @param {boolean} [options.depth] - Boolean that indicates that the drawing buffer is * requested to have a depth buffer of at least 16 bits. Defaults to true. * @param {boolean} [options.stencil] - Boolean that indicates that the drawing buffer is diff --git a/src/platform/graphics/graphics-device.js b/src/platform/graphics/graphics-device.js index b01a0160d0d..4475694dc00 100644 --- a/src/platform/graphics/graphics-device.js +++ b/src/platform/graphics/graphics-device.js @@ -9,7 +9,8 @@ import { TRACEID_TEXTURES } from '../../core/constants.js'; import { CULLFACE_BACK, CLEARFLAG_COLOR, CLEARFLAG_DEPTH, - PRIMITIVE_POINTS, PRIMITIVE_TRIFAN, SEMANTIC_POSITION, TYPE_FLOAT32, PIXELFORMAT_111110F, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F + PRIMITIVE_POINTS, PRIMITIVE_TRIFAN, SEMANTIC_POSITION, TYPE_FLOAT32, PIXELFORMAT_111110F, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F, + DISPLAYFORMAT_LDR } from './constants.js'; import { BlendState } from './blend-state.js'; import { DepthState } from './depth-state.js'; @@ -376,10 +377,12 @@ class GraphicsDevice extends EventHandler { // copy options and handle defaults this.initOptions = { ...options }; + this.initOptions.alpha ??= true; this.initOptions.depth ??= true; this.initOptions.stencil ??= true; this.initOptions.antialias ??= true; this.initOptions.powerPreference ??= 'high-performance'; + this.initOptions.displayFormat ??= DISPLAYFORMAT_LDR; // Some devices window.devicePixelRatio can be less than one // eg Oculus Quest 1 which returns a window.devicePixelRatio of 0.8 diff --git a/src/platform/graphics/render-pass.js b/src/platform/graphics/render-pass.js index 1505c580a9f..8d422465262 100644 --- a/src/platform/graphics/render-pass.js +++ b/src/platform/graphics/render-pass.js @@ -11,10 +11,16 @@ import { TRACEID_RENDER_PASS, TRACEID_RENDER_PASS_DETAIL } from '../../core/cons class ColorAttachmentOps { /** - * A color used to clear the color attachment when the clear is enabled. + * A color used to clear the color attachment when the clear is enabled, specified in sRGB space. */ clearValue = new Color(0, 0, 0, 1); + /** + * A color used to clear the color attachment when the clear is enabled, specified in linear + * space. + */ + clearValueLinear = new Color(0, 0, 0, 1); + /** * True if the attachment should be cleared before rendering, false to preserve * the existing content. @@ -332,8 +338,10 @@ class RenderPass { const count = this.colorArrayOps.length; for (let i = 0; i < count; i++) { const colorOps = this.colorArrayOps[i]; - if (color) + if (color) { colorOps.clearValue.copy(color); + colorOps.clearValueLinear.linear(color); + } colorOps.clear = !!color; } } diff --git a/src/platform/graphics/render-target.js b/src/platform/graphics/render-target.js index 2b4a92bfe24..093fc4981e4 100644 --- a/src/platform/graphics/render-target.js +++ b/src/platform/graphics/render-target.js @@ -487,13 +487,18 @@ class RenderTarget { } /** - * Gets whether the first color buffer format is sRGB. + * Gets whether the format of the specified color buffer is sRGB. * - * @type {boolean} + * @param {number} index - The index of the color buffer. + * @returns {boolean} True if the color buffer is sRGB, false otherwise. * @ignore */ - get srgb() { - return this.colorBuffer ? isSrgbPixelFormat(this.colorBuffer.format) : false; + isColorBufferSrgb(index = 0) { + if (this.device.backBuffer === this) + return isSrgbPixelFormat(this.device.backBufferFormat); + + const colorBuffer = this.getColorBuffer(index); + return colorBuffer ? isSrgbPixelFormat(colorBuffer.format) : false; } } diff --git a/src/platform/graphics/webgl/webgl-texture.js b/src/platform/graphics/webgl/webgl-texture.js index 1217368f17e..703aac50bf2 100644 --- a/src/platform/graphics/webgl/webgl-texture.js +++ b/src/platform/graphics/webgl/webgl-texture.js @@ -12,7 +12,7 @@ import { PIXELFORMAT_RGBA8I, PIXELFORMAT_RGBA8U, PIXELFORMAT_R16F, PIXELFORMAT_RG16F, PIXELFORMAT_R8, PIXELFORMAT_RG8, PIXELFORMAT_DXT1_SRGB, PIXELFORMAT_DXT3_SRGB, PIXELFORMAT_DXT5_SRGB, PIXELFORMAT_PVRTC_2BPP_SRGB_1, PIXELFORMAT_PVRTC_2BPP_SRGBA_1, PIXELFORMAT_PVRTC_4BPP_SRGB_1, PIXELFORMAT_PVRTC_4BPP_SRGBA_1, - PIXELFORMAT_ETC2_SRGB, PIXELFORMAT_ETC2_SRGBA, PIXELFORMAT_ASTC_4x4_SRGB + PIXELFORMAT_ETC2_SRGB, PIXELFORMAT_ETC2_SRGBA, PIXELFORMAT_ASTC_4x4_SRGB, PIXELFORMAT_SBGRA8 } from '../constants.js'; /** @@ -164,6 +164,10 @@ class WebglTexture { this._glInternalFormat = gl.RGBA8; this._glPixelType = gl.UNSIGNED_BYTE; break; + case PIXELFORMAT_BGRA8: + case PIXELFORMAT_SBGRA8: + Debug.error("BGRA8 and SBGRA8 texture formats are not supported by WebGL."); + break; // compressed formats ---- @@ -421,9 +425,6 @@ class WebglTexture { this._glInternalFormat = gl.RGBA32UI; this._glPixelType = gl.UNSIGNED_INT; break; - case PIXELFORMAT_BGRA8: - Debug.error("BGRA8 texture format is not supported by WebGL."); - break; } this._glCreated = false; diff --git a/src/platform/graphics/webgpu/constants.js b/src/platform/graphics/webgpu/constants.js index 774642e8b3d..dc427e73c5c 100644 --- a/src/platform/graphics/webgpu/constants.js +++ b/src/platform/graphics/webgpu/constants.js @@ -11,7 +11,7 @@ import { PIXELFORMAT_RGBA8I, PIXELFORMAT_RGBA8U, PIXELFORMAT_R16F, PIXELFORMAT_RG16F, PIXELFORMAT_R8, PIXELFORMAT_RG8, PIXELFORMAT_DXT1_SRGB, PIXELFORMAT_DXT3_SRGB, PIXELFORMAT_DXT5_SRGB, PIXELFORMAT_PVRTC_2BPP_SRGB_1, PIXELFORMAT_PVRTC_2BPP_SRGBA_1, PIXELFORMAT_PVRTC_4BPP_SRGB_1, PIXELFORMAT_PVRTC_4BPP_SRGBA_1, - PIXELFORMAT_ETC2_SRGB, PIXELFORMAT_ETC2_SRGBA + PIXELFORMAT_ETC2_SRGB, PIXELFORMAT_ETC2_SRGBA, PIXELFORMAT_SBGRA8 } from '../constants.js'; // map of PIXELFORMAT_*** to GPUTextureFormat @@ -52,6 +52,7 @@ gpuTextureFormats[PIXELFORMAT_ASTC_4x4] = 'astc-4x4-unorm'; gpuTextureFormats[PIXELFORMAT_ATC_RGB] = ''; gpuTextureFormats[PIXELFORMAT_ATC_RGBA] = ''; gpuTextureFormats[PIXELFORMAT_BGRA8] = 'bgra8unorm'; +gpuTextureFormats[PIXELFORMAT_SBGRA8] = 'bgra8unorm-srgb'; gpuTextureFormats[PIXELFORMAT_R8I] = 'r8sint'; gpuTextureFormats[PIXELFORMAT_R8U] = 'r8uint'; gpuTextureFormats[PIXELFORMAT_R16I] = 'r16sint'; diff --git a/src/platform/graphics/webgpu/webgpu-graphics-device.js b/src/platform/graphics/webgpu/webgpu-graphics-device.js index 54f15be327e..b77779f8cfa 100644 --- a/src/platform/graphics/webgpu/webgpu-graphics-device.js +++ b/src/platform/graphics/webgpu/webgpu-graphics-device.js @@ -2,7 +2,8 @@ import { TRACEID_RENDER_QUEUE } from '../../../core/constants.js'; import { Debug, DebugHelper } from '../../../core/debug.js'; import { PIXELFORMAT_RGBA8, PIXELFORMAT_BGRA8, DEVICETYPE_WEBGPU, - BUFFERUSAGE_READ, BUFFERUSAGE_COPY_DST, semanticToLocation + BUFFERUSAGE_READ, BUFFERUSAGE_COPY_DST, semanticToLocation, + PIXELFORMAT_SRGBA8, DISPLAYFORMAT_LDR_SRGB, PIXELFORMAT_SBGRA8 } from '../constants.js'; import { BindGroupFormat } from '../bind-group-format.js'; import { BindGroup } from '../bind-group.js'; @@ -267,9 +268,20 @@ class WebgpuGraphicsDevice extends GraphicsDevice { this.gpuContext = this.canvas.getContext('webgpu'); - // pixel format of the framebuffer is the most efficient one on the system + // pixel format of the framebuffer that is the most efficient one on the system const preferredCanvasFormat = navigator.gpu.getPreferredCanvasFormat(); - this.backBufferFormat = preferredCanvasFormat === 'rgba8unorm' ? PIXELFORMAT_RGBA8 : PIXELFORMAT_BGRA8; + + // display format the user asked for + const displayFormat = this.initOptions.displayFormat; + + // combine requested display format with the preferred format + this.backBufferFormat = preferredCanvasFormat === 'rgba8unorm' ? + (displayFormat === DISPLAYFORMAT_LDR_SRGB ? PIXELFORMAT_SRGBA8 : PIXELFORMAT_RGBA8) : // (S)RGBA + (displayFormat === DISPLAYFORMAT_LDR_SRGB ? PIXELFORMAT_SBGRA8 : PIXELFORMAT_BGRA8); // (S)BGRA + + // view format for the backbuffer. Backbuffer is always allocated without srgb conversion, and + // the view we create specifies srgb is needed to handle the conversion. + this.backBufferViewFormat = displayFormat === DISPLAYFORMAT_LDR_SRGB ? `${preferredCanvasFormat}-srgb` : preferredCanvasFormat; /** * Configuration of the main colorframebuffer we obtain using getCurrentTexture @@ -289,7 +301,8 @@ class WebgpuGraphicsDevice extends GraphicsDevice { usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST, // formats that views created from textures returned by getCurrentTexture may use - viewFormats: [] + // (this allows us to view the preferred format as srgb) + viewFormats: displayFormat === DISPLAYFORMAT_LDR_SRGB ? [this.backBufferViewFormat] : [] }; this.gpuContext.configure(this.canvasConfig); @@ -329,6 +342,7 @@ class WebgpuGraphicsDevice extends GraphicsDevice { stencil: this.supportsStencil, samples: this.samples }); + this.backBuffer.impl.isBackbuffer = true; } frameStart() { @@ -361,12 +375,12 @@ class WebgpuGraphicsDevice extends GraphicsDevice { const wrt = rt.impl; // assign the format, allowing following init call to use it to allocate matching multisampled buffer - wrt.setColorAttachment(0, undefined, outColorBuffer.format); + wrt.setColorAttachment(0, undefined, this.backBufferViewFormat); this.initRenderTarget(rt); // assign current frame's render texture - wrt.assignColorTexture(outColorBuffer); + wrt.assignColorTexture(this, outColorBuffer); WebgpuDebug.end(this); WebgpuDebug.end(this); @@ -643,7 +657,7 @@ class WebgpuGraphicsDevice extends GraphicsDevice { } // set up clear / store / load settings - wrt.setupForRenderPass(renderPass); + wrt.setupForRenderPass(renderPass, rt); const renderPassDesc = wrt.renderPassDescriptor; diff --git a/src/platform/graphics/webgpu/webgpu-render-target.js b/src/platform/graphics/webgpu/webgpu-render-target.js index 5e95c30250d..73f3d880105 100644 --- a/src/platform/graphics/webgpu/webgpu-render-target.js +++ b/src/platform/graphics/webgpu/webgpu-render-target.js @@ -88,6 +88,13 @@ class WebgpuRenderTarget { */ renderPassDescriptor = {}; + /** + * True if this is the backbuffer of the device. + * + * @type {boolean} + */ + isBackbuffer = false; + /** * @param {RenderTarget} renderTarget - The render target owning this implementation. */ @@ -149,14 +156,17 @@ class WebgpuRenderTarget { * Assign a color buffer. This allows the color buffer of the main framebuffer * to be swapped each frame to a buffer provided by the context. * + * @param {import('./webgpu-graphics-device.js').WebgpuGraphicsDevice} device - The WebGPU + * graphics device. * @param {any} gpuTexture - The color buffer. */ - assignColorTexture(gpuTexture) { + assignColorTexture(device, gpuTexture) { Debug.assert(gpuTexture); this.assignedColorTexture = gpuTexture; - const view = gpuTexture.createView(); + // create view (optionally handles srgb conversion) + const view = gpuTexture.createView({ format: device.backBufferViewFormat }); DebugHelper.setLabel(view, 'Framebuffer.assignedColor'); // use it as render buffer or resolve target @@ -169,7 +179,8 @@ class WebgpuRenderTarget { } // for main framebuffer, this is how the format is obtained - this.setColorAttachment(0, undefined, gpuTexture.format); + this.setColorAttachment(0, undefined, device.backBufferViewFormat); + this.updateKey(); } @@ -208,7 +219,7 @@ class WebgpuRenderTarget { this.renderPassDescriptor.colorAttachments = []; const count = renderTarget._colorBuffers?.length ?? 1; for (let i = 0; i < count; ++i) { - const colorAttachment = this.initColor(wgpu, renderTarget, i); + const colorAttachment = this.initColor(device, wgpu, renderTarget, i); // default framebuffer, buffer gets assigned later const isDefaultFramebuffer = i === 0 && this.colorAttachments[0]?.format; @@ -280,13 +291,14 @@ class WebgpuRenderTarget { } /** + * @param {WebgpuGraphicsDevice} device - The graphics device. * @param {GPUDevice} wgpu - The WebGPU device. * @param {RenderTarget} renderTarget - The render target. * @param {number} index - The color buffer index. * @returns {GPURenderPassColorAttachment} The color attachment. * @private */ - initColor(wgpu, renderTarget, index) { + initColor(device, wgpu, renderTarget, index) { // Single-sampled color buffer gets passed in: // - for normal render target, constructor takes the color buffer as an option // - for the main framebuffer, the device supplies the buffer each frame @@ -322,12 +334,14 @@ class WebgpuRenderTarget { // multi-sampled color buffer if (samples > 1) { + const format = this.isBackbuffer ? device.backBufferViewFormat : colorBuffer.impl.format; + /** @type {GPUTextureDescriptor} */ const multisampledTextureDesc = { size: [width, height, 1], dimension: '2d', sampleCount: samples, - format: this.colorAttachments[index]?.format ?? colorBuffer.impl.format, + format: format, usage: GPUTextureUsage.RENDER_ATTACHMENT }; @@ -353,8 +367,9 @@ class WebgpuRenderTarget { * Update WebGPU render pass descriptor by RenderPass settings. * * @param {RenderPass} renderPass - The render pass to start. + * @param {RenderTarget} renderTarget - The render target to render to. */ - setupForRenderPass(renderPass) { + setupForRenderPass(renderPass, renderTarget) { Debug.assert(this.renderPassDescriptor); @@ -362,7 +377,8 @@ class WebgpuRenderTarget { for (let i = 0; i < count; ++i) { const colorAttachment = this.renderPassDescriptor.colorAttachments[i]; const colorOps = renderPass.colorArrayOps[i]; - colorAttachment.clearValue = colorOps.clearValue; + const srgb = renderTarget.isColorBufferSrgb(i); + colorAttachment.clearValue = srgb ? colorOps.clearValueLinear : colorOps.clearValue; colorAttachment.loadOp = colorOps.clear ? 'clear' : 'load'; colorAttachment.storeOp = colorOps.store ? 'store' : 'discard'; } diff --git a/src/scene/immediate/immediate.js b/src/scene/immediate/immediate.js index 429a90ea620..feb3aed69bf 100644 --- a/src/scene/immediate/immediate.js +++ b/src/scene/immediate/immediate.js @@ -30,7 +30,7 @@ const lineShaderDesc = { fragmentCode: /* glsl */ ` varying vec4 color; void main(void) { - gl_FragColor = vec4(gammaCorrectOutput(gammaCorrectInput(color.rgb)), color.a); + gl_FragColor = vec4(gammaCorrectOutput(decodeGamma(color.rgb)), color.a); } `, attributes: { diff --git a/src/scene/particle-system/particle-emitter.js b/src/scene/particle-system/particle-emitter.js index ecfcf5545cc..5c0518b9132 100644 --- a/src/scene/particle-system/particle-emitter.js +++ b/src/scene/particle-system/particle-emitter.js @@ -18,7 +18,9 @@ import { PRIMITIVE_TRIANGLES, SEMANTIC_ATTR0, SEMANTIC_ATTR1, SEMANTIC_ATTR2, SEMANTIC_ATTR3, SEMANTIC_ATTR4, SEMANTIC_TEXCOORD0, TYPE_FLOAT32, - typedArrayIndexFormats + typedArrayIndexFormats, + requiresManualGamma, + PIXELFORMAT_SRGBA8 } from '../../platform/graphics/constants.js'; import { DeviceCache } from '../../platform/graphics/device-cache.js'; import { IndexBuffer } from '../../platform/graphics/index-buffer.js'; @@ -52,7 +54,7 @@ const particleVerts = [ function _createTexture(device, width, height, pixelData, format = PIXELFORMAT_RGBA32F, mult8Bit, filter) { let mipFilter = FILTER_NEAREST; - if (filter && format === PIXELFORMAT_RGBA8) + if (filter && (format === PIXELFORMAT_RGBA8 || format === PIXELFORMAT_SRGBA8)) mipFilter = FILTER_LINEAR; const texture = new Texture(device, { @@ -70,7 +72,7 @@ function _createTexture(device, width, height, pixelData, format = PIXELFORMAT_R const pixels = texture.lock(); - if (format === PIXELFORMAT_RGBA8) { + if (format === PIXELFORMAT_RGBA8 || format === PIXELFORMAT_SRGBA8) { const temp = new Uint8Array(pixelData.length); for (let i = 0; i < pixelData.length; i++) { temp[i] = pixelData[i] * mult8Bit * 255; @@ -400,7 +402,7 @@ class ParticleEmitter { } } - const texture = _createTexture(this.graphicsDevice, resolution, resolution, dtex, PIXELFORMAT_RGBA8, 1.0, true); + const texture = _createTexture(this.graphicsDevice, resolution, resolution, dtex, PIXELFORMAT_SRGBA8, 1.0, true); texture.minFilter = FILTER_LINEAR; texture.magFilter = FILTER_LINEAR; return texture; @@ -698,7 +700,7 @@ class ParticleEmitter { this.meshInstance._updateAabb = false; this.meshInstance.visible = wasVisible; - this._initializeTextures(); + this._setMaterialTextures(); this.resetTime(); @@ -791,11 +793,18 @@ class ParticleEmitter { this.internalTex2 = _createTexture(gd, precision, 1, packTexture5Floats(this.qRotSpeed, this.qScale, this.qScaleDiv, this.qRotSpeedDiv, this.qAlphaDiv)); this.internalTex3 = _createTexture(gd, precision, 1, packTexture2Floats(this.qRadialSpeed, this.qRadialSpeedDiv)); } - this.colorParam = _createTexture(gd, precision, 1, packTextureRGBA(this.qColor, this.qAlpha), PIXELFORMAT_RGBA8, 1.0, true); + this.colorParam = _createTexture(gd, precision, 1, packTextureRGBA(this.qColor, this.qAlpha), PIXELFORMAT_SRGBA8, 1.0, true); } - _initializeTextures() { + _setMaterialTextures() { if (this.colorMap) { + + Debug.call(() => { + if (requiresManualGamma(this.colorMap.format)) { + Debug.warnOnce(`ParticleEmitter: colorMap texture [${this.colorMap.name}] is not using sRGB format. Please correct it for the correct rendering.`, this.colorMap); + } + }); + this.material.setParameter('colorMap', this.colorMap); if (this.lighting && this.normalMap) { this.material.setParameter('normalMap', this.normalMap); @@ -860,15 +869,8 @@ class ParticleEmitter { material.setParameter('wrapBounds', this.wrapBoundsUniform); } - if (this.colorMap) { - material.setParameter('colorMap', this.colorMap); - } + this._setMaterialTextures(); - if (this.lighting) { - if (this.normalMap) { - material.setParameter('normalMap', this.normalMap); - } - } if (this.depthSoftening > 0) { material.setParameter('softening', 1.0 / (this.depthSoftening * this.depthSoftening * 100)); // remap to more perceptually linear } @@ -1036,7 +1038,7 @@ class ParticleEmitter { this.particleTex[i] = this.particleTexStart[i]; } } else { - this._initializeTextures(); + this._setMaterialTextures(); } this.resetWorldBounds(); this.resetTime(); diff --git a/src/scene/renderer/forward-renderer.js b/src/scene/renderer/forward-renderer.js index 7d7c9a24713..550577e02e3 100644 --- a/src/scene/renderer/forward-renderer.js +++ b/src/scene/renderer/forward-renderer.js @@ -475,7 +475,7 @@ class ForwardRenderer extends Renderer { const renderParams = camera.renderingParams ?? this.scene.rendering; // output gamma correction is determined by the render target - renderParams.srgbRenderTarget = renderTarget?.srgb ?? false; + renderParams.srgbRenderTarget = renderTarget?.isColorBufferSrgb(0) ?? false; const addCall = (drawCall, shaderInstance, isNewMaterial, lightMaskChanged) => { _drawCallList.drawCalls.push(drawCall); diff --git a/src/scene/renderer/render-pass-forward.js b/src/scene/renderer/render-pass-forward.js index eab9c93bd1d..20108ca6b7b 100644 --- a/src/scene/renderer/render-pass-forward.js +++ b/src/scene/renderer/render-pass-forward.js @@ -282,7 +282,8 @@ class RenderPassForward extends RenderPass { options.clearStencil = renderAction.clearStencil; } - renderer.renderForwardLayer(camera.camera, renderAction.renderTarget, layer, transparent, + const renderTarget = renderAction.renderTarget ?? device.backBuffer; + renderer.renderForwardLayer(camera.camera, renderTarget, layer, transparent, shaderPass, renderAction.viewBindGroups, options); // Revert temp frame stuff diff --git a/src/scene/shader-lib/chunks/particle/frag/particle.js b/src/scene/shader-lib/chunks/particle/frag/particle.js index 04293319b53..809b1b71beb 100644 --- a/src/scene/shader-lib/chunks/particle/frag/particle.js +++ b/src/scene/shader-lib/chunks/particle/frag/particle.js @@ -28,8 +28,8 @@ float unpackFloat(vec4 rgbaDepth) { #endif void main(void) { - vec4 tex = gammaCorrectInput(texture2D(colorMap, vec2(texCoordsAlphaLife.x, 1.0 - texCoordsAlphaLife.y))); - vec4 ramp = gammaCorrectInput(texture2D(colorParam, vec2(texCoordsAlphaLife.w, 0.0))); + vec4 tex = texture2D(colorMap, vec2(texCoordsAlphaLife.x, 1.0 - texCoordsAlphaLife.y)); + vec4 ramp = texture2D(colorParam, vec2(texCoordsAlphaLife.w, 0.0)); ramp.rgb *= colorMult; ramp.a += texCoordsAlphaLife.z; diff --git a/src/scene/shader-lib/chunks/standard/frag/opacity-dither.js b/src/scene/shader-lib/chunks/standard/frag/opacity-dither.js index 8796a91b164..e01ee5c815e 100644 --- a/src/scene/shader-lib/chunks/standard/frag/opacity-dither.js +++ b/src/scene/shader-lib/chunks/standard/frag/opacity-dither.js @@ -26,6 +26,9 @@ void opacityDither(float alpha, float id) { #endif + // convert the noise to linear space, as that is specified in sRGB space (stores perceptual values) + noise = pow(noise, 2.2); + if (alpha < noise) discard; } diff --git a/src/scene/shader-lib/programs/particle.js b/src/scene/shader-lib/programs/particle.js index 288378be29c..07db592174f 100644 --- a/src/scene/shader-lib/programs/particle.js +++ b/src/scene/shader-lib/programs/particle.js @@ -82,20 +82,10 @@ class ShaderGeneratorParticle extends ShaderGenerator { } if (options.soft) fshader += "\nvarying float vDepth;\n"; - if ((options.normal === 0) && (options.fog === "none")) options.srgb = false; // don't have to perform all gamma conversions when no lighting and fogging is used fshader += shaderChunks.decodePS; fshader += ShaderGenerator.gammaCode(options.gamma); fshader += ShaderGenerator.tonemapCode(options.toneMap); - - if (options.fog === 'linear') { - fshader += shaderChunks.fogLinearPS; - } else if (options.fog === 'exp') { - fshader += shaderChunks.fogExpPS; - } else if (options.fog === 'exp2') { - fshader += shaderChunks.fogExp2PS; - } else { - fshader += shaderChunks.fogNonePS; - } + fshader += ShaderGenerator.fogCode(options.fog); if (options.normal === 2) fshader += "\nuniform sampler2D normalMap;\n"; if (options.soft > 0) fshader += shaderChunks.screenDepthPS;