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 shft(x: f32, size: f32) -> f32 {
return x + select(0.0, size, x < 0) + select(0.0, -size, x >= size);
}
fn sense(agent: Agent, sensorAngleOffset: f32, sense_dist: i32, size: vec2i) -> f32 {
let angle = agent.heading + sensorAngleOffset;
let dir = vec2f(cos(angle), sin(angle));
let center = vec2i(agent.pos) + vec2i(dir * f32(sense_dist));
var sum: f32 = 0.0;
for (var offsetX: i32 = -sense_dist; offsetX <= sense_dist; offsetX++) {
let px = i32(center.x + offsetX);
for (var offsetY: i32 = -sense_dist; offsetY <= sense_dist; offsetY++) {
let py = i32(center.y + offsetY);
if (px >= 0 && px< size.x && py >= 0 && py < size.y) {
let color = textureLoad(trails_in, vec2u(u32(px % ${S}), u32(py % ${S})), 0);
sum += dot(color, vec4f(agent.species_mask) * 2.0 - 1.0);
}
}
}
return sum;
}
fn next_agent_state(agent: Agent, texSize: vec2u) -> Agent {
let p = agent.pos;
let a = agent.heading;
let ra = uniforms.rotation_angle;
let step_dist = uniforms.step_distance;
let sense_dist = i32(uniforms.sensor_distance);
let size = vec2i(textureDimensions(trails_in).xy);
let C = sense(agent, 0, sense_dist, size);
let L = sense(agent, uniforms.sensor_angle, sense_dist, size);
let R = sense(agent, -uniforms.sensor_angle, sense_dist, size);
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);
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 / 4.0;
} else if (next_pos.y < 0.0) {
next_pos.y = 0.0;
next_angle += PI / 4.0;
} else if (next_pos.x >= f32(texSize.x)) {
next_pos.x = f32(texSize.x - 1);
next_angle += PI / 4.0;
} else if (next_pos.y >= f32(texSize.y)) {
next_pos.y = f32(texSize.y - 1);
next_angle += PI / 4.0;
}
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++) {
let sy = i32(texPos.y) + y;
for (var x = -blurRadius; x <= blurRadius; x++) {
let sx = i32(texPos.x) + x;
if (sx >= 0 && sx < i32(texSize.x) && sy >= 0 && sy < i32(texSize.x)) {
colorSum = colorSum + textureLoad(inputTexture, vec2i(sx, sy), 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);
}
`
})