This commit is contained in:
parent
9cf8f73e18
commit
5cc94805f1
17 changed files with 672 additions and 239 deletions
|
|
@ -15,7 +15,6 @@
|
||||||
<link rel="icon" href="favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="favicon.ico" type="image/x-icon" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="I'm Andras Schmelczer, and this is my portfolio. Discover some of my projects. I'm passionate about solving challenging problems and designing large-scale systems, especially in the context of machine learning."
|
content="I'm Andras Schmelczer, and this is my portfolio. Discover some of my projects. I'm passionate about solving challenging problems and designing large-scale systems, especially in the context of machine learning."
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,29 @@
|
||||||
html,
|
*,
|
||||||
body {
|
*::before,
|
||||||
|
*::after {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
box-sizing: border-box;
|
||||||
font-size: 100%;
|
|
||||||
font: inherit;
|
@media (prefers-reduced-motion) {
|
||||||
vertical-align: baseline;
|
transition: none !important;
|
||||||
background: #000;
|
animation: none !important;
|
||||||
width: 100%;
|
}
|
||||||
height: 100%;
|
}
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
html {
|
||||||
justify-content: center;
|
height: 100%;
|
||||||
|
-webkit-font-smooth: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
background-color: hotpink;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
background: hotpink;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
import Renderer from './renderer';
|
import Renderer from './renderer';
|
||||||
|
import './utils/mulberry32';
|
||||||
|
|
||||||
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
|
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
|
||||||
canvas.width = canvas.height = 640;
|
canvas.width = canvas.height = 640;
|
||||||
|
|
|
||||||
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);
|
||||||
|
}
|
||||||
311
src/renderer.ts
311
src/renderer.ts
|
|
@ -1,227 +1,124 @@
|
||||||
import fragShaderCode from './shaders/triangle.frag.wgsl';
|
import { Agent } from './pipelines/agents/agent';
|
||||||
import vertShaderCode from './shaders/triangle.vert.wgsl';
|
import { AgentPipeline } from './pipelines/agents/agent-pipeline';
|
||||||
|
import { DiffusionPipeline } from './pipelines/diffusion/diffusion-pipeline';
|
||||||
|
import { RenderPipeline } from './pipelines/render/render-pipeline';
|
||||||
|
import { settings } from './settings';
|
||||||
|
import { randomBetween } from './utils/random-between';
|
||||||
|
|
||||||
const positions = new Float32Array([1.0, -1.0, 0.0, -1.0, -1.0, 0.0, 0.0, 1.0, 0.0]);
|
import { vec2 } from 'gl-matrix';
|
||||||
const colors = new Float32Array([1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0]);
|
|
||||||
|
|
||||||
const indices = new Uint16Array([0, 1, 2]);
|
|
||||||
|
|
||||||
export default class Renderer {
|
export default class Renderer {
|
||||||
canvas: HTMLCanvasElement;
|
private context: GPUCanvasContext;
|
||||||
|
private adapter: GPUAdapter;
|
||||||
|
private device: GPUDevice;
|
||||||
|
private queue: GPUQueue;
|
||||||
|
|
||||||
adapter: GPUAdapter;
|
private agentPipeline: AgentPipeline;
|
||||||
device: GPUDevice;
|
private renderPipeline: RenderPipeline;
|
||||||
queue: GPUQueue;
|
private diffusionPipeline: any;
|
||||||
|
|
||||||
context: GPUCanvasContext;
|
private preferredCanvasFormat: GPUTextureFormat;
|
||||||
colorTexture: GPUTexture;
|
private trailMapA?: GPUTexture;
|
||||||
colorTextureView: GPUTextureView;
|
private trailMapB?: GPUTexture;
|
||||||
depthTexture: GPUTexture;
|
|
||||||
depthTextureView: GPUTextureView;
|
|
||||||
|
|
||||||
positionBuffer: GPUBuffer;
|
public constructor(private canvas: HTMLCanvasElement) {}
|
||||||
colorBuffer: GPUBuffer;
|
|
||||||
indexBuffer: GPUBuffer;
|
|
||||||
vertModule: GPUShaderModule;
|
|
||||||
fragModule: GPUShaderModule;
|
|
||||||
pipeline: GPURenderPipeline;
|
|
||||||
|
|
||||||
commandEncoder: GPUCommandEncoder;
|
|
||||||
passEncoder: GPURenderPassEncoder;
|
|
||||||
|
|
||||||
constructor(canvas: HTMLCanvasElement) {
|
|
||||||
this.canvas = canvas;
|
|
||||||
}
|
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
if (await this.initializeAPI()) {
|
await this.initialize();
|
||||||
this.resizeBackings();
|
requestAnimationFrame(this.render.bind(this));
|
||||||
await this.initializeResources();
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async initializeAPI(): Promise<boolean> {
|
private async initialize(): Promise<void> {
|
||||||
try {
|
await this.initializeDevice();
|
||||||
const entry: GPU = navigator.gpu;
|
|
||||||
if (!entry) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.adapter = await entry.requestAdapter();
|
this.resize();
|
||||||
this.device = await this.adapter.requestDevice();
|
window.addEventListener('resize', this.resize.bind(this));
|
||||||
this.queue = this.device.queue;
|
|
||||||
} catch (e) {
|
const agents: Array<Agent> = new Array(settings.numAgents).fill(0).map(() => ({
|
||||||
console.error(e);
|
position: vec2.fromValues(randomBetween(0, 500), randomBetween(0, 500)),
|
||||||
return false;
|
angle: randomBetween(0, Math.PI * 2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.agentPipeline = new AgentPipeline(this.device, agents);
|
||||||
|
this.renderPipeline = new RenderPipeline(
|
||||||
|
this.context,
|
||||||
|
this.device,
|
||||||
|
this.preferredCanvasFormat
|
||||||
|
);
|
||||||
|
this.diffusionPipeline = new DiffusionPipeline(this.device);
|
||||||
|
}
|
||||||
|
|
||||||
|
private resize() {
|
||||||
|
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||||
|
this.canvas.width = this.canvas.clientWidth * devicePixelRatio;
|
||||||
|
this.canvas.height = this.canvas.clientHeight * devicePixelRatio;
|
||||||
|
|
||||||
|
this.trailMapA?.destroy();
|
||||||
|
this.trailMapA = this.device.createTexture({
|
||||||
|
size: {
|
||||||
|
width: this.canvas.width,
|
||||||
|
height: this.canvas.height,
|
||||||
|
depthOrArrayLayers: 1,
|
||||||
|
},
|
||||||
|
format: 'rgba16float',
|
||||||
|
usage:
|
||||||
|
GPUTextureUsage.STORAGE_BINDING |
|
||||||
|
GPUTextureUsage.TEXTURE_BINDING |
|
||||||
|
GPUTextureUsage.RENDER_ATTACHMENT,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.trailMapB?.destroy();
|
||||||
|
this.trailMapB = this.device.createTexture({
|
||||||
|
size: {
|
||||||
|
width: this.canvas.width,
|
||||||
|
height: this.canvas.height,
|
||||||
|
depthOrArrayLayers: 1,
|
||||||
|
},
|
||||||
|
format: 'rgba16float',
|
||||||
|
usage:
|
||||||
|
GPUTextureUsage.STORAGE_BINDING |
|
||||||
|
GPUTextureUsage.TEXTURE_BINDING |
|
||||||
|
GPUTextureUsage.RENDER_ATTACHMENT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initializeDevice(): Promise<void> {
|
||||||
|
const gpu = navigator.gpu;
|
||||||
|
if (!gpu) {
|
||||||
|
throw new Error('WebGPU is not supported');
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
this.adapter = await gpu.requestAdapter();
|
||||||
|
this.device = await this.adapter.requestDevice(); // could request more resources
|
||||||
|
this.queue = this.device.queue;
|
||||||
|
|
||||||
|
this.context = this.canvas.getContext('webgpu') as any;
|
||||||
|
this.preferredCanvasFormat = navigator.gpu.getPreferredCanvasFormat();
|
||||||
|
this.context.configure({
|
||||||
|
device: this.device,
|
||||||
|
format: this.preferredCanvasFormat,
|
||||||
|
alphaMode: 'premultiplied',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async initializeResources() {
|
private render(time: DOMHighResTimeStamp) {
|
||||||
const createBuffer = (arr: Float32Array | Uint16Array, usage: number) => {
|
this.agentPipeline.setParameters({
|
||||||
// 📏 Align to 4 bytes (thanks @chrimsonite)
|
width: this.canvas.width,
|
||||||
const desc = {
|
height: this.canvas.height,
|
||||||
size: (arr.byteLength + 3) & ~3,
|
time,
|
||||||
usage,
|
deltaTime: 0.016,
|
||||||
mappedAtCreation: true,
|
sensorAngleDegrees: 45,
|
||||||
};
|
...settings,
|
||||||
const buffer = this.device.createBuffer(desc);
|
});
|
||||||
const writeArray =
|
const commandEncoder = this.device.createCommandEncoder();
|
||||||
arr instanceof Uint16Array
|
|
||||||
? new Uint16Array(buffer.getMappedRange())
|
|
||||||
: new Float32Array(buffer.getMappedRange());
|
|
||||||
writeArray.set(arr);
|
|
||||||
buffer.unmap();
|
|
||||||
return buffer;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.positionBuffer = createBuffer(positions, GPUBufferUsage.VERTEX);
|
this.agentPipeline.execute(commandEncoder, this.trailMapA, this.trailMapB);
|
||||||
this.colorBuffer = createBuffer(colors, GPUBufferUsage.VERTEX);
|
this.diffusionPipeline.execute(commandEncoder, this.trailMapB, this.trailMapA);
|
||||||
this.indexBuffer = createBuffer(indices, GPUBufferUsage.INDEX);
|
this.renderPipeline.execute(commandEncoder, this.trailMapB);
|
||||||
|
[this.trailMapA, this.trailMapB] = [this.trailMapB, this.trailMapA];
|
||||||
|
|
||||||
const vsmDesc = {
|
this.queue.submit([commandEncoder.finish()]);
|
||||||
code: vertShaderCode,
|
|
||||||
};
|
|
||||||
this.vertModule = this.device.createShaderModule(vsmDesc);
|
|
||||||
|
|
||||||
const fsmDesc = {
|
requestAnimationFrame(this.render.bind(this));
|
||||||
code: fragShaderCode,
|
|
||||||
};
|
|
||||||
this.fragModule = this.device.createShaderModule(fsmDesc);
|
|
||||||
|
|
||||||
const positionAttribDesc: GPUVertexAttribute = {
|
|
||||||
shaderLocation: 0, // [[location(0)]]
|
|
||||||
offset: 0,
|
|
||||||
format: 'float32x3',
|
|
||||||
};
|
|
||||||
const colorAttribDesc: GPUVertexAttribute = {
|
|
||||||
shaderLocation: 1, // [[location(1)]]
|
|
||||||
offset: 0,
|
|
||||||
format: 'float32x3',
|
|
||||||
};
|
|
||||||
const positionBufferDesc: GPUVertexBufferLayout = {
|
|
||||||
attributes: [positionAttribDesc],
|
|
||||||
arrayStride: 4 * 3, // sizeof(float) * 3
|
|
||||||
stepMode: 'vertex',
|
|
||||||
};
|
|
||||||
const colorBufferDesc: GPUVertexBufferLayout = {
|
|
||||||
attributes: [colorAttribDesc],
|
|
||||||
arrayStride: 4 * 3, // sizeof(float) * 3
|
|
||||||
stepMode: 'vertex',
|
|
||||||
};
|
|
||||||
|
|
||||||
const depthStencil: GPUDepthStencilState = {
|
|
||||||
depthWriteEnabled: true,
|
|
||||||
depthCompare: 'less',
|
|
||||||
format: 'depth24plus-stencil8',
|
|
||||||
};
|
|
||||||
|
|
||||||
const pipelineLayoutDesc = { bindGroupLayouts: [] };
|
|
||||||
const layout = this.device.createPipelineLayout(pipelineLayoutDesc);
|
|
||||||
|
|
||||||
const vertex: GPUVertexState = {
|
|
||||||
module: this.vertModule,
|
|
||||||
entryPoint: 'main',
|
|
||||||
buffers: [positionBufferDesc, colorBufferDesc],
|
|
||||||
};
|
|
||||||
|
|
||||||
const colorState: GPUColorTargetState = {
|
|
||||||
format: 'bgra8unorm',
|
|
||||||
};
|
|
||||||
|
|
||||||
const fragment: GPUFragmentState = {
|
|
||||||
module: this.fragModule,
|
|
||||||
entryPoint: 'main',
|
|
||||||
targets: [colorState],
|
|
||||||
};
|
|
||||||
|
|
||||||
const primitive: GPUPrimitiveState = {
|
|
||||||
frontFace: 'cw',
|
|
||||||
cullMode: 'none',
|
|
||||||
topology: 'triangle-list',
|
|
||||||
};
|
|
||||||
|
|
||||||
const pipelineDesc: GPURenderPipelineDescriptor = {
|
|
||||||
layout,
|
|
||||||
|
|
||||||
vertex,
|
|
||||||
fragment,
|
|
||||||
|
|
||||||
primitive,
|
|
||||||
depthStencil,
|
|
||||||
};
|
|
||||||
this.pipeline = this.device.createRenderPipeline(pipelineDesc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resizeBackings() {
|
|
||||||
if (!this.context) {
|
|
||||||
this.context = this.canvas.getContext('webgpu') as any;
|
|
||||||
const canvasConfig: GPUCanvasConfiguration = {
|
|
||||||
device: this.device,
|
|
||||||
format: 'bgra8unorm',
|
|
||||||
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
|
|
||||||
alphaMode: 'opaque',
|
|
||||||
};
|
|
||||||
this.context.configure(canvasConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
const depthTextureDesc: GPUTextureDescriptor = {
|
|
||||||
size: [this.canvas.width, this.canvas.height, 1],
|
|
||||||
dimension: '2d',
|
|
||||||
format: 'depth24plus-stencil8',
|
|
||||||
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.depthTexture = this.device.createTexture(depthTextureDesc);
|
|
||||||
this.depthTextureView = this.depthTexture.createView();
|
|
||||||
}
|
|
||||||
|
|
||||||
encodeCommands() {
|
|
||||||
const colorAttachment: GPURenderPassColorAttachment = {
|
|
||||||
view: this.colorTextureView,
|
|
||||||
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
||||||
loadOp: 'clear',
|
|
||||||
storeOp: 'store',
|
|
||||||
};
|
|
||||||
|
|
||||||
const depthAttachment: GPURenderPassDepthStencilAttachment = {
|
|
||||||
view: this.depthTextureView,
|
|
||||||
depthClearValue: 1,
|
|
||||||
depthLoadOp: 'clear',
|
|
||||||
depthStoreOp: 'store',
|
|
||||||
stencilClearValue: 0,
|
|
||||||
stencilLoadOp: 'clear',
|
|
||||||
stencilStoreOp: 'store',
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderPassDesc: GPURenderPassDescriptor = {
|
|
||||||
colorAttachments: [colorAttachment],
|
|
||||||
depthStencilAttachment: depthAttachment,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.commandEncoder = this.device.createCommandEncoder();
|
|
||||||
|
|
||||||
this.passEncoder = this.commandEncoder.beginRenderPass(renderPassDesc);
|
|
||||||
this.passEncoder.setPipeline(this.pipeline);
|
|
||||||
this.passEncoder.setViewport(0, 0, this.canvas.width, this.canvas.height, 0, 1);
|
|
||||||
this.passEncoder.setScissorRect(0, 0, this.canvas.width, this.canvas.height);
|
|
||||||
this.passEncoder.setVertexBuffer(0, this.positionBuffer);
|
|
||||||
this.passEncoder.setVertexBuffer(1, this.colorBuffer);
|
|
||||||
this.passEncoder.setIndexBuffer(this.indexBuffer, 'uint16');
|
|
||||||
this.passEncoder.drawIndexed(3, 1);
|
|
||||||
this.passEncoder.end();
|
|
||||||
|
|
||||||
this.queue.submit([this.commandEncoder.finish()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
render = () => {
|
|
||||||
this.colorTexture = this.context.getCurrentTexture();
|
|
||||||
this.colorTextureView = this.colorTexture.createView();
|
|
||||||
|
|
||||||
this.encodeCommands();
|
|
||||||
|
|
||||||
requestAnimationFrame(this.render);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
30
src/settings.ts
Normal file
30
src/settings.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
const SpawnMode = { Random: 0, Point: 1, InwardCircle: 2, RandomCircle: 3 };
|
||||||
|
|
||||||
|
interface Settings {
|
||||||
|
stepsPerFrame: number;
|
||||||
|
numAgents: number;
|
||||||
|
spawnMode: number;
|
||||||
|
trailWeight: number;
|
||||||
|
decayRate: number;
|
||||||
|
diffuseRate: number;
|
||||||
|
moveSpeed: number;
|
||||||
|
turnSpeed: number;
|
||||||
|
sensorAngleSpacing: number;
|
||||||
|
sensorOffsetDst: number;
|
||||||
|
sensorSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settings: Settings = {
|
||||||
|
stepsPerFrame: 2,
|
||||||
|
numAgents: 250000,
|
||||||
|
spawnMode: SpawnMode.InwardCircle,
|
||||||
|
trailWeight: 5,
|
||||||
|
decayRate: 0.2,
|
||||||
|
diffuseRate: 3,
|
||||||
|
|
||||||
|
moveSpeed: 20,
|
||||||
|
turnSpeed: 2,
|
||||||
|
sensorAngleSpacing: 30,
|
||||||
|
sensorOffsetDst: 35,
|
||||||
|
sensorSize: 1,
|
||||||
|
};
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
@fragment
|
|
||||||
fn main(@location(0) inColor: vec3<f32>) -> @location(0) vec4<f32> {
|
|
||||||
return vec4<f32>(inColor, 1.0);
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
struct VSOut {
|
|
||||||
@builtin(position) Position: vec4<f32>,
|
|
||||||
@location(0) color: vec3<f32>,
|
|
||||||
};
|
|
||||||
|
|
||||||
@vertex
|
|
||||||
fn main(@location(0) inPos: vec3<f32>,
|
|
||||||
@location(1) inColor: vec3<f32>) -> VSOut {
|
|
||||||
var vsOut: VSOut;
|
|
||||||
vsOut.Position = vec4<f32>(inPos, 1.0);
|
|
||||||
vsOut.color = inColor;
|
|
||||||
return vsOut;
|
|
||||||
}
|
|
||||||
8
src/utils/mulberry32.ts
Normal file
8
src/utils/mulberry32.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export const mulberry32 = (seed) => () => {
|
||||||
|
let t = (seed += 0x6d2b79f5);
|
||||||
|
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||||
|
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||||
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||||
|
};
|
||||||
|
|
||||||
|
Math.random = mulberry32(123);
|
||||||
3
src/utils/random-between.ts
Normal file
3
src/utils/random-between.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const randomBetween = (min: number, max: number) => {
|
||||||
|
return Math.random() * (max - min) + min;
|
||||||
|
};
|
||||||
|
|
@ -42,7 +42,7 @@ module.exports = (env, argv) => ({
|
||||||
use: 'svg-inline-loader',
|
use: 'svg-inline-loader',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.wgsl/,
|
test: /\.wgsl$/i,
|
||||||
type: 'asset/source',
|
type: 'asset/source',
|
||||||
generator: {
|
generator: {
|
||||||
filename: '[name][ext]',
|
filename: '[name][ext]',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue