This commit is contained in:
Andras Schmelczer 2026-05-21 07:43:10 +01:00
commit 6bc125be1c
104 changed files with 3084 additions and 2410 deletions

View file

@ -17,17 +17,19 @@ export interface RenderSettings {
backgroundGrainStrength: number;
}
export class RenderPipeline {
private static readonly UNIFORM_COUNT = 20;
// 3 channel colors (vec3 + f32 padding) + bg color (vec3) + 5 scalars = 20 floats.
const UNIFORM_COUNT = 20;
export class RenderPipeline {
private readonly bindGroupLayout: GPUBindGroupLayout;
private readonly pipeline: GPURenderPipeline;
private readonly noSourcePipeline: GPURenderPipeline;
private readonly noGrainPipeline: GPURenderPipeline;
private readonly noSourceNoGrainPipeline: GPURenderPipeline;
private readonly uniforms: GPUBuffer;
private readonly uniformValues = new Float32Array(RenderPipeline.UNIFORM_COUNT);
private readonly uniformCache = createCachedFloat32BufferWrite(
RenderPipeline.UNIFORM_COUNT
);
private readonly uniformValues = new Float32Array(UNIFORM_COUNT);
private readonly uniformCache = createCachedFloat32BufferWrite(UNIFORM_COUNT);
private useBackgroundGrain = true;
private readonly getBindGroup = createBindGroupCache<GPUTextureView, GPUTextureView>(
(colorTexture, sourceTexture) =>
@ -46,42 +48,83 @@ export class RenderPipeline {
private readonly device: GPUDevice,
private readonly commonState: CommonState
) {
this.bindGroupLayout = device.createBindGroupLayout(RenderPipeline.bindGroupLayout);
this.bindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.FRAGMENT,
buffer: { type: 'uniform' },
},
{
binding: 2,
visibility: GPUShaderStage.FRAGMENT,
texture: { sampleType: 'float' },
},
{
binding: 3,
visibility: GPUShaderStage.FRAGMENT,
texture: { sampleType: 'float' },
},
],
});
const shaderModule = smartCompile(device, CommonState.shaderCode, shader);
const vertex = setUpFullScreenQuad(device);
const format = navigator.gpu.getPreferredCanvasFormat();
this.pipeline = this.createPipeline(format, vertex, 'fragment');
this.noSourcePipeline = this.createPipeline(format, vertex, 'fragmentNoSource');
const pipelineLayout = device.createPipelineLayout({
bindGroupLayouts: [this.commonState.bindGroupLayout, this.bindGroupLayout],
});
this.pipeline = this.createPipeline(
pipelineLayout,
vertex,
shaderModule,
format,
'fragment'
);
this.noSourcePipeline = this.createPipeline(
pipelineLayout,
vertex,
shaderModule,
format,
'fragmentNoSource'
);
this.noGrainPipeline = this.createPipeline(
pipelineLayout,
vertex,
shaderModule,
format,
'fragmentNoGrain'
);
this.noSourceNoGrainPipeline = this.createPipeline(
pipelineLayout,
vertex,
shaderModule,
format,
'fragmentNoSourceNoGrain'
);
this.uniforms = this.device.createBuffer({
size: RenderPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
this.uniforms = device.createBuffer({
size: UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
}
private createPipeline(
format: GPUTextureFormat,
layout: GPUPipelineLayout,
vertex: GPUVertexState,
shaderModule: GPUShaderModule,
format: GPUTextureFormat,
fragmentEntryPoint: string
): GPURenderPipeline {
return this.device.createRenderPipeline({
layout: this.device.createPipelineLayout({
bindGroupLayouts: [this.commonState.bindGroupLayout, this.bindGroupLayout],
}),
layout,
vertex,
fragment: {
module: smartCompile(this.device, CommonState.shaderCode, shader),
module: shaderModule,
entryPoint: fragmentEntryPoint,
targets: [
{
format,
},
],
},
primitive: {
topology: 'triangle-list',
targets: [{ format }],
},
primitive: { topology: 'triangle-list' },
});
}
@ -101,15 +144,13 @@ export class RenderPipeline {
this.uniformValues[0] = rgbChannelToUnit(a[0]);
this.uniformValues[1] = rgbChannelToUnit(a[1]);
this.uniformValues[2] = rgbChannelToUnit(a[2]);
this.uniformValues[3] = 0;
// uniformValues[3], [7], [11] are WGSL vec3→vec4 alignment padding.
this.uniformValues[4] = rgbChannelToUnit(b[0]);
this.uniformValues[5] = rgbChannelToUnit(b[1]);
this.uniformValues[6] = rgbChannelToUnit(b[2]);
this.uniformValues[7] = 0;
this.uniformValues[8] = rgbChannelToUnit(c[0]);
this.uniformValues[9] = rgbChannelToUnit(c[1]);
this.uniformValues[10] = rgbChannelToUnit(c[2]);
this.uniformValues[11] = 0;
this.uniformValues[12] = rgbChannelToUnit(backgroundColor[0]);
this.uniformValues[13] = rgbChannelToUnit(backgroundColor[1]);
this.uniformValues[14] = rgbChannelToUnit(backgroundColor[2]);
@ -118,6 +159,7 @@ export class RenderPipeline {
this.uniformValues[17] = renderBrushColorBase;
this.uniformValues[18] = renderBrushColorStrengthMultiplier;
this.uniformValues[19] = backgroundGrainStrength;
this.useBackgroundGrain = backgroundGrainStrength !== 0;
writeFloat32BufferIfChanged(
this.device,
this.uniforms,
@ -130,28 +172,18 @@ export class RenderPipeline {
commandEncoder: GPUCommandEncoder,
colorTexture: GPUTextureView,
sourceTexture: GPUTextureView,
useSourceTexture = true
useSourceTexture = true,
timestampWrites?: GPURenderPassTimestampWrites
): GPUTexture {
const bindGroup = this.getBindGroup(colorTexture, sourceTexture);
const canvasTexture = this.context.getCurrentTexture();
const renderPassDescriptor: GPURenderPassDescriptor = {
colorAttachments: [
{
view: canvasTexture.createView(),
clearValue: { r: 0, g: 0, b: 0, a: 1 },
loadOp: 'clear',
storeOp: 'store',
},
],
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(useSourceTexture ? this.pipeline : this.noSourcePipeline);
this.commonState.execute(passEncoder);
passEncoder.setBindGroup(1, bindGroup);
passEncoder.draw(3, 1);
passEncoder.end();
this.encodePass(
commandEncoder,
colorTexture,
sourceTexture,
canvasTexture.createView(),
useSourceTexture,
timestampWrites
);
return canvasTexture;
}
@ -159,56 +191,54 @@ export class RenderPipeline {
commandEncoder: GPUCommandEncoder,
colorTexture: GPUTextureView,
sourceTexture: GPUTextureView,
outputTexture: GPUTextureView
outputTexture: GPUTextureView,
useSourceTexture = true,
timestampWrites?: GPURenderPassTimestampWrites
) {
const bindGroup = this.getBindGroup(colorTexture, sourceTexture);
this.encodePass(
commandEncoder,
colorTexture,
sourceTexture,
outputTexture,
useSourceTexture,
timestampWrites
);
}
private encodePass(
commandEncoder: GPUCommandEncoder,
colorTexture: GPUTextureView,
sourceTexture: GPUTextureView,
output: GPUTextureView,
useSourceTexture: boolean,
timestampWrites?: GPURenderPassTimestampWrites
) {
const passEncoder = commandEncoder.beginRenderPass({
colorAttachments: [
{
view: outputTexture,
view: output,
clearValue: { r: 0, g: 0, b: 0, a: 1 },
loadOp: 'clear',
storeOp: 'store',
},
],
timestampWrites,
});
passEncoder.setPipeline(this.pipeline);
passEncoder.setPipeline(this.getPipeline(useSourceTexture));
this.commonState.execute(passEncoder);
passEncoder.setBindGroup(1, bindGroup);
passEncoder.setBindGroup(1, this.getBindGroup(colorTexture, sourceTexture));
passEncoder.draw(3, 1);
passEncoder.end();
}
private getPipeline(useSourceTexture: boolean): GPURenderPipeline {
if (useSourceTexture) {
return this.useBackgroundGrain ? this.pipeline : this.noGrainPipeline;
}
return this.useBackgroundGrain ? this.noSourcePipeline : this.noSourceNoGrainPipeline;
}
public destroy() {
this.uniforms.destroy();
}
private static get bindGroupLayout(): GPUBindGroupLayoutDescriptor {
return {
entries: [
{
binding: 0,
visibility: GPUShaderStage.FRAGMENT,
buffer: {
type: 'uniform',
},
},
{
binding: 2,
visibility: GPUShaderStage.FRAGMENT,
texture: {
sampleType: 'float',
},
},
{
binding: 3,
visibility: GPUShaderStage.FRAGMENT,
texture: {
sampleType: 'float',
},
},
],
};
}
}

View file

@ -1,10 +1,10 @@
struct Settings {
colorA: vec3<f32>,
backgroundColorPadding0: f32,
_colorAPadding: f32,
colorB: vec3<f32>,
backgroundColorPadding1: f32,
_colorBPadding: f32,
colorC: vec3<f32>,
backgroundColorPadding2: f32,
_colorCPadding: f32,
backgroundColor: vec3<f32>,
clarity: f32,
traceNormalizationFloor: f32,
@ -24,18 +24,32 @@ fn fragment(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> {
let pixel = vec2<i32>(position.xy);
let traces = textureLoad(trailMap, pixel, 0);
let sources = textureLoad(sourceMap, pixel, 0);
return renderColor(traces, sources, pixel);
return renderColor(traces, sources, getTexturedBackground(pixel));
}
@fragment
fn fragmentNoSource(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> {
let pixel = vec2<i32>(position.xy);
let traces = textureLoad(trailMap, pixel, 0);
return renderColor(traces, vec4<f32>(0.0), pixel);
return renderColor(traces, vec4<f32>(0.0), getTexturedBackground(pixel));
}
fn renderColor(traces: vec4<f32>, sources: vec4<f32>, pixel: vec2<i32>) -> vec4<f32> {
let background = getTexturedBackground(pixel);
@fragment
fn fragmentNoGrain(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> {
let pixel = vec2<i32>(position.xy);
let traces = textureLoad(trailMap, pixel, 0);
let sources = textureLoad(sourceMap, pixel, 0);
return renderColor(traces, sources, getFlatBackground());
}
@fragment
fn fragmentNoSourceNoGrain(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> {
let pixel = vec2<i32>(position.xy);
let traces = textureLoad(trailMap, pixel, 0);
return renderColor(traces, vec4<f32>(0.0), getFlatBackground());
}
fn renderColor(traces: vec4<f32>, sources: vec4<f32>, background: vec3<f32>) -> vec4<f32> {
let tracesMax = maxComponent(traces.rgb);
let sourcesMax = maxComponent(sources.rgb);
if max(tracesMax, sourcesMax) <= 0.0 {
@ -93,7 +107,11 @@ fn maxComponent(v: vec3<f32>) -> f32 {
}
fn clarity(strength: f32) -> f32 {
return pow(clamp(strength, 0, 1), settings.clarity);
let clamped = clamp(strength, 0, 1);
if settings.clarity == 1.0 {
return clamped;
}
return pow(clamped, settings.clarity);
}
fn normalizeColorIntensity(color: vec3<f32>) -> vec3<f32> {
@ -101,10 +119,11 @@ fn normalizeColorIntensity(color: vec3<f32>) -> vec3<f32> {
return color / max(settings.traceNormalizationFloor, brightestChannel);
}
fn getFlatBackground() -> vec3<f32> {
return clamp(settings.backgroundColor, vec3(0), vec3(1));
}
fn getTexturedBackground(pixel: vec2<i32>) -> vec3<f32> {
if settings.backgroundGrainStrength == 0.0 {
return clamp(settings.backgroundColor, vec3(0), vec3(1));
}
let noiseCoord = vec2<i32>(vec2<u32>(pixel) & vec2<u32>(NOISE_TEXTURE_MASK));
let grain = textureLoad(noise, noiseCoord, 0).r - 0.5;