fleeting-garden/src/pipelines/eraser/eraser-texture.wgsl
2026-05-19 21:03:53 +01:00

165 lines
4.5 KiB
WebGPU Shading Language

struct Settings {
eraserRadiusSquared: f32,
lineDistanceEpsilon: f32,
clearRed: f32,
clearGreen: f32,
clearBlue: f32,
clearAlpha: f32,
eraserRadius: f32,
padding1: f32,
};
@group(1) @binding(0) var<uniform> settings: Settings;
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@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,
}
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>,
}
@vertex
fn vertex(
@builtin(vertex_index) vertexIndex: u32,
@location(0) start: vec2<f32>,
@location(1) end: vec2<f32>
) -> VertexOutput {
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, 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>,
@location(1) @interpolate(flat) start: vec2<f32>,
@location(2) @interpolate(flat) direction: vec2<f32>,
@location(3) @interpolate(flat) inverseLengthSquared: f32
) -> EraserCombinedTargets {
if shouldDiscardEraserFragment(screenPosition, start, direction, inverseLengthSquared) {
discard;
}
let cleared = getEraserClearValue();
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> {
return vec4<f32>(
settings.clearRed,
settings.clearGreen,
settings.clearBlue,
settings.clearAlpha
);
}
fn shouldDiscardEraserFragment(
screenPosition: vec2<f32>,
start: vec2<f32>,
direction: vec2<f32>,
inverseLengthSquared: f32
) -> bool {
return distanceSquaredFromLine(screenPosition, start, direction, inverseLengthSquared) > settings.eraserRadiusSquared;
}
fn distanceSquaredFromLine(
position: vec2<f32>,
start: vec2<f32>,
direction: vec2<f32>,
inverseLengthSquared: f32
) -> 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);
}
fn segment_vertex_position(
vertexIndex: u32,
start: vec2<f32>,
end: vec2<f32>,
radius: f32
) -> vec2<f32> {
let directionVector = end - start;
let segmentLength = length(directionVector);
var direction = vec2<f32>(1.0, 0.0);
if segmentLength > 0.0 {
direction = directionVector / segmentLength;
}
let perpendicular = vec2<f32>(direction.y, -direction.x);
let corner = segment_vertex_corner(vertexIndex % 6u);
let center = mix(start, end, (corner.x + 1.0) * 0.5);
return center + direction * corner.x * radius + perpendicular * corner.y * radius;
}
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);
}