This commit is contained in:
Andras Schmelczer 2023-04-29 11:58:14 +01:00
commit 9e582110ea
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
27 changed files with 248 additions and 123 deletions

View file

@ -1,18 +1,3 @@
# 🔺 WebGPU Seed ## todo
[![License][license-img]][license-url] -
A WebGPU repo you can use to get started with your own renderer.
- [🔳 Codepen Example](https://codepen.io/alaingalvan/pen/GRgvLGw)
- [💬 Blog Post](https://alain.xyz/blog/raw-webgpu)
## Setup
> Refer to [this blog post on designing web libraries and apps](https://alain.xyz/blog/designing-a-web-app) for more details on Node.js, packages, etc.
As your project becomes more complex, you'll want to separate files and organize your application to something more akin to a game or renderer, check out this post on [game engine architecture](https://alain.xyz/blog/game-engine-architecture) and this one on [real time renderer architecture](https://alain.xyz/blog/realtime-renderer-architectures) for more details.
[license-img]: https://img.shields.io/:license-unlicense-blue.svg?style=flat-square
[license-url]: https://unlicense.org/

View file

@ -0,0 +1,5 @@
export interface GameLoopSettings {
agentCount: number;
renderSpeed: number;
startingRadius: number;
}

View file

@ -1,12 +1,11 @@
import { Agent } from './pipelines/agents/agent'; import { Agent } from '../pipelines/agents/agent';
import { AgentPipeline } from './pipelines/agents/agent-pipeline'; import { AgentPipeline } from '../pipelines/agents/agent-pipeline';
import { BrushPipeline } from './pipelines/brush/brush-pipeline'; import { BrushPipeline } from '../pipelines/brush/brush-pipeline';
import { DiffusionPipeline } from './pipelines/diffusion/diffusion-pipeline'; import { DiffusionPipeline } from '../pipelines/diffusion/diffusion-pipeline';
import { RenderPipeline } from './pipelines/render/render-pipeline'; import { RenderPipeline } from '../pipelines/render/render-pipeline';
import { settings } from './settings'; import { settings } from '../settings';
import { DeltaTimeCalculator } from './utils/delta-time-calculator'; import { DeltaTimeCalculator } from '../utils/delta-time-calculator';
import { randomBetween } from './utils/random-between'; import { Random } from '../utils/random';
import { sleep } from './utils/sleep';
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
@ -77,8 +76,8 @@ export default class Renderer {
); );
vec2.normalize(size, size); vec2.normalize(size, size);
return new Array(settings.agentCount).fill(0).map(() => { return new Array(settings.agentCount).fill(0).map(() => {
const radius = randomBetween(0, settings.startingRadius / ratio); const radius = Random.randomBetween(0, settings.startingRadius / ratio);
const angle = randomBetween(0, Math.PI * 2); const angle = Random.randomBetween(0, Math.PI * 2);
const center = vec2.fromValues(0.5, 0.5); const center = vec2.fromValues(0.5, 0.5);
const delta = vec2.fromValues(Math.cos(angle) * radius, Math.sin(angle) * radius); const delta = vec2.fromValues(Math.cos(angle) * radius, Math.sin(angle) * radius);
@ -142,24 +141,20 @@ export default class Renderer {
private async render(time: DOMHighResTimeStamp) { private async render(time: DOMHighResTimeStamp) {
const deltaTime = this.deltaTimeCalculator.calculateDeltaTimeInSeconds(time); const deltaTime = this.deltaTimeCalculator.calculateDeltaTimeInSeconds(time);
this.agentPipeline.setParameters({ const params = {
...settings, canvasSize: vec2.fromValues(this.canvas.width, this.canvas.height),
width: this.canvas.width,
height: this.canvas.height,
time, time,
deltaTime, deltaTime,
});
this.brushPipeline.setParameters({
width: this.canvas.width,
height: this.canvas.height,
});
this.diffusionPipeline.setParameters({
...settings, ...settings,
width: this.canvas.width, };
height: this.canvas.height,
deltaTime, [
time, this.agentPipeline,
}); this.brushPipeline,
this.diffusionPipeline,
this.renderPipeline,
].forEach((pipeline) => pipeline.setParameters(params));
const commandEncoder = this.device.createCommandEncoder(); const commandEncoder = this.device.createCommandEncoder();
for (let i = 0; i < settings.renderSpeed; i++) { for (let i = 0; i < settings.renderSpeed; i++) {
@ -172,7 +167,7 @@ export default class Renderer {
this.queue.submit([commandEncoder.finish()]); this.queue.submit([commandEncoder.finish()]);
await sleep(200); // await sleep(200);
requestAnimationFrame(this.render.bind(this)); requestAnimationFrame(this.render.bind(this));
} }
} }

View file

@ -1,6 +1,5 @@
import './index.scss'; import Renderer from './game-loop/game-loop';
import Renderer from './renderer'; import './styles/index.scss';
import './utils/mulberry32';
const main = () => { const main = () => {
const canvas = document.querySelector('canvas'); const canvas = document.querySelector('canvas');

View file

@ -1,4 +1,6 @@
import { CommonParameters } from '../common-parameters';
import { AGENT_SIZE_IN_BYTES, Agent } from './agent'; import { AGENT_SIZE_IN_BYTES, Agent } from './agent';
import { AgentSettings } from './agent-settings';
import shader from './agent.wgsl'; import shader from './agent.wgsl';
export class AgentPipeline { export class AgentPipeline {
@ -54,34 +56,24 @@ export class AgentPipeline {
} }
public setParameters({ public setParameters({
width, canvasSize,
height,
trailWeight,
deltaTime, deltaTime,
time, time,
trailWeight,
moveSpeed, moveSpeed,
turnSpeed, turnSpeed,
sensorAngleDegrees, sensorAngleDegrees,
sensorOffsetDst, sensorOffsetDst,
}: { }: CommonParameters & AgentSettings) {
width: number;
height: number;
trailWeight: number;
deltaTime: number;
time: number;
moveSpeed: number;
turnSpeed: number;
sensorAngleDegrees: number;
sensorOffsetDst: number;
}) {
this.device.queue.writeBuffer( this.device.queue.writeBuffer(
this.uniforms, this.uniforms,
0, 0,
new Float32Array([ new Float32Array([
width, canvasSize[0],
height, canvasSize[1],
trailWeight, deltaTime,
time, time,
trailWeight,
moveSpeed * deltaTime, moveSpeed * deltaTime,
turnSpeed * deltaTime, turnSpeed * deltaTime,
(sensorAngleDegrees * Math.PI) / 180, (sensorAngleDegrees * Math.PI) / 180,

View file

@ -0,0 +1,7 @@
export interface AgentSettings {
trailWeight: number;
moveSpeed: number;
turnSpeed: number;
sensorAngleDegrees: number;
sensorOffsetDst: number;
}

View file

@ -5,12 +5,12 @@ struct Agent {
struct Settings { struct Settings {
size: vec2<f32>, size: vec2<f32>,
trailWeight : f32, deltaTime : f32,
time : f32, time : f32,
trailWeight : f32,
moveRate : f32, moveRate : f32,
turnRate : f32, turnRate : f32,
sensorAngle : f32, sensorAngle : f32,
sensorOffsetDst : f32, sensorOffsetDst : f32,
}; };

View file

@ -1,9 +1,11 @@
import { CommonParameters } from '../common-parameters';
import { BrushSettings } from './brush-settings';
import shader from './brush.wgsl'; import shader from './brush.wgsl';
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
export class BrushPipeline { export class BrushPipeline {
private static readonly UNIFORM_COUNT = 2; private static readonly UNIFORM_COUNT = 4;
private static readonly MAX_LINE_COUNT = 100; private static readonly MAX_LINE_COUNT = 100;
private static readonly VERTICES_PER_LINE_SEGMENT = 6; private static readonly VERTICES_PER_LINE_SEGMENT = 6;
@ -85,8 +87,16 @@ export class BrushPipeline {
this.linePoints.length = 0; this.linePoints.length = 0;
} }
public setParameters({ width, height }: { width: number; height: number }) { public setParameters({
this.device.queue.writeBuffer(this.uniforms, 0, new Float32Array([width, height])); canvasSize,
deltaTime,
time,
}: CommonParameters & BrushSettings) {
this.device.queue.writeBuffer(
this.uniforms,
0,
new Float32Array([canvasSize[0], canvasSize[1], deltaTime, time])
);
this.device.queue.writeBuffer( this.device.queue.writeBuffer(
this.vertexBuffer, this.vertexBuffer,

View file

@ -0,0 +1 @@
export interface BrushSettings {}

View file

@ -12,7 +12,9 @@ fn vertex(
} }
struct Settings { struct Settings {
size : vec2<f32> size : vec2<f32>,
deltaTime : f32,
time : f32
}; };
@group(0) @binding(0) var<uniform> settings : Settings; @group(0) @binding(0) var<uniform> settings : Settings;

View file

@ -0,0 +1,7 @@
import { vec2 } from 'gl-matrix';
export interface CommonParameters {
canvasSize: vec2;
deltaTime: number;
time: number;
}

View file

@ -1,9 +1,10 @@
struct Settings { struct Settings {
size : vec2<f32>, size : vec2<f32>,
diffusionRate : f32,
decayRate : f32,
deltaTime : f32, deltaTime : f32,
time : f32, time : f32,
diffusionRate : f32,
decayRate : f32,
swipeRadius : f32, swipeRadius : f32,
swipeBlur : f32, swipeBlur : f32,
}; };

View file

@ -1,5 +1,7 @@
import { setUpFullScreenQuad } from '../../utils/full-screen-quad'; import { setUpFullScreenQuad } from '../../utils/full-screen-quad';
import { CommonParameters } from '../common-parameters';
import shader from './diffuse.wgsl'; import shader from './diffuse.wgsl';
import { DiffusionSettings } from './diffusion-settings';
export class DiffusionPipeline { export class DiffusionPipeline {
private static readonly UNIFORM_COUNT = 16; private static readonly UNIFORM_COUNT = 16;
@ -41,34 +43,24 @@ export class DiffusionPipeline {
} }
public setParameters({ public setParameters({
width, canvasSize,
height,
diffusionRate,
decayRate,
deltaTime, deltaTime,
time, time,
diffusionRate,
decayRate,
swipeRadius, swipeRadius,
swipeBlur, swipeBlur,
}: { }: CommonParameters & DiffusionSettings) {
width: number;
height: number;
diffusionRate: number;
decayRate: number;
deltaTime: number;
time: number;
swipeRadius: number;
swipeBlur: number;
}) {
this.device.queue.writeBuffer( this.device.queue.writeBuffer(
this.uniforms, this.uniforms,
0, 0,
new Float32Array([ new Float32Array([
width, canvasSize[0],
height, canvasSize[1],
diffusionRate,
decayRate,
deltaTime, deltaTime,
time, time,
diffusionRate,
decayRate,
swipeRadius, swipeRadius,
swipeBlur, swipeBlur,
]) ])

View file

@ -0,0 +1,6 @@
export interface DiffusionSettings {
diffusionRate: number;
decayRate: number;
swipeRadius: number;
swipeBlur: number;
}

View file

@ -1,8 +1,13 @@
import { setUpFullScreenQuad } from '../../utils/full-screen-quad'; import { setUpFullScreenQuad } from '../../utils/full-screen-quad';
import { CommonParameters } from '../common-parameters';
import { RenderSettings } from './render-settings';
import shader from './render.wgsl'; import shader from './render.wgsl';
export class RenderPipeline { export class RenderPipeline {
private static readonly UNIFORM_COUNT = 4;
private readonly pipeline: GPURenderPipeline; private readonly pipeline: GPURenderPipeline;
private readonly uniforms: GPUBuffer;
private readonly quadVertexBuffer: GPUBuffer; private readonly quadVertexBuffer: GPUBuffer;
private bindGroup?: GPUBindGroup; private bindGroup?: GPUBindGroup;
@ -34,6 +39,23 @@ export class RenderPipeline {
topology: 'triangle-strip', topology: 'triangle-strip',
}, },
}); });
this.uniforms = this.device.createBuffer({
size: RenderPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
}
public setParameters({
canvasSize,
deltaTime,
time,
}: CommonParameters & RenderSettings) {
this.device.queue.writeBuffer(
this.uniforms,
0,
new Float32Array([canvasSize[0], canvasSize[1], deltaTime, time])
);
} }
public execute(commandEncoder: GPUCommandEncoder, colorTexture: GPUTexture) { public execute(commandEncoder: GPUCommandEncoder, colorTexture: GPUTexture) {
@ -64,13 +86,19 @@ export class RenderPipeline {
entries: [ entries: [
{ {
binding: 0, binding: 0,
resource: {
buffer: this.uniforms,
},
},
{
binding: 1,
resource: this.device.createSampler({ resource: this.device.createSampler({
magFilter: 'linear', magFilter: 'linear',
minFilter: 'linear', minFilter: 'linear',
}), }),
}, },
{ {
binding: 1, binding: 2,
resource: colorTexture.createView(), resource: colorTexture.createView(),
}, },
], ],

View file

@ -0,0 +1 @@
export interface RenderSettings {}

View file

@ -1,8 +1,14 @@
@group(0) @binding(0) var mySampler: sampler; struct Settings {
@group(0) @binding(1) var TargetTexture : texture_2d<f32>; size : vec2<f32>,
deltaTime : f32,
time : f32,
};
@group(0) @binding(0) var<uniform> settings : Settings;
@group(0) @binding(1) var mySampler: sampler;
@group(0) @binding(2) var TargetTexture : texture_2d<f32>;
@fragment @fragment
fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> { fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
return vec4(textureSample(TargetTexture, mySampler, uv).rgb * 1.0, 1); return vec4(textureSample(TargetTexture, mySampler, uv).r * 1.0, settings.deltaTime * 0.0, 0.0, 1.0);
} }

View file

@ -1,32 +1,27 @@
interface Settings { import { GameLoopSettings } from './game-loop/game-loop-settings';
agentCount: number; import { AgentSettings } from './pipelines/agents/agent-settings';
renderSpeed: number; import { BrushSettings } from './pipelines/brush/brush-settings';
startingRadius: number; import { DiffusionSettings } from './pipelines/diffusion/diffusion-settings';
trailWeight: number; import { RenderSettings } from './pipelines/render/render-settings';
decayRate: number;
diffusionRate: number;
moveSpeed: number;
turnSpeed: number;
sensorAngleDegrees: number;
sensorOffsetDst: number;
swipeRadius: number;
swipeBlur: number;
}
export const settings: Settings = { export const settings: GameLoopSettings &
AgentSettings &
BrushSettings &
DiffusionSettings &
RenderSettings = {
agentCount: 1_000, agentCount: 1_000,
renderSpeed: 1, renderSpeed: 1,
startingRadius: 0.15, startingRadius: 0.15,
decayRate: 0.02,
diffusionRate: 0.8,
trailWeight: 5, trailWeight: 5,
moveSpeed: 0.025, moveSpeed: 0.025,
turnSpeed: 6, turnSpeed: 6,
sensorAngleDegrees: 30, sensorAngleDegrees: 30,
sensorOffsetDst: 0.025, sensorOffsetDst: 0.025,
decayRate: 0.02,
diffusionRate: 0.8,
swipeRadius: 0.003, swipeRadius: 0.003,
swipeBlur: 0.002, swipeBlur: 0.002,
}; };

25
src/styles/mixins.scss Normal file
View file

@ -0,0 +1,25 @@
@mixin card {
border: 2px solid white;
border-radius: 12px;
backdrop-filter: blur(24px);
@supports not (backdrop-filter: blur(24px)) {
background-color: rgba(0, 0, 0, 0.15);
}
&:focus {
outline: none;
border: 4px solid white;
}
}
@mixin center-children {
display: flex;
justify-content: center;
align-items: center;
}
@mixin square($size) {
width: $size;
height: $size;
}

4
src/utils/clamp.ts Normal file
View file

@ -0,0 +1,4 @@
export const clamp = (value: number, min: number, max: number): number =>
Math.min(max, Math.max(min, value));
export const clamp01 = (value: number): number => Math.min(1, Math.max(0, value));

View file

@ -0,0 +1,48 @@
export const handleFullScreen = (
minimizeButton: HTMLElement,
maximizeButton: HTMLElement,
target: HTMLElement
) => {
if (!document.fullscreenEnabled) {
minimizeButton.style.visibility = 'hidden';
maximizeButton.style.visibility = 'hidden';
return;
}
const isInFullScreen = (): boolean => document.fullscreenElement !== null;
const showButtons = () => {
minimizeButton.style.visibility = isInFullScreen() ? 'visible' : 'hidden';
maximizeButton.style.visibility = isInFullScreen() ? 'hidden' : 'visible';
};
showButtons();
let currentWindowHeight = innerHeight;
const followToggle = () => {
showButtons();
currentWindowHeight = innerHeight;
};
const triggerToggle = async () => {
await (isInFullScreen() ? document.exitFullscreen() : target.requestFullscreen());
followToggle();
};
addEventListener('keydown', (e) => {
if (e.key === 'F11') {
triggerToggle();
e.preventDefault();
}
});
addEventListener('resize', () => {
if (isInFullScreen() && currentWindowHeight > innerHeight) {
followToggle();
}
});
maximizeButton.addEventListener('click', triggerToggle);
minimizeButton.addEventListener('click', triggerToggle);
};

3
src/utils/last.ts Normal file
View file

@ -0,0 +1,3 @@
export function last<T>(a: Array<T>): T | null {
return a.length > 0 ? a[a.length - 1] : null;
}

View file

@ -1,8 +0,0 @@
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,4 @@
export const prettyPrint = (o: any): string =>
JSON.stringify(o, (_, v) => (v?.toFixed ? Number(v.toFixed(3)) : v), ' ')
.replace(/("|,|{|^\n)/g, '')
.replace(/(\W*}\n?)+/g, '\n\n');

View file

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

20
src/utils/random.ts Normal file
View file

@ -0,0 +1,20 @@
export abstract class Random {
// https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript, Mulberry32
private static _seed = 42;
public static set seed(value: number) {
Random._seed = value;
}
public static getRandom(): number {
let t = (Random._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;
}
public static randomBetween(from: number, to: number): number {
return from + Random.getRandom() * (to - from);
}
}