shaders = ({
agent: `
const PI: f32 = 3.141592653589793;
const TWO_PI: f32 = 6.283185307179586;
struct Uniforms {
time: f32,
sensor_angle: f32,
sensor_distance: f32,
rotation_angle: f32,
step_distance: f32,
decay_rate: f32,
deposit_amount: f32,
blur_strength: f32,
}
struct Agent {
pos: vec2f, // offset = 0, size = 8, alignment = 8
_pad: f32, // offset = 8, size = 4, alignment = 4 (to align next f32)
heading: f32, // offset = 12, size = 4, alignment = 4
species_mask: vec4f, // offset = 16, size = 16, alignment = 16
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var<storage> agentsIn: array<Agent>;
@group(0) @binding(2) var<storage, read_write> agentsOut: array<Agent>;
@group(0) @binding(3) var<storage, read_write> trailCounts: array<atomic<u32>>;
@group(0) @binding(4) var trails_in: texture_2d<f32>;
fn grid_index_from_agent_pos(pos: vec2u) -> u32 {
return (pos.x % ${S}) + (pos.y % ${S}) * ${S};
}
// On generating random numbers, with help of y= [(a+x)sin(bx)] mod 1", W.J.J. Rey,
// 22nd European Meeting of Statisticians 1998
fn rand11(n: f32) -> f32 { return fract(sin(n) * 43758.5453123); }
fn rand22(n: vec2f) -> f32 { return fract(sin(dot(n, vec2f(12.9898, 4.1414))) * 43758.5453); }
// fn hash11(_p: f32) -> f32 {
// var p = fract(_p * .1031);
// p *= p + 33.33;
// p *= p + p;
// return fract(p);
// }
// fn hash12(p: vec2f) -> f32 {
// var p3 = fract(vec3f(p.xyx) * .1031);
// p3 += dot(p3, p3.yzx + 33.33);
// return fract((p3.x + p3.y) * p3.z);
// }
// fn hash13(_p3: vec3f) -> f32 {
// var p3 = fract(_p3 * .1031);
// p3 += dot(p3, p3.zyx + 31.32);
// return fract((p3.x + p3.y) * p3.z);
// }
fn shft(x: f32, size: f32) -> f32 {
return x + select(0.0, size, x < 0) + select(0.0, -size, x >= size);
}
fn next_agent_state(agent: Agent, texSize: vec2u) -> Agent {
let p = agent.pos;
let a = agent.heading;
let sa = uniforms.sensor_angle;
let sd = uniforms.sensor_distance;
let ra = uniforms.rotation_angle;
let step_dist = uniforms.step_distance;
let xc = p.x + cos(a) * sd;
let yc = p.y + sin(a) * sd;
let xl = p.x + cos(a - sa) * sd;
let yl = p.y + sin(a - sa) * sd;
let xr = p.x + cos(a + sa) * sd;
let yr = p.y + sin(a + sa) * sd;
// good enough
let C = textureLoad(trails_in, vec2u(u32(xc % ${S}), u32(yc % ${S})), 0).r;
let L = textureLoad(trails_in, vec2u(u32(xl % ${S}), u32(yl % ${S})), 0).r;
let R = textureLoad(trails_in, vec2u(u32(xr % ${S}), u32(yr % ${S})), 0).r;
var d = 0.0;
if (C > L && C > R) {
d = 0.0;
} else if (C < L && C < R) {
d = select(-1.0, 1.0, rand11(uniforms.time) > 0.5);
} else if (L < R) {
d = 1.0;
} else if (R < L) {
d = -1.0;
}
var rnd = rand11(uniforms.time) * (0.3 * TWO_PI) * select(-1.0, 1.0, rand11(uniforms.time) > 0.5);
// better random:
// var rnd = hash13(vec3f(p.x / ${S}, p.y / ${S}, uniforms.time)) * (0.01 * TWO_PI) * select(-1.0, 1.0, hash11(uniforms.time) > 0.5);
let da = ra * d;
var next_angle = shft(a + da + rnd, TWO_PI);
var next_pos = vec2f(
p.x + cos(next_angle) * step_dist,
p.y + sin(next_angle) * step_dist,
);
// TODO optimize
if (next_pos.x < 0.0) {
next_pos.x = 0.0;
next_angle += PI; // + (rand11(rnd) - 0.5) * 0.1;
} else if (next_pos.y < 0.0) {
next_pos.y = 0.0;
next_angle += PI;
} else if (next_pos.x >= f32(texSize.x)) {
next_pos.x = f32(texSize.x - 1);
next_angle += PI;
} else if (next_pos.y >= f32(texSize.y)) {
next_pos.y = f32(texSize.y - 1);
next_angle += PI;
}
return Agent(next_pos, 0, next_angle % TWO_PI, vec4f(0));
}
@compute
@workgroup_size(64, 1, 1)
fn cs(
@builtin(global_invocation_id) gid: vec3u,
@builtin(local_invocation_id) lid: vec3u,
) {
let agent_index = gid.x;
if (agent_index > arrayLength(&agentsIn)) {
return;
}
let agent = agentsIn[agent_index];
let texSize = textureDimensions(trails_in);
let next_agent = next_agent_state(agent, texSize);
agentsOut[agent_index].pos = next_agent.pos;
agentsOut[agent_index].heading = next_agent.heading;
// add to trail counts
let trail_index = grid_index_from_agent_pos(vec2u(u32(next_agent.pos.x), u32(next_agent.pos.y)));
atomicAdd(&trailCounts[trail_index], 1);
}
`,
trailCombine: `
struct Uniforms {
time: f32,
sensor_angle: f32,
sensor_distance: f32,
rotation_angle: f32,
step_distance: f32,
decay_rate: f32,
deposit_amount: f32,
blur_strength: f32,
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(3) var<storage, read_write> trailCounts: array<atomic<u32>>;
@group(0) @binding(4) var inputTexture: texture_2d<f32>;
@group(0) @binding(5) var outputTexture: texture_storage_2d<rgba8unorm, write>;
const tileSize: vec2u = vec2u(16, 16);
const blurRadius: i32 = 1;
const count: f32 = pow(f32(blurRadius) * 2 + 1, 2);
@compute
@workgroup_size(16, 16, 1)
fn cs(@builtin(global_invocation_id) global_id: vec3u) {
let texSize: vec2u = textureDimensions(inputTexture).xy;
let tileNW: vec2u = (global_id.xy / tileSize) * tileSize;
let localPos: vec2u = global_id.xy % tileSize;
let texPos: vec2u = tileNW + localPos;
let index = texPos.x + texPos.y * texSize.x;
if (texPos.x < texSize.x && texPos.y < texSize.y) {
var colorSum: vec4f = vec4f(0.0, 0.0, 0.0, 0.0);
for (var y = -blurRadius; y <= blurRadius; y++) {
for (var x = -blurRadius; x <= blurRadius; x++) {
let samplePos = vec2i(texPos) + vec2i(x, y);
if (
samplePos.x >= 0 && samplePos.x < i32(texSize.x) &&
samplePos.y >= 0 && samplePos.y < i32(texSize.y)
) {
colorSum = colorSum + textureLoad(inputTexture, samplePos, 0);
}
}
}
let blurred: vec4f = colorSum / count;
let count = f32(atomicLoad(&trailCounts[index])) * uniforms.deposit_amount;
var color = blurred - vec4f(uniforms.decay_rate) + count;
color = clamp(color, vec4f(0), vec4f(1));
textureStore(outputTexture, texPos, color);
}
// reset
atomicStore(&trailCounts[index], 0);
}
`
})