This commit is contained in:
parent
9cf8f73e18
commit
5cc94805f1
17 changed files with 672 additions and 239 deletions
140
src/pipelines/agents/agent-pipeline.ts
Normal file
140
src/pipelines/agents/agent-pipeline.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import { AGENT_SIZE, Agent } from './agent';
|
||||
import shader from './agent.wgsl';
|
||||
|
||||
export class AgentPipeline {
|
||||
private static readonly WORKGROUP_SIZE = 64;
|
||||
private static readonly UNIFORM_COUNT = 10;
|
||||
|
||||
private readonly pipeline: GPUComputePipeline;
|
||||
private readonly uniforms: GPUBuffer;
|
||||
private readonly agentsBuffer: GPUBuffer;
|
||||
private bindGroup?: GPUBindGroup;
|
||||
private previousTrailMapIn?: GPUTexture;
|
||||
private previousTrailMapOut?: GPUTexture;
|
||||
|
||||
public constructor(private readonly device: GPUDevice, agents: Array<Agent>) {
|
||||
this.pipeline = device.createComputePipeline({
|
||||
layout: 'auto',
|
||||
compute: {
|
||||
module: device.createShaderModule({
|
||||
code: shader,
|
||||
}),
|
||||
entryPoint: 'main',
|
||||
},
|
||||
});
|
||||
|
||||
this.uniforms = this.device.createBuffer({
|
||||
size: AgentPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
|
||||
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
||||
});
|
||||
|
||||
const serializedAgents = new Float32Array(
|
||||
new ArrayBuffer(agents.length * AGENT_SIZE)
|
||||
);
|
||||
agents.forEach((agent, index) => {
|
||||
serializedAgents[index * 4 + 0] = agent.position[0];
|
||||
serializedAgents[index * 4 + 1] = agent.position[1];
|
||||
serializedAgents[index * 4 + 2] = agent.angle;
|
||||
});
|
||||
|
||||
this.agentsBuffer = device.createBuffer({
|
||||
size: agents.length * AGENT_SIZE,
|
||||
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
|
||||
});
|
||||
|
||||
device.queue.writeBuffer(this.agentsBuffer, 0, serializedAgents.buffer);
|
||||
}
|
||||
|
||||
public setParameters({
|
||||
width,
|
||||
height,
|
||||
trailWeight,
|
||||
deltaTime,
|
||||
time,
|
||||
moveSpeed,
|
||||
turnSpeed,
|
||||
sensorAngleDegrees,
|
||||
sensorOffsetDst,
|
||||
sensorSize,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
trailWeight: number;
|
||||
deltaTime: number;
|
||||
time: number;
|
||||
moveSpeed: number;
|
||||
turnSpeed: number;
|
||||
sensorAngleDegrees: number;
|
||||
sensorOffsetDst: number;
|
||||
sensorSize: number;
|
||||
}) {
|
||||
this.device.queue.writeBuffer(
|
||||
this.uniforms,
|
||||
0,
|
||||
new Float32Array([
|
||||
width,
|
||||
height,
|
||||
trailWeight,
|
||||
deltaTime,
|
||||
time,
|
||||
moveSpeed,
|
||||
turnSpeed,
|
||||
sensorAngleDegrees,
|
||||
sensorOffsetDst,
|
||||
sensorSize,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
public execute(
|
||||
commandEncoder: GPUCommandEncoder,
|
||||
trailMapIn: GPUTexture,
|
||||
trailMapOut: GPUTexture
|
||||
) {
|
||||
this.ensureBindGroupExists(trailMapIn, trailMapOut);
|
||||
|
||||
const passEncoder = commandEncoder.beginComputePass();
|
||||
passEncoder.setPipeline(this.pipeline);
|
||||
passEncoder.setBindGroup(0, this.bindGroup);
|
||||
passEncoder.dispatchWorkgroups(
|
||||
Math.ceil(this.agentsBuffer.size / AGENT_SIZE / AgentPipeline.WORKGROUP_SIZE)
|
||||
);
|
||||
passEncoder.end();
|
||||
}
|
||||
|
||||
private ensureBindGroupExists(trailMapIn: GPUTexture, trailMapOut: GPUTexture) {
|
||||
if (
|
||||
this.previousTrailMapIn !== trailMapIn ||
|
||||
this.previousTrailMapOut !== trailMapOut
|
||||
) {
|
||||
this.bindGroup = this.device.createBindGroup({
|
||||
layout: this.pipeline.getBindGroupLayout(0),
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
resource: {
|
||||
buffer: this.uniforms,
|
||||
},
|
||||
},
|
||||
{
|
||||
binding: 1,
|
||||
resource: {
|
||||
buffer: this.agentsBuffer,
|
||||
},
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
resource: trailMapIn.createView(),
|
||||
},
|
||||
{
|
||||
binding: 3,
|
||||
resource: trailMapOut.createView(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.previousTrailMapIn = trailMapIn;
|
||||
this.previousTrailMapOut = trailMapOut;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/pipelines/agents/agent.ts
Normal file
8
src/pipelines/agents/agent.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
export interface Agent {
|
||||
position: vec2;
|
||||
angle: number;
|
||||
}
|
||||
|
||||
export const AGENT_SIZE = 4;
|
||||
125
src/pipelines/agents/agent.wgsl
Normal file
125
src/pipelines/agents/agent.wgsl
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
struct Agent {
|
||||
position: vec2<f32>,
|
||||
angle: f32,
|
||||
}
|
||||
|
||||
struct Settings {
|
||||
width : i32,
|
||||
height : i32,
|
||||
trailWeight : f32,
|
||||
deltaTime : f32,
|
||||
time : f32,
|
||||
|
||||
moveSpeed : f32,
|
||||
turnSpeed : f32,
|
||||
sensorAngleDegrees : f32,
|
||||
sensorOffsetDst : f32,
|
||||
sensorSize : f32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> settings : Settings;
|
||||
@group(0) @binding(1) var<storage, read_write> agents: array<Agent>;
|
||||
@group(0) @binding(2) var TrailMapIn : texture_2d<f32>;
|
||||
@group(0) @binding(3) var TrailMapOut : texture_storage_2d<rgba16float, write>;
|
||||
|
||||
|
||||
// Hash function www.cs.ubc.ca/~rbridson/docs/schechter-sca08-turbulence.pdf
|
||||
fn hash(state0 : u32) -> u32
|
||||
{
|
||||
var state : u32 = state0;
|
||||
state = state ^ 2747636419u;
|
||||
state = state * 2654435769u;
|
||||
state = state ^ (state >> 16u);
|
||||
state = state * 2654435769u;
|
||||
state = state ^ (state >> 16u);
|
||||
state = state * 2654435769u;
|
||||
return state;
|
||||
}
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
|
||||
let id = global_id.x;
|
||||
|
||||
if (id >= arrayLength(&agents)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var agent = agents[id];
|
||||
|
||||
|
||||
|
||||
var random = f32(hash(
|
||||
u32(
|
||||
agent.position.y * f32(settings.width) + agent.position.x
|
||||
)
|
||||
+ hash(
|
||||
id + u32(settings.time * 100000.)
|
||||
)
|
||||
)) / 4294967295.0;
|
||||
|
||||
// Steer based on sensory data
|
||||
let sensorAngleRad : f32 = settings.sensorAngleDegrees * (3.1415 / 180.);
|
||||
let weightForward : f32 = sense(agent, 0.);
|
||||
let weightLeft : f32 = sense(agent, sensorAngleRad);
|
||||
let weightRight : f32 = sense(agent, -sensorAngleRad);
|
||||
|
||||
let randomSteerStrength : f32 = random;
|
||||
let turnSpeed : f32 = settings.turnSpeed * 2. * 3.1415;
|
||||
|
||||
// choose random direction
|
||||
if (weightForward < weightLeft && weightForward < weightRight) {
|
||||
agent.angle = agent.angle + (randomSteerStrength - 0.5) * 2. * turnSpeed * settings.deltaTime;
|
||||
}
|
||||
// Turn right
|
||||
else if (weightRight > weightLeft) {
|
||||
agent.angle = agent.angle - randomSteerStrength * turnSpeed * settings.deltaTime;
|
||||
}
|
||||
// Turn left
|
||||
else if (weightLeft > weightRight) {
|
||||
agent.angle = agent.angle + randomSteerStrength * turnSpeed * settings.deltaTime;
|
||||
}
|
||||
|
||||
// Update position
|
||||
let direction : vec2<f32> = vec2<f32>(cos(agent.angle), sin(agent.angle));
|
||||
var newPos : vec2<f32> = agent.position + direction * settings.deltaTime * settings.moveSpeed;
|
||||
|
||||
// Clamp position to map boundaries, and pick new random move dir if hit boundary
|
||||
if (newPos.x < 0. || newPos.x >= f32(settings.width) || newPos.y < 0. || newPos.y >= f32(settings.height)) {
|
||||
// random = hash(random);
|
||||
let randomAngle : f32 = random * 2. * 3.1415;
|
||||
|
||||
newPos.x = min(f32(settings.width - 1), max(0., newPos.x));
|
||||
newPos.y = min(f32(settings.height - 1), max(0., newPos.y));
|
||||
agent.angle = randomAngle;
|
||||
} else {
|
||||
let offset : i32 = i32() * settings.width * 4 + i32() * 4;
|
||||
textureStore(TrailMapOut, vec2<i32>(i32(newPos.x), i32(newPos.y)), vec4(vec3<f32>(1.) * settings.trailWeight * settings.deltaTime, 1.));
|
||||
}
|
||||
|
||||
agent.position = newPos;
|
||||
agents[id] = agent;
|
||||
}
|
||||
|
||||
fn sense(agent : Agent, sensorAngleOffset : f32) -> f32 {
|
||||
let sensorAngle : f32 = agent.angle + sensorAngleOffset;
|
||||
let sensorDir : vec2<f32> = vec2<f32>(cos(sensorAngle), sin(sensorAngle));
|
||||
|
||||
let sensorPos : vec2<f32> = agent.position + sensorDir * settings.sensorOffsetDst;
|
||||
let sensorCentreX : i32 = i32(sensorPos.x);
|
||||
let sensorCentreY : i32 = i32(sensorPos.y);
|
||||
|
||||
var sum : f32 = 0.;
|
||||
|
||||
let senseWeight : vec4<i32> = vec4<i32>(2, 2, 2, 2) - vec4<i32>(1, 1, 1, 1);
|
||||
|
||||
let sensorSize : i32 = i32(settings.sensorSize);
|
||||
for (var offsetX : i32 = -sensorSize; offsetX <= sensorSize; offsetX = offsetX + 1) {
|
||||
for (var offsetY : i32 = -sensorSize; offsetY <= sensorSize; offsetY = offsetY + 1) {
|
||||
let sampleX : i32 = min(settings.width - 1, max(0, sensorCentreX + offsetX));
|
||||
let sampleY : i32 = min(settings.height - 1, max(0, sensorCentreY + offsetY));
|
||||
sum = sum + dot(vec4<f32>(senseWeight), textureLoad(TrailMapIn, vec2<i32>(sampleX, sampleY), 1));
|
||||
}
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
34
src/pipelines/diffusion/diffuse.wgsl
Normal file
34
src/pipelines/diffusion/diffuse.wgsl
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
struct VertexOutput {
|
||||
@builtin(position) Position : vec4<f32>,
|
||||
@location(0) fragUV : vec2<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vertex(@builtin(vertex_index) i : u32) -> VertexOutput {
|
||||
var pos = array<vec2<f32>, 4>(
|
||||
vec2(-1.0, 1.0),
|
||||
vec2(-1.0, -1.0),
|
||||
vec2(1.0, 1.0),
|
||||
vec2(1.0, -1.0),
|
||||
);
|
||||
|
||||
var output : VertexOutput;
|
||||
output.Position = vec4<f32>(pos[i], 0.0, 1.0);
|
||||
output.fragUV = output.Position.xy * 0.5 + 0.5;
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
@group(0) @binding(0) var mySampler: sampler;
|
||||
@group(0) @binding(1) var TargetTexture : texture_2d<f32>;
|
||||
|
||||
|
||||
@fragment
|
||||
fn fragment(@location(0) fragUV: vec2<f32>) -> @location(0) vec4<f32> {
|
||||
// return vec4(1.0, 0.0, 0.0, 1.0);
|
||||
return mix(
|
||||
vec4(textureSample(TargetTexture, mySampler, fragUV).rgb, 0.1),
|
||||
vec4(1.0, 1.0, 1.0, 1.0),
|
||||
0.01
|
||||
);
|
||||
}
|
||||
81
src/pipelines/diffusion/diffusion-pipeline.ts
Normal file
81
src/pipelines/diffusion/diffusion-pipeline.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import shader from './diffuse.wgsl';
|
||||
|
||||
export class DiffusionPipeline {
|
||||
private readonly pipeline: GPURenderPipeline;
|
||||
private bindGroup?: GPUBindGroup;
|
||||
private previousTrailMapIn?: GPUTexture;
|
||||
|
||||
public constructor(private readonly device: GPUDevice) {
|
||||
this.pipeline = device.createRenderPipeline({
|
||||
layout: 'auto',
|
||||
vertex: {
|
||||
module: device.createShaderModule({
|
||||
code: shader,
|
||||
}),
|
||||
entryPoint: 'vertex',
|
||||
},
|
||||
fragment: {
|
||||
module: device.createShaderModule({
|
||||
code: shader,
|
||||
}),
|
||||
entryPoint: 'fragment',
|
||||
targets: [
|
||||
{
|
||||
format: 'rgba16float',
|
||||
},
|
||||
],
|
||||
},
|
||||
primitive: {
|
||||
topology: 'triangle-strip',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public execute(
|
||||
commandEncoder: GPUCommandEncoder,
|
||||
trailMapIn: GPUTexture,
|
||||
trailMapOut: GPUTexture
|
||||
) {
|
||||
this.ensureBindGroupExists(trailMapIn);
|
||||
|
||||
const renderPassDescriptor: GPURenderPassDescriptor = {
|
||||
colorAttachments: [
|
||||
{
|
||||
view: trailMapOut.createView(),
|
||||
clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 },
|
||||
loadOp: 'clear',
|
||||
storeOp: 'store',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const renderPassEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
|
||||
renderPassEncoder.setBindGroup(0, this.bindGroup!);
|
||||
renderPassEncoder.setPipeline(this.pipeline);
|
||||
renderPassEncoder.draw(4, 1);
|
||||
renderPassEncoder.end();
|
||||
}
|
||||
|
||||
private ensureBindGroupExists(trailMapIn: GPUTexture) {
|
||||
if (this.previousTrailMapIn !== trailMapIn) {
|
||||
this.bindGroup = this.device.createBindGroup({
|
||||
layout: this.pipeline.getBindGroupLayout(0),
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
resource: this.device.createSampler({
|
||||
magFilter: 'linear',
|
||||
minFilter: 'linear',
|
||||
}),
|
||||
},
|
||||
{
|
||||
binding: 1,
|
||||
resource: trailMapIn.createView(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.previousTrailMapIn = trailMapIn;
|
||||
}
|
||||
}
|
||||
}
|
||||
80
src/pipelines/render/render-pipeline.ts
Normal file
80
src/pipelines/render/render-pipeline.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import shader from './render.wgsl';
|
||||
|
||||
export class RenderPipeline {
|
||||
private readonly pipeline: GPURenderPipeline;
|
||||
private bindGroup?: GPUBindGroup;
|
||||
private previousColorTexture?: GPUTexture;
|
||||
|
||||
public constructor(
|
||||
private readonly context: GPUCanvasContext,
|
||||
private readonly device: GPUDevice,
|
||||
preferredCanvasFormat: GPUTextureFormat
|
||||
) {
|
||||
this.pipeline = device.createRenderPipeline({
|
||||
layout: 'auto',
|
||||
vertex: {
|
||||
module: device.createShaderModule({
|
||||
code: shader,
|
||||
}),
|
||||
entryPoint: 'vertex',
|
||||
},
|
||||
fragment: {
|
||||
module: device.createShaderModule({
|
||||
code: shader,
|
||||
}),
|
||||
entryPoint: 'fragment',
|
||||
targets: [
|
||||
{
|
||||
format: preferredCanvasFormat,
|
||||
},
|
||||
],
|
||||
},
|
||||
primitive: {
|
||||
topology: 'triangle-strip',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public execute(commandEncoder: GPUCommandEncoder, colorTexture: GPUTexture) {
|
||||
this.ensureBindGroupExists(colorTexture);
|
||||
|
||||
const renderPassDescriptor: GPURenderPassDescriptor = {
|
||||
colorAttachments: [
|
||||
{
|
||||
view: this.context.getCurrentTexture().createView(),
|
||||
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
|
||||
loadOp: 'clear',
|
||||
storeOp: 'store',
|
||||
},
|
||||
],
|
||||
};
|
||||
const renderPassEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
|
||||
renderPassEncoder.setBindGroup(0, this.bindGroup);
|
||||
renderPassEncoder.setPipeline(this.pipeline);
|
||||
renderPassEncoder.draw(4, 1);
|
||||
renderPassEncoder.end();
|
||||
}
|
||||
|
||||
private ensureBindGroupExists(colorTexture: GPUTexture) {
|
||||
if (this.previousColorTexture !== colorTexture) {
|
||||
this.bindGroup = this.device.createBindGroup({
|
||||
layout: this.pipeline.getBindGroupLayout(0),
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
resource: this.device.createSampler({
|
||||
magFilter: 'linear',
|
||||
minFilter: 'linear',
|
||||
}),
|
||||
},
|
||||
{
|
||||
binding: 1,
|
||||
resource: colorTexture.createView(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.previousColorTexture = colorTexture;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/pipelines/render/render.wgsl
Normal file
30
src/pipelines/render/render.wgsl
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
struct VertexOutput {
|
||||
@builtin(position) Position : vec4<f32>,
|
||||
@location(0) fragUV : vec2<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vertex(@builtin(vertex_index) i : u32) -> VertexOutput {
|
||||
var pos = array<vec2<f32>, 4>(
|
||||
vec2(-1.0, 1.0),
|
||||
vec2(-1.0, -1.0),
|
||||
vec2(1.0, 1.0),
|
||||
vec2(1.0, -1.0),
|
||||
);
|
||||
|
||||
var output : VertexOutput;
|
||||
output.Position = vec4<f32>(pos[i], 0.0, 1.0);
|
||||
output.fragUV = output.Position.xy * 0.5 + 0.5;
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
@group(0) @binding(0) var mySampler: sampler;
|
||||
@group(0) @binding(1) var TargetTexture : texture_2d<f32>;
|
||||
|
||||
|
||||
@fragment
|
||||
fn fragment(@location(0) fragUV: vec2<f32>) -> @location(0) vec4<f32> {
|
||||
// return vec4(1.0, 0.0, 0.0, 1.0);
|
||||
return textureSample(TargetTexture, mySampler, fragUV);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue