This commit is contained in:
Andras Schmelczer 2026-05-19 21:03:53 +01:00
commit 7c70f15e49
65 changed files with 1122 additions and 1906 deletions

View file

@ -14,6 +14,7 @@ export class EraserAgentPipeline {
private readonly pipeline: GPUComputePipeline;
private readonly uniforms: GPUBuffer;
private readonly uniformValues = new Float32Array(EraserAgentPipeline.UNIFORM_COUNT);
private readonly uniformUintValues = new Uint32Array(this.uniformValues.buffer);
private readonly uniformCache = createCachedFloat32BufferWrite(
EraserAgentPipeline.UNIFORM_COUNT
);
@ -93,7 +94,7 @@ export class EraserAgentPipeline {
this.activeSegmentCount = this.pendingSegmentCount;
this.pendingSegmentCount = 0;
this.uniformValues[0] = agentCount;
this.uniformUintValues[0] = Math.max(0, Math.floor(agentCount));
this.uniformValues[1] = eraserMaskAlphaThreshold;
this.uniformValues[2] = 0;
this.uniformValues[3] = 0;

View file

@ -1,5 +1,5 @@
struct Settings {
agentCount: f32,
agentCount: u32,
eraserMaskAlphaThreshold: f32,
padding1: f32,
padding2: f32,
@ -15,7 +15,7 @@ fn main(
) {
let id = get_id(global_id, num_workgroups);
if id >= u32(settings.agentCount) {
if id >= settings.agentCount {
return;
}
@ -32,7 +32,7 @@ fn main(
);
let maskSample = textureLoad(eraserMask, maskPosition, 0);
if maskSample.a < settings.eraserMaskAlphaThreshold {
if maskSample.r < settings.eraserMaskAlphaThreshold {
agents[id].colorIndex = -1.0;
}
}

View file

@ -64,7 +64,12 @@ export class EraserTexturePipeline {
});
const shaderModule = smartCompile(device, CommonState.shaderCode, shader);
this.combinedPipeline = this.createPipeline(shaderModule, 'fragmentCombined', 4);
this.combinedPipeline = this.createPipeline(shaderModule, 'fragmentCombined', [
'r8unorm',
'rgba16float',
'rgba16float',
'rgba16float',
]);
this.uniforms = this.device.createBuffer({
size: EraserTexturePipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
@ -119,7 +124,7 @@ export class EraserTexturePipeline {
this.uniformValues[3] = eraserClearGreen;
this.uniformValues[4] = eraserClearBlue;
this.uniformValues[5] = eraserClearAlpha;
this.uniformValues[6] = 0;
this.uniformValues[6] = eraserRadius;
this.uniformValues[7] = 0;
writeFloat32BufferIfChanged(
this.device,
@ -225,7 +230,7 @@ export class EraserTexturePipeline {
private createPipeline(
shaderModule: GPUShaderModule,
fragmentEntryPoint: string,
colorTargetCount: number
targetFormats: Array<GPUTextureFormat>
): GPURenderPipeline {
return this.device.createRenderPipeline({
layout: this.device.createPipelineLayout({
@ -258,9 +263,7 @@ export class EraserTexturePipeline {
fragment: {
module: shaderModule,
entryPoint: fragmentEntryPoint,
targets: Array.from({ length: colorTargetCount }, () => ({
format: 'rgba16float' as const,
})),
targets: targetFormats.map((format) => ({ format })),
},
primitive: {
topology: 'triangle-list',

View file

@ -5,7 +5,7 @@ struct Settings {
clearGreen: f32,
clearBlue: f32,
clearAlpha: f32,
padding0: f32,
eraserRadius: f32,
padding1: f32,
};
@ -15,7 +15,8 @@ struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) screenPosition: vec2<f32>,
@location(1) @interpolate(flat) start: vec2<f32>,
@location(2) @interpolate(flat) end: vec2<f32>
@location(2) @interpolate(flat) direction: vec2<f32>,
@location(3) @interpolate(flat) inverseLengthSquared: f32,
}
struct EraserTextureTargets {
@ -37,19 +38,26 @@ fn vertex(
@location(0) start: vec2<f32>,
@location(1) end: vec2<f32>
) -> VertexOutput {
let screenPosition = segment_vertex_position(vertexIndex, start, end, sqrt(settings.eraserRadiusSquared));
let direction = end - start;
let denominator = dot(direction, direction);
var inverseLengthSquared = 0.0;
if denominator > settings.lineDistanceEpsilon {
inverseLengthSquared = 1.0 / denominator;
}
let screenPosition = segment_vertex_position(vertexIndex, start, end, settings.eraserRadius);
let uv = screenPosition / state.size;
let position = vec2(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);
return VertexOutput(vec4(position, 0.0, 1.0), screenPosition, start, end);
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) end: vec2<f32>
@location(2) @interpolate(flat) direction: vec2<f32>,
@location(3) @interpolate(flat) inverseLengthSquared: f32
) -> @location(0) vec4<f32> {
if shouldDiscardEraserFragment(screenPosition, start, end) {
if shouldDiscardEraserFragment(screenPosition, start, direction, inverseLengthSquared) {
discard;
}
@ -60,9 +68,10 @@ fn fragment(
fn fragmentMrt(
@location(0) screenPosition: vec2<f32>,
@location(1) @interpolate(flat) start: vec2<f32>,
@location(2) @interpolate(flat) end: vec2<f32>
@location(2) @interpolate(flat) direction: vec2<f32>,
@location(3) @interpolate(flat) inverseLengthSquared: f32
) -> EraserTextureTargets {
if shouldDiscardEraserFragment(screenPosition, start, end) {
if shouldDiscardEraserFragment(screenPosition, start, direction, inverseLengthSquared) {
discard;
}
@ -74,14 +83,19 @@ fn fragmentMrt(
fn fragmentCombined(
@location(0) screenPosition: vec2<f32>,
@location(1) @interpolate(flat) start: vec2<f32>,
@location(2) @interpolate(flat) end: vec2<f32>
@location(2) @interpolate(flat) direction: vec2<f32>,
@location(3) @interpolate(flat) inverseLengthSquared: f32
) -> EraserCombinedTargets {
if shouldDiscardEraserFragment(screenPosition, start, end) {
if shouldDiscardEraserFragment(screenPosition, start, direction, inverseLengthSquared) {
discard;
}
let cleared = getEraserClearValue();
return EraserCombinedTargets(cleared, cleared, cleared, cleared);
return EraserCombinedTargets(getEraserMaskValue(), cleared, cleared, cleared);
}
fn getEraserMaskValue() -> vec4<f32> {
return vec4<f32>(settings.clearAlpha, 0.0, 0.0, 1.0);
}
fn getEraserClearValue() -> vec4<f32> {
@ -96,21 +110,25 @@ fn getEraserClearValue() -> vec4<f32> {
fn shouldDiscardEraserFragment(
screenPosition: vec2<f32>,
start: vec2<f32>,
end: vec2<f32>
direction: vec2<f32>,
inverseLengthSquared: f32
) -> bool {
return distanceSquaredFromLine(screenPosition, start, end) > settings.eraserRadiusSquared;
return distanceSquaredFromLine(screenPosition, start, direction, inverseLengthSquared) > settings.eraserRadiusSquared;
}
fn distanceSquaredFromLine(position: vec2<f32>, start: vec2<f32>, end: vec2<f32>) -> f32 {
fn distanceSquaredFromLine(
position: vec2<f32>,
start: vec2<f32>,
direction: vec2<f32>,
inverseLengthSquared: f32
) -> f32 {
let pa = position - start;
let direction = end - start;
let denominator = dot(direction, direction);
if denominator <= settings.lineDistanceEpsilon {
if inverseLengthSquared <= 0.0 {
return dot(pa, pa);
}
let q = clamp(dot(pa, direction) / denominator, 0.0, 1.0);
let q = clamp(dot(pa, direction) * inverseLengthSquared, 0.0, 1.0);
let nearestOffset = pa - direction * q;
return dot(nearestOffset, nearestOffset);
}