@@ -114,23 +114,26 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
114114 }
115115 }
116116
117- let coal_target = min (params . bass * 0 .25 + activity * 2 .5 , 1 .0 );
117+ // Use exponential soft-clipping for AGC (Automatic Gain Control)
118+ // This ensures quiet tracks still generate nice flames, while brickwalled tracks smoothly approach 1.0 without hard clipping.
119+ let coal_target = 1 .0 - exp (- (params . bass * 0 .5 + activity * 3 .0 ));
118120 let current = coal_bed [x ];
119121 if (coal_target > current ) {
120122 coal_bed [x ] = current + (coal_target - current ) * 0 .18 ;
121123 } else {
122124 coal_bed [x ] = current + (coal_target - current ) * 0 .008 ;
123125 }
124126
125- let jitter = (perlin_noise (vec2 <f32 >(f32 (x ) * 0 .5 , params . time * 10 .0 )) + 1 .0 ) * 0 .5 ;
127+ // Use lower frequency noise to create wide, organic hotspots rather than single-pixel static
128+ let jitter = (perlin_noise (vec2 <f32 >(f32 (x ) * 0 .05 , params . time * 10 .0 )) + 1 .0 ) * 0 .5 ;
126129 output_grid [idx ] = min (coal_bed [x ] * (0 .7 + 0 .3 * jitter ), 1 .0 );
127130 return ;
128131 }
129132
130133 // === Hearth rows: inject coal heat ===
131134 if (y >= bottom - 2u ) {
132135 let coal_heat = min (coal_bed [x ], 1 .0 );
133- let jitter = (perlin_noise (vec2 <f32 >(f32 (x ) * 0 .5 , params . time * 10 .0 )) + 1 .0 ) * 0 .5 ;
136+ let jitter = (perlin_noise (vec2 <f32 >(f32 (x ) * 0 .05 , params . time * 10 .0 )) + 1 .0 ) * 0 .5 ;
134137
135138 if (y == bottom - 1u ) {
136139 output_grid [idx ] = coal_heat * (0 .85 + 0 .15 * jitter );
@@ -152,16 +155,31 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
152155 // Global wind swaying the fire
153156 let wind = i32 (sin (params . time * 1 .5 + f32 (x ) * 0 .003 ) * params . highs * 2 .0 );
154157
155- // Cellular Automaton: read from exactly one pixel in the row below
158+ // Classic DOS fire averaging: blend adjacent pixels to allow heat to diffuse into organic shapes
156159 let src_x = clamp (i32 (x ) + spread + wind , 0i , i32 (w ) - 1 );
157- let heat = input_grid [(y + 1u ) * w + u32 (src_x )];
160+ let xl = max (src_x - 1 , 0i );
161+ let xr = min (src_x + 1 , i32 (w ) - 1 );
162+
163+ let h1 = input_grid [(y + 1u ) * w + u32 (xl )];
164+ let h2 = input_grid [(y + 1u ) * w + u32 (src_x )];
165+ let h3 = input_grid [(y + 1u ) * w + u32 (xr )];
166+
167+ // Weighted horizontal blur favors the center pixel
168+ let heat = (h1 + h2 * 2 .0 + h3 ) / 4 .0 ;
158169
159170 // Cooling varies spatially to create uneven tips and licks
160- let cool_noise = perlin_noise (p1 * 3 .0 - vec2 <f32 >(params . time * 2 .0 , 0 .0 ));
161- let base_cooling = 0 .001 + (cool_noise + 1 .0 ) * 0 .0008 ;
171+ // Stretch the noise vertically and move it upwards to simulate rising flame tongues
172+ let p_cool = vec2 <f32 >(f32 (x ) * 0 .015 , f32 (y ) * 0 .03 );
173+ let cool_noise = perlin_noise (p_cool + vec2 <f32 >(0 .0 , params . time * 6 .0 ));
174+
175+ // Higher variance in cooling forces the flame to tear into distinct licks
176+ let base_cooling = 0 .0015 + (cool_noise + 1 .0 ) * 0 .0015 ;
162177
163178 // Occasional sparks (holes in the flame) for realism, clumped by noise
164179 let spark = select (0 .0 , 0 .06 , cool_noise > 0 .8 && y > 100u );
165180
166- output_grid [idx ] = max (heat - base_cooling * params . cooling_factor - spark , 0 .0 );
181+ // Normalize the cooling factor so heavily compressed tracks don't turn off cooling entirely
182+ let dynamic_cooling = mix (0 .6 , 1 .0 , params . cooling_factor );
183+
184+ output_grid [idx ] = max (heat - base_cooling * dynamic_cooling - spark , 0 .0 );
167185}
0 commit comments