This commit is contained in:
Andras Schmelczer 2026-05-13 21:07:10 +01:00
parent 34ac200437
commit 39b0160064
136 changed files with 7144 additions and 1965 deletions

View file

@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest';
import {
getSafeInverseDiffusionRate,
setDiffusionUniformValues,
} from './diffusion-pipeline';
describe('diffusion pipeline parameters', () => {
it('keeps zero diffusion rates finite before writing shader uniforms', () => {
const uniformValues = new Float32Array(4);
setDiffusionUniformValues(uniformValues, {
decayRateBrush: 900,
decayRateTrails: 970,
diffusionRateBrush: 0,
diffusionRateTrails: 0,
});
expect(Number.isFinite(uniformValues[0])).toBe(true);
expect(Number.isFinite(uniformValues[2])).toBe(true);
expect(uniformValues[0]).toBeGreaterThan(0);
expect(uniformValues[2]).toBeGreaterThan(0);
});
it('passes valid diffusion rates through as inverse values', () => {
expect(getSafeInverseDiffusionRate(2)).toBe(0.5);
expect(getSafeInverseDiffusionRate(0.25)).toBe(4);
});
});

View file

@ -1,3 +1,4 @@
import { appConfig } from '../../config';
import {
createCachedFloat32BufferWrite,
writeFloat32BufferIfChanged,
@ -8,8 +9,36 @@ import { CommonState } from '../common-state/common-state';
import shader from './diffuse.wgsl?raw';
import { DiffusionSettings } from './diffusion-settings';
const MIN_DIFFUSION_RATE = appConfig.pipelines.diffusion.minDiffusionRate;
type DiffusionUniformSettings = Pick<
DiffusionSettings,
'diffusionRateTrails' | 'decayRateTrails' | 'diffusionRateBrush' | 'decayRateBrush'
>;
export const getSafeInverseDiffusionRate = (diffusionRate: number): number =>
1 /
(Number.isFinite(diffusionRate) && diffusionRate > MIN_DIFFUSION_RATE
? diffusionRate
: MIN_DIFFUSION_RATE);
export const setDiffusionUniformValues = (
target: Float32Array,
{
diffusionRateTrails,
decayRateTrails,
diffusionRateBrush,
decayRateBrush,
}: DiffusionUniformSettings
): void => {
target[0] = getSafeInverseDiffusionRate(diffusionRateTrails);
target[1] = decayRateTrails / 1000;
target[2] = getSafeInverseDiffusionRate(diffusionRateBrush);
target[3] = decayRateBrush / 1000;
};
export class DiffusionPipeline {
private static readonly UNIFORM_COUNT = 5;
private static readonly UNIFORM_COUNT = 4;
private readonly bindGroupLayout: GPUBindGroupLayout;
private readonly pipeline: GPURenderPipeline;
@ -18,10 +47,10 @@ export class DiffusionPipeline {
private readonly uniformCache = createCachedFloat32BufferWrite(
DiffusionPipeline.UNIFORM_COUNT
);
private readonly sampler: GPUSampler;
private readonly vertexBuffer: GPUBuffer;
private bindGroup?: GPUBindGroup;
private previousTrailMapIn?: GPUTextureView;
private readonly bindGroupsByInput = new WeakMap<GPUTextureView, GPUBindGroup>();
public constructor(
private readonly device: GPUDevice,
@ -57,6 +86,11 @@ export class DiffusionPipeline {
size: DiffusionPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
this.sampler = this.device.createSampler({
magFilter: 'linear',
minFilter: 'linear',
});
}
public setParameters({
@ -64,13 +98,13 @@ export class DiffusionPipeline {
decayRateTrails,
diffusionRateBrush,
decayRateBrush,
anisotropy,
}: DiffusionSettings) {
this.uniformValues[0] = 1 / diffusionRateTrails;
this.uniformValues[1] = decayRateTrails / 1000;
this.uniformValues[2] = 1 / diffusionRateBrush;
this.uniformValues[3] = decayRateBrush / 1000;
this.uniformValues[4] = anisotropy;
setDiffusionUniformValues(this.uniformValues, {
diffusionRateTrails,
decayRateTrails,
diffusionRateBrush,
decayRateBrush,
});
writeFloat32BufferIfChanged(
this.device,
this.uniforms,
@ -84,7 +118,7 @@ export class DiffusionPipeline {
trailMapIn: GPUTextureView,
trailMapOut: GPUTextureView
) {
this.ensureBindGroupExists(trailMapIn);
const bindGroup = this.getBindGroup(trailMapIn);
const renderPassDescriptor: GPURenderPassDescriptor = {
colorAttachments: [
@ -101,38 +135,39 @@ export class DiffusionPipeline {
passEncoder.setPipeline(this.pipeline);
passEncoder.setVertexBuffer(0, this.vertexBuffer);
this.commonState.execute(passEncoder);
passEncoder.setBindGroup(1, this.bindGroup);
passEncoder.setBindGroup(1, bindGroup);
passEncoder.draw(4, 1);
passEncoder.end();
}
private ensureBindGroupExists(trailMapIn: GPUTextureView) {
if (this.previousTrailMapIn !== trailMapIn) {
this.bindGroup = this.device.createBindGroup({
layout: this.bindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: this.uniforms,
},
},
{
binding: 1,
resource: this.device.createSampler({
magFilter: 'linear',
minFilter: 'linear',
}),
},
{
binding: 2,
resource: trailMapIn,
},
],
});
this.previousTrailMapIn = trailMapIn;
private getBindGroup(trailMapIn: GPUTextureView): GPUBindGroup {
const cached = this.bindGroupsByInput.get(trailMapIn);
if (cached) {
return cached;
}
const bindGroup = this.device.createBindGroup({
layout: this.bindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: this.uniforms,
},
},
{
binding: 1,
resource: this.sampler,
},
{
binding: 2,
resource: trailMapIn,
},
],
});
this.bindGroupsByInput.set(trailMapIn, bindGroup);
return bindGroup;
}
public destroy() {

View file

@ -3,4 +3,5 @@ export interface DiffusionSettings {
decayRateTrails: number;
diffusionRateBrush: number;
decayRateBrush: number;
brushEffectDuration: number;
}