More clean up

This commit is contained in:
Andras Schmelczer 2026-05-20 21:03:41 +01:00
commit f03da42b5e
43 changed files with 818 additions and 1076 deletions

View file

@ -1,28 +1,19 @@
export const AGENT_WORKGROUP_SIZE = 64;
export const AGENT_MAX_DISPATCH_WORKGROUPS_PER_DIMENSION = 65_535;
export const AGENT_MAX_DISPATCHABLE_COUNT = 0xffffffff;
const AGENT_WORKGROUP_SIZE = 64;
const AGENT_MAX_DISPATCH_WORKGROUPS_PER_DIMENSION = 65_535;
export const AGENT_MAX_DISPATCHABLE_COUNT =
AGENT_MAX_DISPATCH_WORKGROUPS_PER_DIMENSION * AGENT_WORKGROUP_SIZE;
export const getAgentDispatchWorkgroups = (agentCount: number): [number, number] => {
const getAgentDispatchWorkgroups = (agentCount: number): [number, number] => {
if (!Number.isFinite(agentCount) || agentCount <= 0) {
throw new Error('Agent count must be a positive finite number');
}
const workgroupCount = Math.ceil(agentCount / AGENT_WORKGROUP_SIZE);
if (workgroupCount <= AGENT_MAX_DISPATCH_WORKGROUPS_PER_DIMENSION) {
return [workgroupCount, 1];
}
const workgroupX = Math.min(
Math.ceil(Math.sqrt(workgroupCount)),
AGENT_MAX_DISPATCH_WORKGROUPS_PER_DIMENSION
);
const workgroupY = Math.ceil(workgroupCount / workgroupX);
if (workgroupY > AGENT_MAX_DISPATCH_WORKGROUPS_PER_DIMENSION) {
if (workgroupCount > AGENT_MAX_DISPATCH_WORKGROUPS_PER_DIMENSION) {
throw new Error('Agent count exceeds dispatchable workgroup range');
}
return [workgroupX, workgroupY];
return [workgroupCount, 1];
};
export const dispatchAgentWorkgroups = (

View file

@ -9,6 +9,8 @@ struct Counters {
aliveAgentCount: atomic<u32>,
};
const clearCompactedTailStride = 4u;
@group(1) @binding(0) var<uniform> settings: Settings;
@group(1) @binding(2) var<storage, read_write> counters: Counters;
@group(1) @binding(3) var<storage, read_write> compactedAgents: array<Agent>;
@ -20,10 +22,9 @@ var<workgroup> clearAliveAgentCount: u32;
@compute @workgroup_size(64)
fn main(
@builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(local_invocation_id) local_id: vec3<u32>,
@builtin(num_workgroups) num_workgroups: vec3<u32>
@builtin(local_invocation_id) local_id: vec3<u32>
) {
let id = get_id(global_id, num_workgroups);
let id = get_id(global_id);
if local_id.x == 0u {
atomicStore(&workgroupAliveCount, 0u);
@ -63,10 +64,9 @@ fn main(
@compute @workgroup_size(64)
fn clearCompactedTail(
@builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(local_invocation_id) local_id: vec3<u32>,
@builtin(num_workgroups) num_workgroups: vec3<u32>
@builtin(local_invocation_id) local_id: vec3<u32>
) {
let id = get_id(global_id, num_workgroups);
let id = get_id(global_id);
if local_id.x == 0u {
clearAliveAgentCount = atomicLoad(&counters.aliveAgentCount);
@ -74,11 +74,11 @@ fn clearCompactedTail(
workgroupBarrier();
if id >= settings.agentCount {
return;
}
if id >= clearAliveAgentCount {
compactedAgents[id].colorIndex = -1.0;
let firstClearId = clearAliveAgentCount + id * clearCompactedTailStride;
for (var offset = 0u; offset < clearCompactedTailStride; offset += 1u) {
let clearId = firstClearId + offset;
if clearId < settings.agentCount {
compactedAgents[clearId].colorIndex = -1.0;
}
}
}

View file

@ -1,21 +1,19 @@
import { vec2 } from 'gl-matrix';
import { smartCompile } from '../../../utils/graphics/smart-compile';
import {
AGENT_MAX_DISPATCHABLE_COUNT,
dispatchAgentWorkgroups,
} from '../agent-dispatch';
import { AGENT_MAX_DISPATCHABLE_COUNT, dispatchAgentWorkgroups } from '../agent-dispatch';
import compactionShader from './agent-compaction.wgsl?raw';
import resizeShader from './agent-resize.wgsl?raw';
import agentSchema from './agent-schema.wgsl?raw';
export const AGENT_FLOAT_COUNT = 8;
export const AGENT_SIZE_IN_BYTES =
AGENT_FLOAT_COUNT * Float32Array.BYTES_PER_ELEMENT;
const AGENT_SIZE_IN_BYTES = AGENT_FLOAT_COUNT * Float32Array.BYTES_PER_ELEMENT;
export class AgentGenerationPipeline {
private static readonly UNIFORM_COUNT = 4;
private static readonly COUNTER_COUNT = 1;
private static readonly CLEAR_COMPACTED_TAIL_STRIDE = 4;
private static readonly ALLOCATION_GROWTH_FACTOR = 1.25;
private readonly bindGroupLayout: GPUBindGroupLayout;
private readonly uniforms: GPUBuffer;
@ -33,21 +31,14 @@ export class AgentGenerationPipeline {
private allocatedMaxAgentCount: number;
private readonly countersBuffer: GPUBuffer;
private readonly countersStagingBuffer: GPUBuffer;
private readonly counterClearValues = new Uint32Array(
AgentGenerationPipeline.COUNTER_COUNT
);
private readonly agentCountUniformValues = new Uint32Array(
AgentGenerationPipeline.UNIFORM_COUNT
);
private readonly resizeUniformBuffer = new ArrayBuffer(
AgentGenerationPipeline.UNIFORM_COUNT * Uint32Array.BYTES_PER_ELEMENT
);
private readonly resizeUniformFloatValues = new Float32Array(
this.resizeUniformBuffer
);
private readonly resizeUniformUintValues = new Uint32Array(
this.resizeUniformBuffer
);
private readonly resizeUniformFloatValues = new Float32Array(this.resizeUniformBuffer);
private readonly resizeUniformUintValues = new Uint32Array(this.resizeUniformBuffer);
public constructor(
private readonly device: GPUDevice,
@ -173,11 +164,19 @@ export class AgentGenerationPipeline {
requestedMaxAgentCount: number,
activeAgentCount: number
): number {
const nextMaxAgentCount = this.clampMaxAgentCount(requestedMaxAgentCount);
if (nextMaxAgentCount <= this.allocatedMaxAgentCount) {
const requestedClampedMaxAgentCount = this.clampMaxAgentCount(requestedMaxAgentCount);
if (requestedClampedMaxAgentCount <= this.allocatedMaxAgentCount) {
return this.allocatedMaxAgentCount;
}
const nextMaxAgentCount = this.clampMaxAgentCount(
Math.max(
requestedClampedMaxAgentCount,
Math.ceil(
this.allocatedMaxAgentCount * AgentGenerationPipeline.ALLOCATION_GROWTH_FACTOR
)
)
);
const previousActiveAgentsBuffer = this.activeAgentsBuffer;
const previousMaxAgentCount = this.allocatedMaxAgentCount;
this.allocatedMaxAgentCount = nextMaxAgentCount;
@ -270,16 +269,19 @@ export class AgentGenerationPipeline {
this.inactiveAgentsBuffer = this.createAgentsBuffer();
this.agentCountUniformValues[0] = agentCount;
this.device.queue.writeBuffer(this.countersBuffer, 0, this.counterClearValues);
this.device.queue.writeBuffer(this.uniforms, 0, this.agentCountUniformValues);
const commandEncoder = this.device.createCommandEncoder();
commandEncoder.clearBuffer(this.countersBuffer, 0, Uint32Array.BYTES_PER_ELEMENT);
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(this.compactionPipeline);
passEncoder.setBindGroup(1, this.getBindGroup());
dispatchAgentWorkgroups(passEncoder, agentCount);
passEncoder.setPipeline(this.clearCompactedTailPipeline);
dispatchAgentWorkgroups(passEncoder, agentCount);
dispatchAgentWorkgroups(
passEncoder,
Math.ceil(agentCount / AgentGenerationPipeline.CLEAR_COMPACTED_TAIL_STRIDE)
);
passEncoder.end();
commandEncoder.copyBufferToBuffer(

View file

@ -8,10 +8,9 @@ struct ResizeSettings {
@compute @workgroup_size(64)
fn main(
@builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(num_workgroups) num_workgroups: vec3<u32>
@builtin(global_invocation_id) global_id: vec3<u32>
) {
let id = get_id(global_id, num_workgroups);
let id = get_id(global_id);
if id >= resizeSettings.agentCount {
return;

View file

@ -11,6 +11,6 @@ struct Agent {
const agentWorkgroupSize = 64u;
fn get_id(global_id: vec3<u32>, num_workgroups: vec3<u32>) -> u32 {
return global_id.x + global_id.y * num_workgroups.x * agentWorkgroupSize;
fn get_id(global_id: vec3<u32>) -> u32 {
return global_id.x;
}

View file

@ -79,10 +79,12 @@ export class AgentPipeline {
introNearMoveMultiplier,
introStepStopDistance,
randomTimeScale,
time,
agentCount,
introProgress,
}: AgentSettings & {
deltaTime: number;
time: number;
agentCount: number;
introProgress?: number;
}) {
@ -117,7 +119,7 @@ export class AgentPipeline {
this.uniformValues[26] = introFarMoveMultiplier;
this.uniformValues[27] = introNearMoveMultiplier;
this.uniformValues[28] = introStepStopDistance;
this.uniformValues[29] = randomTimeScale;
this.uniformUintValues[29] = Math.max(0, Math.floor(time * randomTimeScale)) >>> 0;
writeFloat32BufferIfChanged(
this.device,
this.uniforms,

View file

@ -28,7 +28,7 @@ struct Settings {
introFarMoveMultiplier: f32,
introNearMoveMultiplier: f32,
introStepStopDistance: f32,
randomTimeScale: f32,
randomTimeSeed: u32,
};
@group(1) @binding(0) var<uniform> settings: Settings;
@ -37,10 +37,9 @@ struct Settings {
@compute @workgroup_size(64)
fn main(
@builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(num_workgroups) num_workgroups: vec3<u32>
@builtin(global_invocation_id) global_id: vec3<u32>
) {
let id = get_id(global_id, num_workgroups);
let id = get_id(global_id);
if id >= settings.agentCount {
return;
@ -65,7 +64,8 @@ fn main(
let channelMask = get_channel_mask(colorIndex);
let reactionMask = get_reaction_mask(colorIndex);
let randomSeed = random_seed(id, state.time);
let randomSeed = random_seed(id);
let maxPosition = state.size - vec2<f32>(1.0, 1.0);
var rotation = 0.0;
var step = vec2<f32>(0.0, 0.0);
@ -108,16 +108,18 @@ fn main(
let randomTurn = random_float(randomSeed);
let direction = vec2(cos(angle), sin(angle));
let forwardSensor = sensor_position(position, direction, settings.sensorOffset);
let forwardSensor = sensor_position(position, direction, settings.sensorOffset, maxPosition);
let leftSensor = sensor_position(
position,
rotate_direction(direction, settings.sensorAngleSin, settings.sensorAngleCos),
settings.sensorOffset
settings.sensorOffset,
maxPosition
);
let rightSensor = sensor_position(
position,
rotate_direction(direction, -settings.sensorAngleSin, settings.sensorAngleCos),
settings.sensorOffset
settings.sensorOffset,
maxPosition
);
let trailForward = textureLoad(trailMapIn, forwardSensor, 0);
@ -138,7 +140,6 @@ fn main(
step = direction * settings.moveRate;
}
let maxPosition = state.size - vec2<f32>(1.0, 1.0);
let nextPosition = clamp(position + step, vec2<f32>(0, 0), maxPosition);
if nextPosition.x == 0 || nextPosition.x == maxPosition.x || nextPosition.y == 0 || nextPosition.y == maxPosition.y {
rotation = 3.14159265359 + random_float(randomSeed + 22695477u) - 0.5;
@ -155,11 +156,16 @@ fn main(
agents[id].position = nextPosition;
}
fn sensor_position(agentPosition: vec2<f32>, direction: vec2<f32>, sensorOffset: f32) -> vec2<i32> {
fn sensor_position(
agentPosition: vec2<f32>,
direction: vec2<f32>,
sensorOffset: f32,
maxPosition: vec2<f32>
) -> vec2<i32> {
return vec2<i32>(clamp(
agentPosition + direction * sensorOffset,
vec2<f32>(0, 0),
state.size - vec2<f32>(1, 1)
maxPosition
));
}
@ -212,9 +218,8 @@ fn angle_delta(sourceAngle: f32, targetAngle: f32) -> f32 {
return atan2(sin(targetAngle - sourceAngle), cos(targetAngle - sourceAngle));
}
fn random_seed(id: u32, time: f32) -> u32 {
let timeSeed = u32(time * settings.randomTimeScale);
return id * 747796405u + timeSeed * 2891336453u;
fn random_seed(id: u32) -> u32 {
return id * 747796405u + settings.randomTimeSeed * 2891336453u;
}
fn random_float(seed: u32) -> f32 {

View file

@ -25,7 +25,7 @@ export const getSafePixelRatio = (pixelRatio: number | undefined): number =>
? pixelRatio
: 1;
export const setBrushUniformValues = (
const setBrushUniformValues = (
target: Float32Array,
{
brushSize,
@ -105,7 +105,7 @@ export class BrushPipeline {
});
const shaderModule = smartCompile(device, CommonState.shaderCode, shader);
this.multiTargetPipeline = this.createPipeline(shaderModule, 'fragmentMrt', 2);
this.multiTargetPipeline = this.createPipeline(shaderModule, 'fragmentMrt', 1);
this.uniforms = this.device.createBuffer({
size: BrushPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
@ -209,12 +209,10 @@ export class BrushPipeline {
public executeMultiTarget(
commandEncoder: GPUCommandEncoder,
sourceMapOut: GPUTextureView,
influenceMapOut: GPUTextureView
): void {
this.executeWithPipeline(commandEncoder, this.multiTargetPipeline, [
sourceMapOut: GPUTextureView
): boolean {
return this.executeWithPipeline(commandEncoder, this.multiTargetPipeline, [
sourceMapOut,
influenceMapOut,
]);
}
@ -222,9 +220,9 @@ export class BrushPipeline {
commandEncoder: GPUCommandEncoder,
pipeline: GPURenderPipeline,
textureViews: Array<GPUTextureView>
): void {
): boolean {
if (this.lineCount === 0) {
return;
return false;
}
const renderPassDescriptor: GPURenderPassDescriptor = {
@ -242,6 +240,7 @@ export class BrushPipeline {
passEncoder.setVertexBuffer(0, this.vertexBuffer);
passEncoder.draw(BrushPipeline.VERTICES_PER_LINE_SEGMENT, this.lineCount);
passEncoder.end();
return true;
}
public destroy() {

View file

@ -25,7 +25,6 @@ struct VertexOutput {
struct BrushTargets {
@location(0) source: vec4<f32>,
@location(1) influence: vec4<f32>,
}
@vertex
@ -53,6 +52,22 @@ fn fragmentMrt(
@location(2) @interpolate(flat) direction: vec2<f32>,
@location(3) @interpolate(flat) inverseLengthSquared: f32
) -> BrushTargets {
let strength = brushStrength(screenPosition, start, direction, inverseLengthSquared);
if(strength < settings.brushDiscardThreshold) {
discard;
}
let color = brushOutput(strength);
return BrushTargets(color);
}
fn brushStrength(
screenPosition: vec2<f32>,
start: vec2<f32>,
direction: vec2<f32>,
inverseLengthSquared: f32
) -> f32 {
let distanceSquared = distanceSquaredFromLine(
screenPosition,
start,
@ -60,24 +75,9 @@ fn fragmentMrt(
inverseLengthSquared
);
if distanceSquared > settings.brushGeometryRadiusSquared {
discard;
return 0.0;
}
let strength = brushStrength(screenPosition, distanceSquared);
if(strength < settings.brushDiscardThreshold) {
discard;
}
let color = brushOutput(strength);
return BrushTargets(color, color);
}
fn brushStrength(
screenPosition: vec2<f32>,
distanceSquared: f32
) -> f32 {
let distance = sqrt(distanceSquared);
let coarseNoise = textureSampleLevel(
noise,
noiseSampler,
@ -85,7 +85,7 @@ fn brushStrength(
0.0
).r;
let radius = settings.brushSize + (coarseNoise - 0.5) * settings.brushSizeVariation;
let edge = 1.0 - step(radius, distance);
let edge = 1.0 - step(radius * radius, distanceSquared);
if edge * max(settings.brushGrainMinStrength, settings.brushGrainMaxStrength) < settings.brushDiscardThreshold {
return 0.0;
}
@ -112,10 +112,6 @@ fn distanceSquaredFromLine(
) -> f32 {
let pa = position - start;
if inverseLengthSquared <= 0.0 {
return dot(pa, pa);
}
let q = clamp(dot(pa, direction) * inverseLengthSquared, 0, 1);
let nearestOffset = pa - direction * q;
return dot(nearestOffset, nearestOffset);
@ -140,14 +136,13 @@ fn segment_vertex_position(
}
fn segment_vertex_corner(index: u32) -> vec2<f32> {
if index == 0u {
return vec2<f32>(-1.0, 1.0);
}
if index == 1u || index == 3u {
return vec2<f32>(-1.0, -1.0);
}
if index == 2u || index == 4u {
return vec2<f32>(1.0, 1.0);
}
return vec2<f32>(1.0, -1.0);
let corners = array<vec2<f32>, 6>(
vec2<f32>(-1.0, 1.0),
vec2<f32>(-1.0, -1.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(-1.0, -1.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(1.0, -1.0),
);
return corners[index];
}

View file

@ -20,6 +20,7 @@ const TILE_TEXEL_COUNT = TILE_SIZE_X * TILE_SIZE_Y;
@group(0) @binding(2) var trailMapOut: texture_storage_2d<rgba16float, write>;
var<workgroup> tile: array<vec4<f32>, 324>;
var<workgroup> tileTrailStrength: array<f32, 324>;
@compute @workgroup_size(16, 16)
fn main(
@ -28,31 +29,32 @@ fn main(
@builtin(workgroup_id) workgroup_id: vec3<u32>
) {
let textureSize = vec2<i32>(textureDimensions(trailMap, 0));
let textureSizeU32 = vec2<u32>(textureSize);
let localLinearIndex = local_id.y * WORKGROUP_SIZE_X + local_id.x;
var tileIndex = localLinearIndex;
loop {
if tileIndex >= TILE_TEXEL_COUNT {
break;
}
let workgroupOrigin = workgroup_id.xy * vec2<u32>(WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y);
let isInteriorTile =
workgroupOrigin.x > 0u &&
workgroupOrigin.y > 0u &&
workgroupOrigin.x + WORKGROUP_SIZE_X < textureSizeU32.x &&
workgroupOrigin.y + WORKGROUP_SIZE_Y < textureSizeU32.y;
for (var tileIndex = localLinearIndex; tileIndex < TILE_TEXEL_COUNT; tileIndex += WORKGROUP_SIZE_X * WORKGROUP_SIZE_Y) {
let tilePosition = vec2<u32>(tileIndex % TILE_SIZE_X, tileIndex / TILE_SIZE_X);
let sourcePixelU32 =
workgroup_id.xy * vec2<u32>(WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y) +
tilePosition;
let sourcePixel = clamp(
vec2<i32>(i32(sourcePixelU32.x), i32(sourcePixelU32.y)) - vec2<i32>(1, 1),
vec2<i32>(0, 0),
textureSize - vec2<i32>(1, 1)
);
tile[tileIndex] = textureLoad(trailMap, sourcePixel, 0);
tileIndex += WORKGROUP_SIZE_X * WORKGROUP_SIZE_Y;
let unclampedSourcePixel = vec2<i32>(workgroupOrigin + tilePosition) - vec2<i32>(1, 1);
var sourcePixel = unclampedSourcePixel;
if !isInteriorTile {
sourcePixel = clamp(unclampedSourcePixel, vec2<i32>(0, 0), textureSize - vec2<i32>(1, 1));
}
let texel = textureLoad(trailMap, sourcePixel, 0);
tile[tileIndex] = texel;
tileTrailStrength[tileIndex] = length(texel.rgb);
}
workgroupBarrier();
let pixel = vec2<i32>(i32(global_id.x), i32(global_id.y));
if pixel.x >= textureSize.x || pixel.y >= textureSize.y {
let inBounds = pixel.x < textureSize.x && pixel.y < textureSize.y;
if !inBounds {
return;
}
@ -110,11 +112,12 @@ fn propagate(
brushWeight: f32
) -> vec4<f32> {
let neighbourIndex = i32(centerTileIndex) + offsetY * i32(TILE_SIZE_X) + offsetX;
let neighbour = tile[u32(neighbourIndex)];
let neighbourTileIndex = u32(neighbourIndex);
let neighbour = tile[neighbourTileIndex];
let difference = clamp(neighbour - currentColor, vec4(0), vec4(1));
return vec4(
vec3(length(neighbour.rgb) * trailWeight),
vec3(tileTrailStrength[neighbourTileIndex] * trailWeight),
neighbour.a * brushWeight
) * difference;
}

View file

@ -21,14 +21,14 @@ type DiffusionUniformSettings = Pick<
| 'brushDecayAlphaOffset'
>;
export const getSafeInverseDiffusionRate = (diffusionRate: number): number =>
const getSafeInverseDiffusionRate = (diffusionRate: number): number =>
1 /
(Number.isFinite(diffusionRate) &&
diffusionRate > appConfig.pipelines.diffusion.minDiffusionRate
? diffusionRate
: appConfig.pipelines.diffusion.minDiffusionRate);
export const setDiffusionUniformValues = (
const setDiffusionUniformValues = (
target: Float32Array,
{
diffusionRateTrails,

View file

@ -1,3 +1,5 @@
import { vec2 } from 'gl-matrix';
import {
createCachedFloat32BufferWrite,
writeFloat32BufferIfChanged,
@ -86,9 +88,11 @@ export class EraserAgentPipeline {
public setParameters({
agentCount,
eraserMaskAlphaThreshold,
maskSize,
}: {
agentCount: number;
eraserMaskAlphaThreshold: number;
maskSize: vec2;
}): void {
this.agentCount = agentCount;
this.activeSegmentCount = this.pendingSegmentCount;
@ -96,8 +100,8 @@ export class EraserAgentPipeline {
this.uniformUintValues[0] = Math.max(0, Math.floor(agentCount));
this.uniformValues[1] = eraserMaskAlphaThreshold;
this.uniformValues[2] = 0;
this.uniformValues[3] = 0;
this.uniformUintValues[2] = Math.max(0, Math.floor(maskSize[0]));
this.uniformUintValues[3] = Math.max(0, Math.floor(maskSize[1]));
writeFloat32BufferIfChanged(
this.device,
this.uniforms,

View file

@ -1,8 +1,8 @@
struct Settings {
agentCount: u32,
eraserMaskAlphaThreshold: f32,
padding1: f32,
padding2: f32,
maskWidth: u32,
maskHeight: u32,
};
@group(1) @binding(0) var<uniform> settings: Settings;
@ -10,10 +10,9 @@ struct Settings {
@compute @workgroup_size(64)
fn main(
@builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(num_workgroups) num_workgroups: vec3<u32>
@builtin(global_invocation_id) global_id: vec3<u32>
) {
let id = get_id(global_id, num_workgroups);
let id = get_id(global_id);
if id >= settings.agentCount {
return;
@ -24,7 +23,7 @@ fn main(
return;
}
let maskSize = vec2<i32>(textureDimensions(eraserMask));
let maskSize = vec2<i32>(i32(settings.maskWidth), i32(settings.maskHeight));
let maskPosition = clamp(
vec2<i32>(agents[id].position),
vec2<i32>(0, 0),

View file

@ -68,7 +68,6 @@ export class EraserTexturePipeline {
'r8unorm',
'rgba16float',
'rgba16float',
'rgba16float',
]);
this.uniforms = this.device.createBuffer({
@ -169,7 +168,6 @@ export class EraserTexturePipeline {
commandEncoder: GPUCommandEncoder,
eraserMaskOut: GPUTextureView,
sourceMapOut: GPUTextureView,
influenceMapOut: GPUTextureView,
trailMapOut: GPUTextureView
): void {
if (this.lineCount === 0) {
@ -200,11 +198,6 @@ export class EraserTexturePipeline {
loadOp: 'load',
storeOp: 'store',
},
{
view: influenceMapOut,
loadOp: 'load',
storeOp: 'store',
},
{
view: trailMapOut,
loadOp: 'load',

View file

@ -19,17 +19,10 @@ struct VertexOutput {
@location(3) @interpolate(flat) inverseLengthSquared: f32,
}
struct EraserTextureTargets {
@location(0) source: vec4<f32>,
@location(1) influence: vec4<f32>,
@location(2) trail: vec4<f32>,
}
struct EraserCombinedTargets {
@location(0) mask: vec4<f32>,
@location(1) source: vec4<f32>,
@location(2) influence: vec4<f32>,
@location(3) trail: vec4<f32>,
@location(2) trail: vec4<f32>,
}
@vertex
@ -50,35 +43,6 @@ fn vertex(
return VertexOutput(vec4(position, 0.0, 1.0), screenPosition, start, direction, inverseLengthSquared);
}
@fragment
fn fragment(
@location(0) screenPosition: vec2<f32>,
@location(1) @interpolate(flat) start: vec2<f32>,
@location(2) @interpolate(flat) direction: vec2<f32>,
@location(3) @interpolate(flat) inverseLengthSquared: f32
) -> @location(0) vec4<f32> {
if shouldDiscardEraserFragment(screenPosition, start, direction, inverseLengthSquared) {
discard;
}
return getEraserClearValue();
}
@fragment
fn fragmentMrt(
@location(0) screenPosition: vec2<f32>,
@location(1) @interpolate(flat) start: vec2<f32>,
@location(2) @interpolate(flat) direction: vec2<f32>,
@location(3) @interpolate(flat) inverseLengthSquared: f32
) -> EraserTextureTargets {
if shouldDiscardEraserFragment(screenPosition, start, direction, inverseLengthSquared) {
discard;
}
let cleared = getEraserClearValue();
return EraserTextureTargets(cleared, cleared, cleared);
}
@fragment
fn fragmentCombined(
@location(0) screenPosition: vec2<f32>,
@ -91,7 +55,7 @@ fn fragmentCombined(
}
let cleared = getEraserClearValue();
return EraserCombinedTargets(getEraserMaskValue(), cleared, cleared, cleared);
return EraserCombinedTargets(getEraserMaskValue(), cleared, cleared);
}
fn getEraserMaskValue() -> vec4<f32> {
@ -124,10 +88,6 @@ fn distanceSquaredFromLine(
) -> f32 {
let pa = position - start;
if inverseLengthSquared <= 0.0 {
return dot(pa, pa);
}
let q = clamp(dot(pa, direction) * inverseLengthSquared, 0.0, 1.0);
let nearestOffset = pa - direction * q;
return dot(nearestOffset, nearestOffset);
@ -152,14 +112,13 @@ fn segment_vertex_position(
}
fn segment_vertex_corner(index: u32) -> vec2<f32> {
if index == 0u {
return vec2<f32>(-1.0, 1.0);
}
if index == 1u || index == 3u {
return vec2<f32>(-1.0, -1.0);
}
if index == 2u || index == 4u {
return vec2<f32>(1.0, 1.0);
}
return vec2<f32>(1.0, -1.0);
let corners = array<vec2<f32>, 6>(
vec2<f32>(-1.0, 1.0),
vec2<f32>(-1.0, -1.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(-1.0, -1.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(1.0, -1.0),
);
return corners[index];
}

View file

@ -14,6 +14,7 @@ export class RenderPipeline {
private readonly bindGroupLayout: GPUBindGroupLayout;
private readonly pipeline: GPURenderPipeline;
private readonly noSourcePipeline: GPURenderPipeline;
private readonly uniforms: GPUBuffer;
private readonly uniformValues = new Float32Array(RenderPipeline.UNIFORM_COUNT);
private readonly uniformCache = createCachedFloat32BufferWrite(
@ -36,6 +37,7 @@ export class RenderPipeline {
const format = navigator.gpu.getPreferredCanvasFormat();
this.pipeline = this.createPipeline(format, vertex, 'fragment');
this.noSourcePipeline = this.createPipeline(format, vertex, 'fragmentNoSource');
this.uniforms = this.device.createBuffer({
size: RenderPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
@ -112,7 +114,8 @@ export class RenderPipeline {
public execute(
commandEncoder: GPUCommandEncoder,
colorTexture: GPUTextureView,
sourceTexture: GPUTextureView
sourceTexture: GPUTextureView,
useSourceTexture = true
): GPUTexture {
const bindGroup = this.getBindGroup(colorTexture, sourceTexture);
const canvasTexture = this.context.getCurrentTexture();
@ -128,7 +131,7 @@ export class RenderPipeline {
],
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(this.pipeline);
passEncoder.setPipeline(useSourceTexture ? this.pipeline : this.noSourcePipeline);
this.commonState.execute(passEncoder);
passEncoder.setBindGroup(1, bindGroup);
passEncoder.draw(3, 1);

View file

@ -27,9 +27,18 @@ fn fragment(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> {
return renderColor(traces, sources, 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);
}
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 {
let tracesMax = maxComponent(traces.rgb);
let sourcesMax = maxComponent(sources.rgb);
if max(tracesMax, sourcesMax) <= 0.0 {
return vec4(background, 1);
}
@ -38,13 +47,13 @@ fn renderColor(traces: vec4<f32>, sources: vec4<f32>, pixel: vec2<i32>) -> vec4<
clarity(traces.g),
clarity(traces.b)
);
if max(max(sources.r, sources.g), sources.b) <= 0.0 {
if sourcesMax <= 0.0 {
let traceColor =
traceStrengths.r * settings.colorA
+ traceStrengths.g * settings.colorB
+ traceStrengths.b * settings.colorC;
let normalizedTraceColor = normalizeColorIntensity(traceColor);
let traceStrength = max(max(traceStrengths.r, traceStrengths.g), traceStrengths.b);
let traceStrength = maxComponent(traceStrengths);
return vec4(mix(background, clamp(normalizedTraceColor, vec3(0), vec3(1)), traceStrength), 1);
}
@ -64,7 +73,7 @@ fn renderColor(traces: vec4<f32>, sources: vec4<f32>, pixel: vec2<i32>) -> vec4<
+ sourceStrengths.g * settings.colorB
+ sourceStrengths.b * settings.colorC;
let normalizedBrushColor = normalizeColorIntensity(brushColor);
let brushStrength = max(max(sourceStrengths.r, sourceStrengths.g), sourceStrengths.b);
let brushStrength = maxComponent(sourceStrengths);
let brushVisibility = clamp(
brushStrength * (
settings.brushColorBase +
@ -75,20 +84,27 @@ fn renderColor(traces: vec4<f32>, sources: vec4<f32>, pixel: vec2<i32>) -> vec4<
);
let color = max(normalizedTraceColor, normalizedBrushColor);
let strength = max(max(max(strengths.r, strengths.g), strengths.b), brushVisibility);
let strength = max(maxComponent(strengths), brushVisibility);
return vec4(mix(background, clamp(color, vec3(0), vec3(1)), strength), 1);
}
fn maxComponent(v: vec3<f32>) -> f32 {
return max(max(v.r, v.g), v.b);
}
fn clarity(strength: f32) -> f32 {
return pow(clamp(strength, 0, 1), settings.clarity);
}
fn normalizeColorIntensity(color: vec3<f32>) -> vec3<f32> {
let brightestChannel = max(max(color.r, color.g), color.b);
let brightestChannel = maxComponent(color);
return color / max(settings.traceNormalizationFloor, brightestChannel);
}
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;