This commit is contained in:
parent
9cf8f73e18
commit
5cc94805f1
17 changed files with 670 additions and 237 deletions
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
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;
|
||||
}
|
||||
|
||||
canvas {
|
||||
background: hotpink;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
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);
|
||||
}
|
||||
309
src/renderer.ts
309
src/renderer.ts
|
|
@ -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.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);
|
||||
}
|
||||
|
||||
this.adapter = await entry.requestAdapter();
|
||||
this.device = await this.adapter.requestDevice();
|
||||
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');
|
||||
}
|
||||
|
||||
this.adapter = await gpu.requestAdapter();
|
||||
this.device = await this.adapter.requestDevice(); // could request more resources
|
||||
this.queue = this.device.queue;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
this.positionBuffer = createBuffer(positions, GPUBufferUsage.VERTEX);
|
||||
this.colorBuffer = createBuffer(colors, GPUBufferUsage.VERTEX);
|
||||
this.indexBuffer = createBuffer(indices, GPUBufferUsage.INDEX);
|
||||
|
||||
const vsmDesc = {
|
||||
code: vertShaderCode,
|
||||
};
|
||||
this.vertModule = this.device.createShaderModule(vsmDesc);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
resizeBackings() {
|
||||
if (!this.context) {
|
||||
this.context = this.canvas.getContext('webgpu') as any;
|
||||
const canvasConfig: GPUCanvasConfiguration = {
|
||||
this.preferredCanvasFormat = navigator.gpu.getPreferredCanvasFormat();
|
||||
this.context.configure({
|
||||
device: this.device,
|
||||
format: 'bgra8unorm',
|
||||
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
|
||||
alphaMode: 'opaque',
|
||||
};
|
||||
this.context.configure(canvasConfig);
|
||||
format: this.preferredCanvasFormat,
|
||||
alphaMode: 'premultiplied',
|
||||
});
|
||||
}
|
||||
|
||||
const depthTextureDesc: GPUTextureDescriptor = {
|
||||
size: [this.canvas.width, this.canvas.height, 1],
|
||||
dimension: '2d',
|
||||
format: 'depth24plus-stencil8',
|
||||
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
|
||||
};
|
||||
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.depthTexture = this.device.createTexture(depthTextureDesc);
|
||||
this.depthTextureView = this.depthTexture.createView();
|
||||
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];
|
||||
|
||||
this.queue.submit([commandEncoder.finish()]);
|
||||
|
||||
requestAnimationFrame(this.render.bind(this));
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
{
|
||||
test: /\.wgsl/,
|
||||
test: /\.wgsl$/i,
|
||||
type: 'asset/source',
|
||||
generator: {
|
||||
filename: '[name][ext]',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue