Remove alpha from deposit layer
Some checks failed
Check & deploy / build (push) Failing after 1m18s

This commit is contained in:
Andras Schmelczer 2026-08-03 07:48:44 +01:00
commit e00b394bc3
10 changed files with 45 additions and 73 deletions

View file

@ -35,10 +35,8 @@ export const defaultSettings: GardenDefaultSettings = {
randomTimeScale: 0.34816, randomTimeScale: 0.34816,
diffusionRateTrails: 0.22, diffusionRateTrails: 0.22,
decayRateBrush: 18,
diffusionDecayRateDivisor: 1000, diffusionDecayRateDivisor: 1000,
diffusionNeighborDivisor: 8, diffusionNeighborDivisor: 8,
brushDecayAlphaOffset: 1.001,
brushEffectDuration: 8, brushEffectDuration: 8,
brushCurveResolution: 12, brushCurveResolution: 12,
@ -48,7 +46,6 @@ export const defaultSettings: GardenDefaultSettings = {
brushCurveSegmentBrushRadiusRatio: 0.65, brushCurveSegmentBrushRadiusRatio: 0.65,
brushSmoothingMinSampleDistance: 0.5, brushSmoothingMinSampleDistance: 0.5,
brushAlpha: 1,
brushDiscardThreshold: 0.02, brushDiscardThreshold: 0.02,
brushGrainNoiseScale: 22, brushGrainNoiseScale: 22,
brushGrainNoiseOffsetX: 0.31, brushGrainNoiseOffsetX: 0.31,
@ -56,12 +53,12 @@ export const defaultSettings: GardenDefaultSettings = {
brushGrainMinStrength: 0.45, brushGrainMinStrength: 0.45,
brushGrainMaxStrength: 1, brushGrainMaxStrength: 1,
eraserClearAlpha: 0,
eraserClearBlue: 0, eraserClearBlue: 0,
eraserClearGreen: 0, eraserClearGreen: 0,
eraserClearRed: 0, eraserClearRed: 0,
eraserLineDistanceEpsilon: 0.0001, eraserLineDistanceEpsilon: 0.0001,
eraserMaskAlphaThreshold: 0.5, eraserMaskAlphaThreshold: 0.5,
eraserMaskErasedValue: 0,
adaptiveCapInitial: 1_000_000, adaptiveCapInitial: 1_000_000,
adaptiveCapMin: 50_000, adaptiveCapMin: 50_000,

View file

@ -28,12 +28,12 @@ export type GardenRuntimeSettings = {
brushCurveSegmentBrushRadiusRatio: number; brushCurveSegmentBrushRadiusRatio: number;
brushEffectDuration: number; brushEffectDuration: number;
brushSmoothingMinSampleDistance: number; brushSmoothingMinSampleDistance: number;
eraserClearAlpha: number;
eraserClearBlue: number; eraserClearBlue: number;
eraserClearGreen: number; eraserClearGreen: number;
eraserClearRed: number; eraserClearRed: number;
eraserLineDistanceEpsilon: number; eraserLineDistanceEpsilon: number;
eraserMaskAlphaThreshold: number; eraserMaskAlphaThreshold: number;
eraserMaskErasedValue: number;
eraserSize: number; eraserSize: number;
internalRenderAreaMegapixels: number; internalRenderAreaMegapixels: number;
mirrorSegmentCount: number; mirrorSegmentCount: number;

View file

@ -163,7 +163,7 @@ export class GameLoopResources {
eraserClearRed: runtimeSettings.eraserClearRed, eraserClearRed: runtimeSettings.eraserClearRed,
eraserClearGreen: runtimeSettings.eraserClearGreen, eraserClearGreen: runtimeSettings.eraserClearGreen,
eraserClearBlue: runtimeSettings.eraserClearBlue, eraserClearBlue: runtimeSettings.eraserClearBlue,
eraserClearAlpha: runtimeSettings.eraserClearAlpha, eraserMaskErasedValue: runtimeSettings.eraserMaskErasedValue,
}); });
} }

View file

@ -215,7 +215,8 @@ fn agent_finalize(
// Writes only this agent's last-writer-wins deposit into a per-frame-cleared // Writes only this agent's last-writer-wins deposit into a per-frame-cleared
// depositMap. Storage textures do not blend concurrent compute writes, so // depositMap. Storage textures do not blend concurrent compute writes, so
// overlapping agents intentionally collapse to whichever write wins. The // overlapping agents intentionally collapse to whichever write wins. The
// diffusion pass then sums trailMap + depositMap at tile-load time. // diffusion pass then sums trailMap + depositMap at tile-load time. The
// rgba8unorm alpha lane is unused, so deposits leave it at zero.
textureStore( textureStore(
trailMapOut, trailMapOut,
vec2<i32>(nextPosition), vec2<i32>(nextPosition),

View file

@ -18,7 +18,6 @@ import shader from './brush.wgsl?raw';
export interface BrushSettings { export interface BrushSettings {
brushSize: number; brushSize: number;
brushAlpha: number;
brushDiscardThreshold: number; brushDiscardThreshold: number;
brushGrainNoiseScale: number; brushGrainNoiseScale: number;
brushGrainNoiseOffsetX: number; brushGrainNoiseOffsetX: number;
@ -45,7 +44,6 @@ const setBrushUniformValues = (
target: Float32Array, target: Float32Array,
{ {
brushSize, brushSize,
brushAlpha,
brushDiscardThreshold, brushDiscardThreshold,
brushGrainNoiseScale, brushGrainNoiseScale,
brushGrainNoiseOffsetX, brushGrainNoiseOffsetX,
@ -65,17 +63,16 @@ const setBrushUniformValues = (
target[0] = brushRadius; target[0] = brushRadius;
target[1] = brushRadius * brushRadius; target[1] = brushRadius * brushRadius;
// target[2], target[3] are WGSL alignment padding for brushValue:vec4 — never read by the shader. // target[2], target[3] are WGSL alignment padding for brushColor:vec3 — never read by the shader.
target[4] = selectedColorIndex === 0 ? 1 : 0; target[4] = selectedColorIndex === 0 ? 1 : 0;
target[5] = selectedColorIndex === 1 ? 1 : 0; target[5] = selectedColorIndex === 1 ? 1 : 0;
target[6] = selectedColorIndex === 2 ? 1 : 0; target[6] = selectedColorIndex === 2 ? 1 : 0;
target[7] = brushAlpha; target[7] = 1 / Math.max(Number.EPSILON, brushGrainNoiseScale * safePixelRatio);
target[8] = 1 / Math.max(Number.EPSILON, brushGrainNoiseScale * safePixelRatio); target[8] = brushGrainNoiseOffsetX;
target[9] = brushGrainNoiseOffsetX; target[9] = brushGrainNoiseOffsetY;
target[10] = brushGrainNoiseOffsetY; target[10] = brushDiscardThreshold;
target[11] = brushDiscardThreshold; target[11] = brushGrainMinStrength;
target[12] = brushGrainMinStrength; target[12] = brushGrainMaxStrength;
target[13] = brushGrainMaxStrength;
}; };
export class BrushPipeline { export class BrushPipeline {

View file

@ -3,10 +3,10 @@ const SEGMENT_LENGTH_EPSILON: f32 = 0.0001;
struct Settings { struct Settings {
brushRadius: f32, brushRadius: f32,
brushRadiusSquared: f32, brushRadiusSquared: f32,
// padding to 16-byte alignment for the following vec4 // padding to 16-byte alignment for the following vec3
_pad0: f32, _pad0: f32,
_pad1: f32, _pad1: f32,
brushValue: vec4<f32>, brushColor: vec3<f32>,
brushGrainNoiseScale: f32, brushGrainNoiseScale: f32,
brushGrainNoiseOffsetX: f32, brushGrainNoiseOffsetX: f32,
brushGrainNoiseOffsetY: f32, brushGrainNoiseOffsetY: f32,
@ -115,5 +115,7 @@ fn brushStrength(
} }
fn brushOutput(strength: f32) -> vec4<f32> { fn brushOutput(strength: f32) -> vec4<f32> {
return vec4(settings.brushValue.rgb * strength, settings.brushValue.a * strength); // The rgba8unorm source map needs a vec4; its alpha lane is unused, so the
// brush leaves it at zero.
return vec4(settings.brushColor * strength, 0.0);
} }

View file

@ -2,11 +2,7 @@ struct Settings {
inverseDiffusionRateTrails: f32, inverseDiffusionRateTrails: f32,
decayRateTrails: f32, decayRateTrails: f32,
diffusionNeighborScale: f32, diffusionNeighborScale: f32,
brushDecayAlphaMultiplier: f32,
brushDecayAlphaSubtract: f32,
padding0: f32, padding0: f32,
padding1: f32,
padding2: f32,
}; };
const WORKGROUP_SIZE_X = __WORKGROUP_SIZE__u; const WORKGROUP_SIZE_X = __WORKGROUP_SIZE__u;
@ -32,7 +28,9 @@ const HASH_TO_UNIT_FLOAT: f32 = 2.3283064365386963e-10;
// in the same frame. // in the same frame.
@group(0) @binding(3) var depositMap: texture_2d<f32>; @group(0) @binding(3) var depositMap: texture_2d<f32>;
var<workgroup> tile: array<vec4<f32>, TILE_TEXEL_COUNT>; // Only the color channels take part in the simulation; the alpha lane of the
// rgba8unorm trail and source maps is unused and always stored as zero.
var<workgroup> tile: array<vec3<f32>, TILE_TEXEL_COUNT>;
var<workgroup> tileTrailStrength: array<f32, TILE_TEXEL_COUNT>; var<workgroup> tileTrailStrength: array<f32, TILE_TEXEL_COUNT>;
@compute @workgroup_size(__WORKGROUP_SIZE__, __WORKGROUP_SIZE__) @compute @workgroup_size(__WORKGROUP_SIZE__, __WORKGROUP_SIZE__)
@ -53,10 +51,10 @@ fn main(
vec2<i32>(0, 0), vec2<i32>(0, 0),
textureBound textureBound
); );
let texel = textureLoad(trailMap, sourcePixel, 0) let texel = textureLoad(trailMap, sourcePixel, 0).rgb
+ textureLoad(depositMap, sourcePixel, 0); + textureLoad(depositMap, sourcePixel, 0).rgb;
tile[tileIndex] = texel; tile[tileIndex] = texel;
tileTrailStrength[tileIndex] = length(texel.rgb); tileTrailStrength[tileIndex] = length(texel);
} }
workgroupBarrier(); workgroupBarrier();
@ -106,25 +104,23 @@ fn main(
+ propagate_value(nBR, sBR, current, trailWeight); + propagate_value(nBR, sBR, current, trailWeight);
let updated = current + propagated * settings.diffusionNeighborScale; let updated = current + propagated * settings.diffusionNeighborScale;
let decayed = clamp(vec4( let decayed = clamp(
updated.rgb * settings.decayRateTrails - vec3(TRAIL_RGB_DECAY_SUBTRACT), updated * settings.decayRateTrails - vec3(TRAIL_RGB_DECAY_SUBTRACT),
updated.a * settings.brushDecayAlphaMultiplier - settings.brushDecayAlphaSubtract vec3(0),
), vec4(0), vec4(1)); vec3(1)
);
textureStore(trailMapOut, pixel, decayed); textureStore(trailMapOut, pixel, vec4(decayed, 0.0));
} }
fn propagate_value( fn propagate_value(
neighbour: vec4<f32>, neighbour: vec3<f32>,
neighbourStrength: f32, neighbourStrength: f32,
current: vec4<f32>, current: vec3<f32>,
trailWeight: f32 trailWeight: f32
) -> vec4<f32> { ) -> vec3<f32> {
let difference = clamp(neighbour - current, vec4(0), vec4(1)); let difference = clamp(neighbour - current, vec3(0), vec3(1));
return vec4( return vec3(neighbourStrength * trailWeight) * difference;
vec3(neighbourStrength * trailWeight),
neighbour.a * trailWeight
) * difference;
} }
fn random_from_pixel(pixel: vec2<i32>) -> f32 { fn random_from_pixel(pixel: vec2<i32>) -> f32 {

View file

@ -12,22 +12,10 @@ import shader from './diffuse.wgsl?raw';
export interface DiffusionSettings { export interface DiffusionSettings {
diffusionRateTrails: number; diffusionRateTrails: number;
decayRateTrails: number; decayRateTrails: number;
decayRateBrush: number;
diffusionDecayRateDivisor: number; diffusionDecayRateDivisor: number;
diffusionNeighborDivisor: number; diffusionNeighborDivisor: number;
brushDecayAlphaOffset: number;
} }
type DiffusionUniformSettings = Pick<
DiffusionSettings,
| 'diffusionRateTrails'
| 'decayRateTrails'
| 'decayRateBrush'
| 'diffusionDecayRateDivisor'
| 'diffusionNeighborDivisor'
| 'brushDecayAlphaOffset'
>;
const MIN_DIFFUSION_RATE = 0.000001; const MIN_DIFFUSION_RATE = 0.000001;
const getSafeInverseDiffusionRate = (diffusionRate: number): number => const getSafeInverseDiffusionRate = (diffusionRate: number): number =>
@ -41,30 +29,23 @@ const setDiffusionUniformValues = (
{ {
diffusionRateTrails, diffusionRateTrails,
decayRateTrails, decayRateTrails,
decayRateBrush,
diffusionDecayRateDivisor, diffusionDecayRateDivisor,
diffusionNeighborDivisor, diffusionNeighborDivisor,
brushDecayAlphaOffset, }: DiffusionSettings
}: DiffusionUniformSettings
): void => { ): void => {
const decayDivisor = Math.max(Number.EPSILON, diffusionDecayRateDivisor); const decayDivisor = Math.max(Number.EPSILON, diffusionDecayRateDivisor);
const brushDecayRate = decayRateBrush / decayDivisor;
const neighborDivisor = Number.isFinite(diffusionNeighborDivisor) const neighborDivisor = Number.isFinite(diffusionNeighborDivisor)
? Math.max(1, diffusionNeighborDivisor) ? Math.max(1, diffusionNeighborDivisor)
: 1; : 1;
target[0] = getSafeInverseDiffusionRate(diffusionRateTrails); target[0] = getSafeInverseDiffusionRate(diffusionRateTrails);
target[1] = decayRateTrails / decayDivisor; target[1] = decayRateTrails / decayDivisor;
target[2] = 1 / neighborDivisor; target[2] = 1 / neighborDivisor;
target[3] = 1 + brushDecayRate; // target[3] is WGSL 16-byte alignment padding — never read by the shader.
target[4] = brushDecayAlphaOffset * brushDecayRate;
target[5] = 0;
target[6] = 0;
target[7] = 0;
}; };
export class DiffusionPipeline { export class DiffusionPipeline {
private static readonly WORKGROUP_SIZE = 16; private static readonly WORKGROUP_SIZE = 16;
private static readonly UNIFORM_COUNT = 8; private static readonly UNIFORM_COUNT = 4;
private readonly bindGroupLayout: GPUBindGroupLayout; private readonly bindGroupLayout: GPUBindGroupLayout;
private readonly pipeline: GPUComputePipeline; private readonly pipeline: GPUComputePipeline;
@ -136,18 +117,14 @@ export class DiffusionPipeline {
public setParameters({ public setParameters({
diffusionRateTrails, diffusionRateTrails,
decayRateTrails, decayRateTrails,
decayRateBrush,
diffusionDecayRateDivisor, diffusionDecayRateDivisor,
diffusionNeighborDivisor, diffusionNeighborDivisor,
brushDecayAlphaOffset,
}: DiffusionSettings) { }: DiffusionSettings) {
setDiffusionUniformValues(this.uniformValues, { setDiffusionUniformValues(this.uniformValues, {
diffusionRateTrails, diffusionRateTrails,
decayRateTrails, decayRateTrails,
decayRateBrush,
diffusionDecayRateDivisor, diffusionDecayRateDivisor,
diffusionNeighborDivisor, diffusionNeighborDivisor,
brushDecayAlphaOffset,
}); });
writeBufferIfChanged( writeBufferIfChanged(
this.device, this.device,

View file

@ -24,7 +24,7 @@ interface EraserTextureParameters {
eraserClearRed: number; eraserClearRed: number;
eraserClearGreen: number; eraserClearGreen: number;
eraserClearBlue: number; eraserClearBlue: number;
eraserClearAlpha: number; eraserMaskErasedValue: number;
} }
const UNIFORM_COUNT = 8; const UNIFORM_COUNT = 8;
@ -110,7 +110,7 @@ export class EraserTexturePipeline {
eraserClearRed, eraserClearRed,
eraserClearGreen, eraserClearGreen,
eraserClearBlue, eraserClearBlue,
eraserClearAlpha, eraserMaskErasedValue,
}: EraserTextureParameters): void { }: EraserTextureParameters): void {
const eraserRadius = eraserSize / 2; const eraserRadius = eraserSize / 2;
@ -119,7 +119,7 @@ export class EraserTexturePipeline {
this.uniformValues[2] = eraserClearRed; this.uniformValues[2] = eraserClearRed;
this.uniformValues[3] = eraserClearGreen; this.uniformValues[3] = eraserClearGreen;
this.uniformValues[4] = eraserClearBlue; this.uniformValues[4] = eraserClearBlue;
this.uniformValues[5] = eraserClearAlpha; this.uniformValues[5] = eraserMaskErasedValue;
this.uniformValues[6] = eraserRadius; this.uniformValues[6] = eraserRadius;
writeBufferIfChanged( writeBufferIfChanged(
this.device, this.device,

View file

@ -4,7 +4,7 @@ struct Settings {
clearRed: f32, clearRed: f32,
clearGreen: f32, clearGreen: f32,
clearBlue: f32, clearBlue: f32,
clearAlpha: f32, erasedMaskValue: f32,
eraserRadius: f32, eraserRadius: f32,
}; };
@ -66,14 +66,16 @@ fn fragmentCombined(
} }
fn getEraserMaskValue() -> vec4<f32> { fn getEraserMaskValue() -> vec4<f32> {
return vec4<f32>(settings.clearAlpha, 0.0, 0.0, 1.0); return vec4<f32>(settings.erasedMaskValue, 0.0, 0.0, 1.0);
} }
fn getEraserClearValue() -> vec4<f32> { fn getEraserClearValue() -> vec4<f32> {
// The rgba8unorm trail and source maps need a vec4; their alpha lane is
// unused, so erased texels keep it at zero.
return vec4<f32>( return vec4<f32>(
settings.clearRed, settings.clearRed,
settings.clearGreen, settings.clearGreen,
settings.clearBlue, settings.clearBlue,
settings.clearAlpha 0.0
); );
} }