Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 100 additions & 4 deletions examples/experimental/bloom/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: Copyright (c) vis.gl contributors

import type {Device, Framebuffer, TextureFormatColor} from '@luma.gl/core';
import {Texture, type Device, type Framebuffer, type TextureFormatColor} from '@luma.gl/core';
import {bloom, createBloomShaderPassPipeline, toneMapping} from '@luma.gl/effects';
import {
AnimationLoopTemplate,
Expand All @@ -25,13 +25,20 @@ export const title = 'Bloom';
export const description = 'Compare compact and HDR multiscale bloom on an animated HDR scene.';

const BLOOM_TECHNIQUES = ['Multiscale HDR', 'Compact', 'Off'] as const;
const BLOOM_QUALITIES = ['low', 'medium', 'high', 'ultra'] as const;

type BloomTechnique = (typeof BLOOM_TECHNIQUES)[number];
type BloomQuality = (typeof BLOOM_QUALITIES)[number];
type BloomSettings = {
technique: BloomTechnique;
quality: BloomQuality;
threshold: number;
intensity: number;
radius: number;
scatter: number;
softKnee: number;
fireflyReduction: number;
anamorphicRatio: number;
animate: boolean;
};
type SceneUniforms = {
Expand All @@ -42,14 +49,20 @@ type ShaderPassLike = ShaderPass | ShaderPassPipeline;

const DEFAULT_SETTINGS: BloomSettings = {
technique: 'Multiscale HDR',
quality: 'high',
threshold: 0.8,
intensity: 1.35,
radius: 12,
scatter: 0.55,
softKnee: 0.5,
fireflyReduction: 0.15,
anamorphicRatio: 0,
animate: true
};

const BLOOM_BACKGROUND_HTML = `
<p><b>Multiscale HDR bloom:</b> bright scene radiance is extracted once, blurred across half, quarter, and eighth-resolution targets, then composited before presentation. This is the reusable pipeline intended for richer effects integrations.</p>
<p><b>Multiscale HDR bloom:</b> bright scene radiance is extracted once, filtered across an adaptive two-to-five-level pyramid, then progressively reconstructed before presentation. Normalized upsampling keeps the glow stable as wider levels are combined.</p>
<p><b>Cinematic controls:</b> scatter balances tight highlights against broad glow, a soft knee smooths threshold transitions, firefly reduction stabilizes isolated HDR samples, and anamorphic ratio stretches the bloom horizontally or vertically.</p>
<p><b>Compact bloom:</b> the legacy single-pass glow samples one small neighborhood directly from the source image. It is cheaper, but it cannot spread highlights as naturally as the multiscale pyramid.</p>
<p><b>Scene setup:</b> this page renders animated HDR emitters into an offscreen texture before bloom. The previous static image hid the useful part of the effect by baking most of the lighting into SDR pixels.</p>
`;
Expand Down Expand Up @@ -287,6 +300,7 @@ export default class AppAnimationLoopTemplate extends AnimationLoopTemplate {
scene: sceneShaderModule
});
readonly sceneModel: ClipSpace;
readonly sceneColorTexture: Texture;
readonly sceneFramebuffer: Framebuffer;
readonly settingsPanel: ExampleSettingsPanelManager;
readonly panels: ExamplePanelManager;
Expand All @@ -305,11 +319,18 @@ export default class AppAnimationLoopTemplate extends AnimationLoopTemplate {
colorAttachmentFormats: [this.colorFormat],
shaderInputs: this.sceneShaderInputs
});
this.sceneColorTexture = device.createTexture({
id: 'bloom-hdr-scene-color',
width,
height,
format: this.colorFormat,
usage: Texture.RENDER | Texture.SAMPLE
});
this.sceneFramebuffer = device.createFramebuffer({
id: 'bloom-hdr-scene-framebuffer',
width,
height,
colorAttachments: [this.colorFormat]
colorAttachments: [this.sceneColorTexture]
});
this.settingsPanel = new ExampleSettingsPanelManager({
id: 'bloom-settings',
Expand All @@ -326,6 +347,7 @@ export default class AppAnimationLoopTemplate extends AnimationLoopTemplate {
this.settingsPanel.finalize();
this.panels.finalize();
this.sceneFramebuffer.destroy();
this.sceneColorTexture.destroy();
this.sceneModel.destroy();
this.sceneShaderInputs.destroy();
this.shaderPassRenderer?.destroy();
Expand Down Expand Up @@ -372,7 +394,12 @@ export default class AppAnimationLoopTemplate extends AnimationLoopTemplate {
colorFormat: this.colorFormat,
threshold: this.settings.threshold,
intensity: this.settings.intensity,
radius: this.settings.radius
radius: this.settings.radius,
quality: this.settings.quality,
scatter: this.settings.scatter,
softKnee: this.settings.softKnee,
fireflyReduction: this.settings.fireflyReduction,
anamorphicRatio: this.settings.anamorphicRatio
})
);
} else if (this.settings.technique === 'Compact') {
Expand Down Expand Up @@ -423,6 +450,7 @@ export default class AppAnimationLoopTemplate extends AnimationLoopTemplate {
technique: isBloomTechnique(settings['technique'])
? settings['technique']
: this.settings.technique,
quality: isBloomQuality(settings['quality']) ? settings['quality'] : this.settings.quality,
threshold:
typeof settings['threshold'] === 'number'
? clampNumber(settings['threshold'], 0, 4)
Expand All @@ -435,6 +463,22 @@ export default class AppAnimationLoopTemplate extends AnimationLoopTemplate {
typeof settings['radius'] === 'number'
? clampNumber(settings['radius'], 0, 24)
: this.settings.radius,
scatter:
typeof settings['scatter'] === 'number'
? clampNumber(settings['scatter'], 0, 1)
: this.settings.scatter,
softKnee:
typeof settings['softKnee'] === 'number'
? clampNumber(settings['softKnee'], 0, 1)
: this.settings.softKnee,
fireflyReduction:
typeof settings['fireflyReduction'] === 'number'
? clampNumber(settings['fireflyReduction'], 0, 1)
: this.settings.fireflyReduction,
anamorphicRatio:
typeof settings['anamorphicRatio'] === 'number'
? clampNumber(settings['anamorphicRatio'], -1, 1)
: this.settings.anamorphicRatio,
animate:
typeof settings['animate'] === 'boolean' ? settings['animate'] : this.settings.animate
};
Expand Down Expand Up @@ -509,6 +553,18 @@ export function makeBloomSettingsSchema(): SettingsSchema {
max: 4,
step: 0.05
},
{
name: 'quality',
label: 'Pyramid Quality',
type: 'select',
persist: 'none',
options: [
{label: 'Low (2 levels)', value: 'low'},
{label: 'Medium (3 levels)', value: 'medium'},
{label: 'High (4 levels)', value: 'high'},
{label: 'Ultra (5 levels)', value: 'ultra'}
]
},
{
name: 'intensity',
label: 'Glow Intensity',
Expand All @@ -527,6 +583,42 @@ export function makeBloomSettingsSchema(): SettingsSchema {
max: 24,
step: 1
},
{
name: 'scatter',
label: 'Wide Glow Scatter',
type: 'number',
persist: 'none',
min: 0,
max: 1,
step: 0.05
},
{
name: 'softKnee',
label: 'Threshold Soft Knee',
type: 'number',
persist: 'none',
min: 0,
max: 1,
step: 0.05
},
{
name: 'fireflyReduction',
label: 'Firefly Reduction',
type: 'number',
persist: 'none',
min: 0,
max: 1,
step: 0.05
},
{
name: 'anamorphicRatio',
label: 'Anamorphic Stretch',
type: 'number',
persist: 'none',
min: -1,
max: 1,
step: 0.05
},
{
name: 'animate',
label: 'Animate Scene',
Expand All @@ -547,6 +639,10 @@ function isBloomTechnique(value: unknown): value is BloomTechnique {
return BLOOM_TECHNIQUES.includes(value as BloomTechnique);
}

function isBloomQuality(value: unknown): value is BloomQuality {
return BLOOM_QUALITIES.includes(value as BloomQuality);
}

function clampNumber(value: number, minimum: number, maximum: number): number {
return Math.min(Math.max(value, minimum), maximum);
}
98 changes: 49 additions & 49 deletions examples/experimental/gpu-trace-viewer/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1119,22 +1119,22 @@ export default class GPUTraceViewerAnimationLoopTemplate extends AnimationLoopTe
workgroupSize: 1
});
for (const chunk of handles.spanChunks) {
addTraceIndirectComputePass(graph, {
addTraceIndirectComputePass(graph, {
id: `trace-candidate-span-visibility-${chunk.chunkIndex}`,
source: getCandidateVisibilityShader(chunk),
bindings: [
bindings: [
storageRead('spans', chunk.spans),
storageRead('spanBatches', handles.spanBatchIndex),
storageRead('candidateBatchIds', handles.candidateBatchIds),
uniformBinding('viewUniforms', handles.uniforms),
storageRead('processStates', handles.processStates),
storageRead('threadOffsets', handles.threadOffsets),
storageRead('threadStates', handles.threadStates),
storageRead('reachedSpans', handles.reachedSpans),
storageWrite('visibilityFlags', handles.spanVisibility)
],
dispatchBuffer: handles.exactCandidateDispatchCommands
});
storageRead('spanBatches', handles.spanBatchIndex),
storageRead('candidateBatchIds', handles.candidateBatchIds),
uniformBinding('viewUniforms', handles.uniforms),
storageRead('processStates', handles.processStates),
storageRead('threadOffsets', handles.threadOffsets),
storageRead('threadStates', handles.threadStates),
storageRead('reachedSpans', handles.reachedSpans),
storageWrite('visibilityFlags', handles.spanVisibility)
],
dispatchBuffer: handles.exactCandidateDispatchCommands
});
}
addTraceComputePass(graph, {
id: 'trace-clear-density',
Expand All @@ -1144,37 +1144,37 @@ export default class GPUTraceViewerAnimationLoopTemplate extends AnimationLoopTe
workgroupSize: TRACE_WORKGROUP_SIZE
});
for (const chunk of handles.spanChunks) {
addTraceIndirectComputePass(graph, {
addTraceIndirectComputePass(graph, {
id: `trace-candidate-density-${chunk.chunkIndex}`,
source: getCandidateDensityShader(chunk),
bindings: [
bindings: [
storageRead('spans', chunk.spans),
storageRead('spanBatches', handles.spanBatchIndex),
storageRead('candidateBatchIds', handles.candidateBatchIds),
uniformBinding('viewUniforms', handles.uniforms),
storageRead('processStates', handles.processStates),
storageRead('threadOffsets', handles.threadOffsets),
storageRead('threadStates', handles.threadStates),
storageRead('reachedSpans', handles.reachedSpans),
storageWrite('densityBins', handles.densityBins)
],
dispatchBuffer: handles.densityCandidateDispatchCommands
});
addTraceIndirectComputePass(graph, {
storageRead('spanBatches', handles.spanBatchIndex),
storageRead('candidateBatchIds', handles.candidateBatchIds),
uniformBinding('viewUniforms', handles.uniforms),
storageRead('processStates', handles.processStates),
storageRead('threadOffsets', handles.threadOffsets),
storageRead('threadStates', handles.threadStates),
storageRead('reachedSpans', handles.reachedSpans),
storageWrite('densityBins', handles.densityBins)
],
dispatchBuffer: handles.densityCandidateDispatchCommands
});
addTraceIndirectComputePass(graph, {
id: `trace-candidate-pick-${chunk.chunkIndex}`,
source: getCandidatePickShader(chunk),
bindings: [
bindings: [
storageRead('spans', chunk.spans),
storageRead('spanBatches', handles.spanBatchIndex),
storageRead('candidateBatchIds', handles.candidateBatchIds),
uniformBinding('viewUniforms', handles.uniforms),
storageRead('processStates', handles.processStates),
storageRead('threadOffsets', handles.threadOffsets),
storageRead('threadStates', handles.threadStates),
storageWrite('pickResult', handles.pickResult)
],
dispatchBuffer: handles.pickCandidateDispatchCommands
});
storageRead('spanBatches', handles.spanBatchIndex),
storageRead('candidateBatchIds', handles.candidateBatchIds),
uniformBinding('viewUniforms', handles.uniforms),
storageRead('processStates', handles.processStates),
storageRead('threadOffsets', handles.threadOffsets),
storageRead('threadStates', handles.threadStates),
storageWrite('pickResult', handles.pickResult)
],
dispatchBuffer: handles.pickCandidateDispatchCommands
});
}
const visibleSpanCountBuffer = graph.createTransientBuffer({
id: 'trace-visible-span-count',
Expand Down Expand Up @@ -1357,18 +1357,18 @@ export default class GPUTraceViewerAnimationLoopTemplate extends AnimationLoopTe
}

if (resources.dependencyCount > 0) {
encoder.setPipeline(this.dependencyModel.pipeline);
encoder.setVertexArray(this.dependencyModel.vertexArray);
encoder.setBindings({
dependencies: resources.dependencies,
visibleDependencyIds: resources.visibleDependencyIds,
encoder.setPipeline(this.dependencyModel.pipeline);
encoder.setVertexArray(this.dependencyModel.vertexArray);
encoder.setBindings({
dependencies: resources.dependencies,
visibleDependencyIds: resources.visibleDependencyIds,
spans: resources.spanChunks[0].buffer,
processStates: resources.processStates,
threadStates: resources.threadStates,
threadOffsets: resources.threadOffsets,
dependencyResults: resources.dependencyResults,
viewUniforms: this.viewUniformBuffer
});
processStates: resources.processStates,
threadStates: resources.threadStates,
threadOffsets: resources.threadOffsets,
dependencyResults: resources.dependencyResults,
viewUniforms: this.viewUniformBuffer
});
resources.drawCommands.draw(encoder, resources.dependencyDrawCommandIndex);
}

Expand Down
20 changes: 18 additions & 2 deletions modules/effects/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,31 @@ deferred lighting, higher-quality ambient visibility, diffuse bounce, specular r
participating-media scattering. Shared effects such as SSR use the same exported implementation
in both scenes.

`createBloomShaderPassPipeline` builds an HDR bloom pyramid with quality presets from two to five
levels. The pyramid progressively reconstructs its levels with normalized tent filtering; `scatter`,
`softKnee`, `fireflyReduction`, `anamorphicRatio`, and `tint` control the resulting glow without
requiring application-owned intermediate textures. The source texture must allow both sampling and,
when it is produced by an offscreen scene pass, rendering.

`toneMapping` applies an ACES filmic curve after exposure and preserves the source alpha channel.
Place it after bloom or other HDR effects so bright highlights roll off before presentation:

```typescript
import {ShaderPassRenderer} from '@luma.gl/engine';
import {bloomShaderPassPipeline, toneMapping} from '@luma.gl/effects';
import {createBloomShaderPassPipeline, toneMapping} from '@luma.gl/effects';

const renderer = new ShaderPassRenderer(device, {
shaderPasses: [bloomShaderPassPipeline, toneMapping]
shaderPasses: [
createBloomShaderPassPipeline({
quality: 'high',
threshold: 0.8,
intensity: 1.25,
scatter: 0.55,
softKnee: 0.5,
fireflyReduction: 0.15
}),
toneMapping
]
});

renderer.renderToScreen({
Expand Down
Loading
Loading