diff --git a/src/config/default-settings.ts b/src/config/default-settings.ts index ba59a1e..f98a3ea 100644 --- a/src/config/default-settings.ts +++ b/src/config/default-settings.ts @@ -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, diff --git a/src/config/types.ts b/src/config/types.ts index cd94c9a..efbab4a 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -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; diff --git a/src/game-loop/game-loop-resources.ts b/src/game-loop/game-loop-resources.ts index a380c23..9b0653c 100644 --- a/src/game-loop/game-loop-resources.ts +++ b/src/game-loop/game-loop-resources.ts @@ -163,7 +163,7 @@ export class GameLoopResources { eraserClearRed: runtimeSettings.eraserClearRed, eraserClearGreen: runtimeSettings.eraserClearGreen, eraserClearBlue: runtimeSettings.eraserClearBlue, - eraserClearAlpha: runtimeSettings.eraserClearAlpha, + eraserMaskErasedValue: runtimeSettings.eraserMaskErasedValue, }); } diff --git a/src/pipelines/agents/agent.wgsl b/src/pipelines/agents/agent.wgsl index 912e2a0..cdc9143 100644 --- a/src/pipelines/agents/agent.wgsl +++ b/src/pipelines/agents/agent.wgsl @@ -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(nextPosition), diff --git a/src/pipelines/brush/brush-pipeline.ts b/src/pipelines/brush/brush-pipeline.ts index 4897020..4051b3c 100644 --- a/src/pipelines/brush/brush-pipeline.ts +++ b/src/pipelines/brush/brush-pipeline.ts @@ -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 { diff --git a/src/pipelines/brush/brush.wgsl b/src/pipelines/brush/brush.wgsl index 8152986..1a3427f 100644 --- a/src/pipelines/brush/brush.wgsl +++ b/src/pipelines/brush/brush.wgsl @@ -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, + brushColor: vec3, brushGrainNoiseScale: f32, brushGrainNoiseOffsetX: f32, brushGrainNoiseOffsetY: f32, @@ -115,5 +115,7 @@ fn brushStrength( } fn brushOutput(strength: f32) -> vec4 { - 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); } diff --git a/src/pipelines/diffusion/diffuse.wgsl b/src/pipelines/diffusion/diffuse.wgsl index 92ae448..3f3a772 100644 --- a/src/pipelines/diffusion/diffuse.wgsl +++ b/src/pipelines/diffusion/diffuse.wgsl @@ -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; -var tile: array, 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 tile: array, TILE_TEXEL_COUNT>; var tileTrailStrength: array; @compute @workgroup_size(__WORKGROUP_SIZE__, __WORKGROUP_SIZE__) @@ -53,10 +51,10 @@ fn main( vec2(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, + neighbour: vec3, neighbourStrength: f32, - current: vec4, + current: vec3, trailWeight: f32 -) -> vec4 { - let difference = clamp(neighbour - current, vec4(0), vec4(1)); - return vec4( - vec3(neighbourStrength * trailWeight), - neighbour.a * trailWeight - ) * difference; +) -> vec3 { + let difference = clamp(neighbour - current, vec3(0), vec3(1)); + return vec3(neighbourStrength * trailWeight) * difference; } fn random_from_pixel(pixel: vec2) -> f32 { diff --git a/src/pipelines/diffusion/diffusion-pipeline.ts b/src/pipelines/diffusion/diffusion-pipeline.ts index ef9bfef..94b82f5 100644 --- a/src/pipelines/diffusion/diffusion-pipeline.ts +++ b/src/pipelines/diffusion/diffusion-pipeline.ts @@ -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, diff --git a/src/pipelines/eraser/eraser-texture-pipeline.ts b/src/pipelines/eraser/eraser-texture-pipeline.ts index cf3e84a..167e970 100644 --- a/src/pipelines/eraser/eraser-texture-pipeline.ts +++ b/src/pipelines/eraser/eraser-texture-pipeline.ts @@ -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, diff --git a/src/pipelines/eraser/eraser-texture.wgsl b/src/pipelines/eraser/eraser-texture.wgsl index 635258e..0d95654 100644 --- a/src/pipelines/eraser/eraser-texture.wgsl +++ b/src/pipelines/eraser/eraser-texture.wgsl @@ -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 { - return vec4(settings.clearAlpha, 0.0, 0.0, 1.0); + return vec4(settings.erasedMaskValue, 0.0, 0.0, 1.0); } fn getEraserClearValue() -> vec4 { + // The rgba8unorm trail and source maps need a vec4; their alpha lane is + // unused, so erased texels keep it at zero. return vec4( settings.clearRed, settings.clearGreen, settings.clearBlue, - settings.clearAlpha + 0.0 ); }