This commit is contained in:
Andras Schmelczer 2023-04-15 21:57:33 +01:00
parent 9cf8f73e18
commit 5cc94805f1
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
17 changed files with 670 additions and 237 deletions

View file

@ -15,7 +15,6 @@
<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="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta
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."

View file

@ -1,15 +1,29 @@
html,
body {
*,
*::before,
*::after {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
background: #000;
width: 100%;
box-sizing: border-box;
@media (prefers-reduced-motion) {
transition: none !important;
animation: none !important;
}
}
html {
height: 100%;
-webkit-font-smooth: antialiased;
}
body {
height: 100%;
background-color: hotpink;
display: flex;
align-items: center;
justify-content: center;
}
}
canvas {
background: hotpink;
height: 100%;
width: 100%;
}

View file

@ -1,5 +1,6 @@
import './index.scss';
import Renderer from './renderer';
import './utils/mulberry32';
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
canvas.width = canvas.height = 640;

View 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;
}
}
}

View file

@ -0,0 +1,8 @@
import { vec2 } from 'gl-matrix';
export interface Agent {
position: vec2;
angle: number;
}
export const AGENT_SIZE = 4;

View 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;
}

View 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
);
}

View 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;
}
}
}

View 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;
}
}
}

View 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);
}

View file

@ -1,227 +1,124 @@
import fragShaderCode from './shaders/triangle.frag.wgsl';
import vertShaderCode from './shaders/triangle.vert.wgsl';
import { Agent } from './pipelines/agents/agent';
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]);
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]);
import { vec2 } from 'gl-matrix';
export default class Renderer {
canvas: HTMLCanvasElement;
private context: GPUCanvasContext;
private adapter: GPUAdapter;
private device: GPUDevice;
private queue: GPUQueue;
adapter: GPUAdapter;
device: GPUDevice;
queue: GPUQueue;
private agentPipeline: AgentPipeline;
private renderPipeline: RenderPipeline;
private diffusionPipeline: any;
context: GPUCanvasContext;
colorTexture: GPUTexture;
colorTextureView: GPUTextureView;
depthTexture: GPUTexture;
depthTextureView: GPUTextureView;
private preferredCanvasFormat: GPUTextureFormat;
private trailMapA?: GPUTexture;
private trailMapB?: GPUTexture;
positionBuffer: GPUBuffer;
colorBuffer: GPUBuffer;
indexBuffer: GPUBuffer;
vertModule: GPUShaderModule;
fragModule: GPUShaderModule;
pipeline: GPURenderPipeline;
commandEncoder: GPUCommandEncoder;
passEncoder: GPURenderPassEncoder;
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
}
public constructor(private canvas: HTMLCanvasElement) {}
async start() {
if (await this.initializeAPI()) {
this.resizeBackings();
await this.initializeResources();
this.render();
}
await this.initialize();
requestAnimationFrame(this.render.bind(this));
}
async initializeAPI(): Promise<boolean> {
try {
const entry: GPU = navigator.gpu;
if (!entry) {
return false;
}
private async initialize(): Promise<void> {
await this.initializeDevice();
this.adapter = await entry.requestAdapter();
this.device = await this.adapter.requestDevice();
this.queue = this.device.queue;
} catch (e) {
console.error(e);
return false;
this.resize();
window.addEventListener('resize', this.resize.bind(this));
const agents: Array<Agent> = new Array(settings.numAgents).fill(0).map(() => ({
position: vec2.fromValues(randomBetween(0, 500), randomBetween(0, 500)),
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() {
const createBuffer = (arr: Float32Array | Uint16Array, usage: number) => {
// 📏 Align to 4 bytes (thanks @chrimsonite)
const desc = {
size: (arr.byteLength + 3) & ~3,
usage,
mappedAtCreation: true,
};
const buffer = this.device.createBuffer(desc);
const writeArray =
arr instanceof Uint16Array
? new Uint16Array(buffer.getMappedRange())
: new Float32Array(buffer.getMappedRange());
writeArray.set(arr);
buffer.unmap();
return buffer;
};
private render(time: DOMHighResTimeStamp) {
this.agentPipeline.setParameters({
width: this.canvas.width,
height: this.canvas.height,
time,
deltaTime: 0.016,
sensorAngleDegrees: 45,
...settings,
});
const commandEncoder = this.device.createCommandEncoder();
this.positionBuffer = createBuffer(positions, GPUBufferUsage.VERTEX);
this.colorBuffer = createBuffer(colors, GPUBufferUsage.VERTEX);
this.indexBuffer = createBuffer(indices, GPUBufferUsage.INDEX);
this.agentPipeline.execute(commandEncoder, this.trailMapA, this.trailMapB);
this.diffusionPipeline.execute(commandEncoder, this.trailMapB, this.trailMapA);
this.renderPipeline.execute(commandEncoder, this.trailMapB);
[this.trailMapA, this.trailMapB] = [this.trailMapB, this.trailMapA];
const vsmDesc = {
code: vertShaderCode,
};
this.vertModule = this.device.createShaderModule(vsmDesc);
this.queue.submit([commandEncoder.finish()]);
const fsmDesc = {
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);
requestAnimationFrame(this.render.bind(this));
}
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
View 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,
};

View file

@ -1,4 +0,0 @@
@fragment
fn main(@location(0) inColor: vec3<f32>) -> @location(0) vec4<f32> {
return vec4<f32>(inColor, 1.0);
}

View file

@ -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
View 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);

View file

@ -0,0 +1,3 @@
export const randomBetween = (min: number, max: number) => {
return Math.random() * (max - min) + min;
};

View file

@ -42,7 +42,7 @@ module.exports = (env, argv) => ({
use: 'svg-inline-loader',
},
{
test: /\.wgsl/,
test: /\.wgsl$/i,
type: 'asset/source',
generator: {
filename: '[name][ext]',