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

View file

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

View file

@ -163,7 +163,7 @@ export class GameLoopResources {
eraserClearRed: runtimeSettings.eraserClearRed,
eraserClearGreen: runtimeSettings.eraserClearGreen,
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
// depositMap. Storage textures do not blend concurrent compute writes, so
// 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(
trailMapOut,
vec2<i32>(nextPosition),

View file

@ -18,7 +18,6 @@ import shader from './brush.wgsl?raw';
export interface BrushSettings {
brushSize: number;
brushAlpha: number;
brushDiscardThreshold: number;
brushGrainNoiseScale: number;
brushGrainNoiseOffsetX: number;
@ -45,7 +44,6 @@ const setBrushUniformValues = (
target: Float32Array,
{
brushSize,
brushAlpha,
brushDiscardThreshold,
brushGrainNoiseScale,
brushGrainNoiseOffsetX,
@ -65,17 +63,16 @@ const setBrushUniformValues = (
target[0] = 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[5] = selectedColorIndex === 1 ? 1 : 0;
target[6] = selectedColorIndex === 2 ? 1 : 0;
target[7] = brushAlpha;
target[8] = 1 / Math.max(Number.EPSILON, brushGrainNoiseScale * safePixelRatio);
target[9] = brushGrainNoiseOffsetX;
target[10] = brushGrainNoiseOffsetY;
target[11] = brushDiscardThreshold;
target[12] = brushGrainMinStrength;
target[13] = brushGrainMaxStrength;
target[7] = 1 / Math.max(Number.EPSILON, brushGrainNoiseScale * safePixelRatio);
target[8] = brushGrainNoiseOffsetX;
target[9] = brushGrainNoiseOffsetY;
target[10] = brushDiscardThreshold;
target[11] = brushGrainMinStrength;
target[12] = brushGrainMaxStrength;
};
export class BrushPipeline {

View file

@ -3,10 +3,10 @@ const SEGMENT_LENGTH_EPSILON: f32 = 0.0001;
struct Settings {
brushRadius: f32,
brushRadiusSquared: f32,
// padding to 16-byte alignment for the following vec4
// padding to 16-byte alignment for the following vec3
_pad0: f32,
_pad1: f32,
brushValue: vec4<f32>,
brushColor: vec3<f32>,
brushGrainNoiseScale: f32,
brushGrainNoiseOffsetX: f32,
brushGrainNoiseOffsetY: f32,
@ -115,5 +115,7 @@ fn brushStrength(
}
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,
decayRateTrails: f32,
diffusionNeighborScale: f32,
brushDecayAlphaMultiplier: f32,
brushDecayAlphaSubtract: f32,
padding0: f32,
padding1: f32,
padding2: f32,
};
const WORKGROUP_SIZE_X = __WORKGROUP_SIZE__u;
@ -32,7 +28,9 @@ const HASH_TO_UNIT_FLOAT: f32 = 2.3283064365386963e-10;
// in the same frame.
@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>;
@compute @workgroup_size(__WORKGROUP_SIZE__, __WORKGROUP_SIZE__)
@ -53,10 +51,10 @@ fn main(
vec2<i32>(0, 0),
textureBound
);
let texel = textureLoad(trailMap, sourcePixel, 0)
+ textureLoad(depositMap, sourcePixel, 0);
let texel = textureLoad(trailMap, sourcePixel, 0).rgb
+ textureLoad(depositMap, sourcePixel, 0).rgb;
tile[tileIndex] = texel;
tileTrailStrength[tileIndex] = length(texel.rgb);
tileTrailStrength[tileIndex] = length(texel);
}
workgroupBarrier();
@ -106,25 +104,23 @@ fn main(
+ propagate_value(nBR, sBR, current, trailWeight);
let updated = current + propagated * settings.diffusionNeighborScale;
let decayed = clamp(vec4(
updated.rgb * settings.decayRateTrails - vec3(TRAIL_RGB_DECAY_SUBTRACT),
updated.a * settings.brushDecayAlphaMultiplier - settings.brushDecayAlphaSubtract
), vec4(0), vec4(1));
let decayed = clamp(
updated * settings.decayRateTrails - vec3(TRAIL_RGB_DECAY_SUBTRACT),
vec3(0),
vec3(1)
);
textureStore(trailMapOut, pixel, decayed);
textureStore(trailMapOut, pixel, vec4(decayed, 0.0));
}
fn propagate_value(
neighbour: vec4<f32>,
neighbour: vec3<f32>,
neighbourStrength: f32,
current: vec4<f32>,
current: vec3<f32>,
trailWeight: f32
) -> vec4<f32> {
let difference = clamp(neighbour - current, vec4(0), vec4(1));
return vec4(
vec3(neighbourStrength * trailWeight),
neighbour.a * trailWeight
) * difference;
) -> vec3<f32> {
let difference = clamp(neighbour - current, vec3(0), vec3(1));
return vec3(neighbourStrength * trailWeight) * difference;
}
fn random_from_pixel(pixel: vec2<i32>) -> f32 {

View file

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

View file

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

View file

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