diff --git a/sample/particles/main.ts b/sample/particles/main.ts index 89237ff6..0fedd621 100644 --- a/sample/particles/main.ts +++ b/sample/particles/main.ts @@ -30,13 +30,23 @@ canvas.width = canvas.clientWidth * devicePixelRatio; canvas.height = canvas.clientHeight * devicePixelRatio; const presentationFormat = 'rgba16float'; +const simulationParams = { + simulate: true, + deltaTime: 0.04, + colorSpace: 'srgb' as string, + toneMappingMode: 'standard' as GPUCanvasToneMappingMode, + brightnessFactor: 1.0, +}; + function configureContext() { context.configure({ device, + colorSpace: simulationParams.colorSpace as PredefinedColorSpace, format: presentationFormat, toneMapping: { mode: simulationParams.toneMappingMode }, }); hdrFolder.name = getHdrFolderName(); + updateColorSpaceName(); } const particlesBuffer = device.createBuffer({ @@ -189,18 +199,27 @@ const mipLevelCount = const texture = device.createTexture({ size: [imageBitmap.width, imageBitmap.height, 1], mipLevelCount, - format: 'rgba8unorm', + format: 'rgba16float', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, }); -device.queue.copyExternalImageToTexture( - { source: imageBitmap }, - { texture: texture }, - [imageBitmap.width, imageBitmap.height] -); +// Copies the image into mip level 0, converting its colors to the current +// color space. The alpha channel (used for the probability map) is unaffected +// by the color space, so the probability map doesn't need regenerating. +function copyImageToTexture() { + device.queue.copyExternalImageToTexture( + { source: imageBitmap }, + { + texture: texture, + colorSpace: simulationParams.colorSpace as PredefinedColorSpace, + }, + [imageBitmap.width, imageBitmap.height] + ); +} +copyImageToTexture(); ////////////////////////////////////////////////////////////////////////////// // Probability map generation @@ -275,7 +294,7 @@ device.queue.copyExternalImageToTexture( // tex_in / tex_out binding: 3, resource: texture.createView({ - format: 'rgba8unorm', + format: 'rgba16float', dimension: '2d', baseMipLevel: level, mipLevelCount: 1, @@ -303,13 +322,6 @@ device.queue.copyExternalImageToTexture( ////////////////////////////////////////////////////////////////////////////// // Simulation compute pipeline ////////////////////////////////////////////////////////////////////////////// -const simulationParams = { - simulate: true, - deltaTime: 0.04, - toneMappingMode: 'standard' as GPUCanvasToneMappingMode, - brightnessFactor: 1.0, -}; - const simulationUBOBufferSize = 1 * 4 + // deltaTime 1 * 4 + // brightnessFactor @@ -325,7 +337,30 @@ const gui = new GUI(); gui.width = 325; gui.add(simulationParams, 'simulate'); gui.add(simulationParams, 'deltaTime'); -const hdrFolder = gui.addFolder(''); +const colorFolder = gui.addFolder('Color settings'); +colorFolder + .add( + simulationParams, + 'colorSpace', + ['srgb', 'srgb-linear', 'display-p3', 'display-p3-linear'].filter( + isColorSpaceSupported + ) + ) + .onChange(() => { + configureContext(); + copyImageToTexture(); + }); +colorFolder.open(); +const p3MediaQuery = window.matchMedia('(color-gamut: p3)'); +function updateColorSpaceName() { + const wantsP3 = simulationParams.colorSpace.startsWith('display-p3'); + colorFolder.name = + wantsP3 && !p3MediaQuery.matches + ? "Color settings ⚠️ Display isn't wide gamut" + : 'Color settings'; +} +p3MediaQuery.onchange = updateColorSpaceName; +const hdrFolder = gui.addFolder('HDR settings'); hdrFolder .add(simulationParams, 'toneMappingMode', ['standard', 'extended']) .onChange(configureContext); @@ -446,3 +481,21 @@ function assert(cond: boolean, msg = '') { throw new Error(msg); } } + +// Configuring a canvas context with an unsupported color space throws a +// TypeError, so probe each one on a throwaway OffscreenCanvas context. +function isColorSpaceSupported(colorSpace: string) { + const ctx = new OffscreenCanvas(1, 1).getContext('webgpu'); + try { + ctx.configure({ + device, + format: presentationFormat, + colorSpace: colorSpace as PredefinedColorSpace, + }); + return true; + } catch { + return false; + } finally { + ctx.unconfigure(); + } +} diff --git a/sample/particles/meta.ts b/sample/particles/meta.ts index a3416d84..465bb856 100644 --- a/sample/particles/meta.ts +++ b/sample/particles/meta.ts @@ -2,7 +2,7 @@ export default { name: 'Particles (HDR)', tocName: 'particles (HDR)', description: - 'This example demonstrates rendering of particles (using HDR capabilities when possible) simulated with compute shaders.', + 'This example demonstrates rendering of particles simulated with compute shaders, using HDR and wide color gamut canvas capabilities when possible.', filename: __DIRNAME__, sources: [ { path: 'main.ts' }, diff --git a/sample/particles/probabilityMap.wgsl b/sample/particles/probabilityMap.wgsl index f5e08f5b..0016a539 100644 --- a/sample/particles/probabilityMap.wgsl +++ b/sample/particles/probabilityMap.wgsl @@ -6,7 +6,7 @@ struct UBO { @binding(1) @group(0) var buf_in : array; @binding(2) @group(0) var buf_out : array; @binding(3) @group(0) var tex_in : texture_2d; -@binding(3) @group(0) var tex_out : texture_storage_2d; +@binding(3) @group(0) var tex_out : texture_storage_2d; //////////////////////////////////////////////////////////////////////////////// // import_level