This commit is contained in:
Andras Schmelczer 2026-05-17 14:12:01 +01:00
commit 80ed37298b
73 changed files with 3619 additions and 1295 deletions

View file

@ -9,17 +9,17 @@ import { RenderSettings } from './render-settings';
import shader from './render.wgsl?raw';
export class RenderPipeline {
private static readonly UNIFORM_COUNT = 16;
private static readonly UNIFORM_COUNT = 20;
private readonly bindGroupLayout: GPUBindGroupLayout;
private readonly pipeline: GPURenderPipeline;
private readonly sampledPipeline: GPURenderPipeline;
private readonly sampler: GPUSampler;
private readonly uniforms: GPUBuffer;
private readonly uniformValues = new Float32Array(RenderPipeline.UNIFORM_COUNT);
private readonly uniformCache = createCachedFloat32BufferWrite(
RenderPipeline.UNIFORM_COUNT
);
private readonly vertexBuffer: GPUBuffer;
private readonly bindGroupsByTexture = new WeakMap<
GPUTextureView,
@ -33,8 +33,7 @@ export class RenderPipeline {
) {
this.bindGroupLayout = device.createBindGroupLayout(RenderPipeline.bindGroupLayout);
const { buffer, vertex } = setUpFullScreenQuad(device);
this.vertexBuffer = buffer;
const vertex = setUpFullScreenQuad(device);
this.sampler = device.createSampler({
magFilter: 'linear',
@ -42,7 +41,8 @@ export class RenderPipeline {
});
const format = navigator.gpu.getPreferredCanvasFormat();
this.pipeline = this.createPipeline(format, vertex);
this.pipeline = this.createPipeline(format, vertex, 'fragment');
this.sampledPipeline = this.createPipeline(format, vertex, 'fragmentSampled');
this.uniforms = this.device.createBuffer({
size: RenderPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
@ -52,7 +52,8 @@ export class RenderPipeline {
private createPipeline(
format: GPUTextureFormat,
vertex: GPUVertexState
vertex: GPUVertexState,
fragmentEntryPoint: string
): GPURenderPipeline {
return this.device.createRenderPipeline({
layout: this.device.createPipelineLayout({
@ -61,7 +62,7 @@ export class RenderPipeline {
vertex,
fragment: {
module: smartCompile(this.device, CommonState.shaderCode, shader),
entryPoint: 'fragment',
entryPoint: fragmentEntryPoint,
targets: [
{
format,
@ -69,7 +70,7 @@ export class RenderPipeline {
],
},
primitive: {
topology: 'triangle-strip',
topology: 'triangle-list',
},
});
}
@ -78,6 +79,10 @@ export class RenderPipeline {
channelColors,
backgroundColor,
clarity,
renderTraceNormalizationFloor,
renderBrushColorBase,
renderBrushColorStrengthMultiplier,
backgroundGrainStrength,
}: RenderSettings & {
channelColors: Array<[number, number, number]>;
backgroundColor: [number, number, number];
@ -99,6 +104,10 @@ export class RenderPipeline {
this.uniformValues[13] = backgroundColor[1];
this.uniformValues[14] = backgroundColor[2];
this.uniformValues[15] = clarity;
this.uniformValues[16] = renderTraceNormalizationFloor;
this.uniformValues[17] = renderBrushColorBase;
this.uniformValues[18] = renderBrushColorStrengthMultiplier;
this.uniformValues[19] = backgroundGrainStrength;
writeFloat32BufferIfChanged(
this.device,
this.uniforms,
@ -128,9 +137,8 @@ export class RenderPipeline {
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(this.pipeline);
this.commonState.execute(passEncoder);
passEncoder.setVertexBuffer(0, this.vertexBuffer);
passEncoder.setBindGroup(1, bindGroup);
passEncoder.draw(4, 1);
passEncoder.draw(3, 1);
passEncoder.end();
return canvasTexture;
@ -154,11 +162,10 @@ export class RenderPipeline {
},
],
});
passEncoder.setPipeline(this.pipeline);
passEncoder.setPipeline(this.sampledPipeline);
this.commonState.execute(passEncoder);
passEncoder.setVertexBuffer(0, this.vertexBuffer);
passEncoder.setBindGroup(1, bindGroup);
passEncoder.draw(4, 1);
passEncoder.draw(3, 1);
passEncoder.end();
}
@ -206,7 +213,6 @@ export class RenderPipeline {
}
public destroy() {
this.vertexBuffer.destroy();
this.uniforms.destroy();
}

View file

@ -1,3 +1,7 @@
export interface RenderSettings {
clarity: number;
renderTraceNormalizationFloor: number;
renderBrushColorBase: number;
renderBrushColorStrengthMultiplier: number;
backgroundGrainStrength: number;
}

View file

@ -7,6 +7,10 @@ struct Settings {
backgroundColorPadding2: f32,
backgroundColor: vec3<f32>,
clarity: f32,
traceNormalizationFloor: f32,
brushColorBase: f32,
brushColorStrengthMultiplier: f32,
backgroundGrainStrength: f32,
};
@group(1) @binding(0) var<uniform> settings: Settings;
@ -15,9 +19,29 @@ struct Settings {
@group(1) @binding(3) var sourceMap: texture_2d<f32>;
@fragment
fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
fn fragment(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> {
let textureSize = vec2<i32>(textureDimensions(trailMap, 0));
let pixel = clamp(vec2<i32>(position.xy), vec2<i32>(0, 0), textureSize - vec2<i32>(1, 1));
let traces = textureLoad(trailMap, pixel, 0);
let sources = textureLoad(sourceMap, pixel, 0);
return renderColor(traces, sources, vec2<i32>(position.xy));
}
@fragment
fn fragmentSampled(
@location(0) uv: vec2<f32>,
@builtin(position) position: vec4<f32>
) -> @location(0) vec4<f32> {
let traces = textureSample(trailMap, Sampler, uv);
let sources = textureSample(sourceMap, Sampler, uv);
return renderColor(traces, sources, vec2<i32>(position.xy));
}
fn renderColor(traces: vec4<f32>, sources: vec4<f32>, pixel: vec2<i32>) -> vec4<f32> {
let background = getTexturedBackground(pixel);
if max(max(max(traces.r, traces.g), traces.b), max(max(sources.r, sources.g), sources.b)) <= 0.0 {
return vec4(background, 1);
}
let traceStrengths = vec3(
clarity(traces.r),
@ -34,17 +58,19 @@ fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
strengths.r * settings.colorA
+ strengths.g * settings.colorB
+ strengths.b * settings.colorC;
let normalizedTraceColor = traceColor / max(1.0, strengths.r + strengths.g + strengths.b);
let normalizedTraceColor =
traceColor / max(settings.traceNormalizationFloor, strengths.r + strengths.g + strengths.b);
let brushColor =
sourceStrengths.r * settings.colorA
+ sourceStrengths.g * settings.colorB
+ sourceStrengths.b * settings.colorC;
let brushStrength = clamp(max(max(sourceStrengths.r, sourceStrengths.g), sourceStrengths.b), 0, 1);
let color = max(normalizedTraceColor, brushColor * (1.2 + brushStrength * 1.6));
let strength = clamp(max(max(max(strengths.r, strengths.g), strengths.b), brushStrength), 0, 1);
let background = getTexturedBackground(uv);
let brushStrength = max(max(sourceStrengths.r, sourceStrengths.g), sourceStrengths.b);
let color = max(
normalizedTraceColor,
brushColor * (settings.brushColorBase + brushStrength * settings.brushColorStrengthMultiplier)
);
let strength = max(max(strengths.r, strengths.g), strengths.b);
return vec4(mix(background, clamp(color, vec3(0), vec3(1)), strength), 1);
}
@ -52,11 +78,14 @@ fn clarity(strength: f32) -> f32 {
return pow(clamp(strength, 0, 1), settings.clarity);
}
fn getTexturedBackground(uv: vec2<f32>) -> vec3<f32> {
let noiseSize = vec2<f32>(textureDimensions(noise, 0));
let pixel = floor(uv * state.size);
let noiseCoord = vec2<i32>(fract(pixel / noiseSize) * noiseSize);
fn getTexturedBackground(pixel: vec2<i32>) -> vec3<f32> {
let noiseSize = vec2<i32>(textureDimensions(noise, 0));
let noiseCoord = pixel % noiseSize;
let grain = textureLoad(noise, noiseCoord, 0).r - 0.5;
return clamp(settings.backgroundColor + vec3(grain * 0.018), vec3(0), vec3(1));
return clamp(
settings.backgroundColor + vec3(grain * settings.backgroundGrainStrength),
vec3(0),
vec3(1)
);
}