This commit is contained in:
Andras Schmelczer 2023-04-29 11:58:14 +01:00
parent de7fcc15d0
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 { AgentPipeline } from './pipelines/agents/agent-pipeline';
import { BrushPipeline } from './pipelines/brush/brush-pipeline';
import { DiffusionPipeline } from './pipelines/diffusion/diffusion-pipeline';
import { RenderPipeline } from './pipelines/render/render-pipeline';
import { settings } from './settings';
import { DeltaTimeCalculator } from './utils/delta-time-calculator';
import { randomBetween } from './utils/random-between';
import { sleep } from './utils/sleep';
import { Agent } from '../pipelines/agents/agent';
import { AgentPipeline } from '../pipelines/agents/agent-pipeline';
import { BrushPipeline } from '../pipelines/brush/brush-pipeline';
import { DiffusionPipeline } from '../pipelines/diffusion/diffusion-pipeline';
import { RenderPipeline } from '../pipelines/render/render-pipeline';
import { settings } from '../settings';
import { DeltaTimeCalculator } from '../utils/delta-time-calculator';
import { Random } from '../utils/random';
import { vec2 } from 'gl-matrix';
@ -77,8 +76,8 @@ export default class Renderer {
);
vec2.normalize(size, size);
return new Array(settings.agentCount).fill(0).map(() => {
const radius = randomBetween(0, settings.startingRadius / ratio);
const angle = randomBetween(0, Math.PI * 2);
const radius = Random.randomBetween(0, settings.startingRadius / ratio);
const angle = Random.randomBetween(0, Math.PI * 2);
const center = vec2.fromValues(0.5, 0.5);
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) {
const deltaTime = this.deltaTimeCalculator.calculateDeltaTimeInSeconds(time);
this.agentPipeline.setParameters({
...settings,
width: this.canvas.width,
height: this.canvas.height,
const params = {
canvasSize: vec2.fromValues(this.canvas.width, this.canvas.height),
time,
deltaTime,
});
this.brushPipeline.setParameters({
width: this.canvas.width,
height: this.canvas.height,
});
this.diffusionPipeline.setParameters({
...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();
for (let i = 0; i < settings.renderSpeed; i++) {
@ -172,7 +167,7 @@ export default class Renderer {
this.queue.submit([commandEncoder.finish()]);
await sleep(200);
// await sleep(200);
requestAnimationFrame(this.render.bind(this));
}
}

View file

@ -1,6 +1,5 @@
import './index.scss';
import Renderer from './renderer';
import './utils/mulberry32';
import Renderer from './game-loop/game-loop';
import './styles/index.scss';
const main = () => {
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 { AgentSettings } from './agent-settings';
import shader from './agent.wgsl';
export class AgentPipeline {
@ -54,34 +56,24 @@ export class AgentPipeline {
}
public setParameters({
width,
height,
trailWeight,
canvasSize,
deltaTime,
time,
trailWeight,
moveSpeed,
turnSpeed,
sensorAngleDegrees,
sensorOffsetDst,
}: {
width: number;
height: number;
trailWeight: number;
deltaTime: number;
time: number;
moveSpeed: number;
turnSpeed: number;
sensorAngleDegrees: number;
sensorOffsetDst: number;
}) {
}: CommonParameters & AgentSettings) {
this.device.queue.writeBuffer(
this.uniforms,
0,
new Float32Array([
width,
height,
trailWeight,
canvasSize[0],
canvasSize[1],
deltaTime,
time,
trailWeight,
moveSpeed * deltaTime,
turnSpeed * deltaTime,
(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 {
size: vec2<f32>,
trailWeight : f32,
deltaTime : f32,
time : f32,
trailWeight : f32,
moveRate : f32,
turnRate : f32,
sensorAngle : 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 { vec2 } from 'gl-matrix';
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 VERTICES_PER_LINE_SEGMENT = 6;
@ -85,8 +87,16 @@ export class BrushPipeline {
this.linePoints.length = 0;
}
public setParameters({ width, height }: { width: number; height: number }) {
this.device.queue.writeBuffer(this.uniforms, 0, new Float32Array([width, height]));
public setParameters({
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.vertexBuffer,

View file

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

View file

@ -12,7 +12,9 @@ fn vertex(
}
struct Settings {
size : vec2<f32>
size : vec2<f32>,
deltaTime : f32,
time : f32
};
@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 {
size : vec2<f32>,
diffusionRate : f32,
decayRate : f32,
deltaTime : f32,
time : f32,
diffusionRate : f32,
decayRate : f32,
swipeRadius : f32,
swipeBlur : f32,
};

View file

@ -1,5 +1,7 @@
import { setUpFullScreenQuad } from '../../utils/full-screen-quad';
import { CommonParameters } from '../common-parameters';
import shader from './diffuse.wgsl';
import { DiffusionSettings } from './diffusion-settings';
export class DiffusionPipeline {
private static readonly UNIFORM_COUNT = 16;
@ -41,34 +43,24 @@ export class DiffusionPipeline {
}
public setParameters({
width,
height,
diffusionRate,
decayRate,
canvasSize,
deltaTime,
time,
diffusionRate,
decayRate,
swipeRadius,
swipeBlur,
}: {
width: number;
height: number;
diffusionRate: number;
decayRate: number;
deltaTime: number;
time: number;
swipeRadius: number;
swipeBlur: number;
}) {
}: CommonParameters & DiffusionSettings) {
this.device.queue.writeBuffer(
this.uniforms,
0,
new Float32Array([
width,
height,
diffusionRate,
decayRate,
canvasSize[0],
canvasSize[1],
deltaTime,
time,
diffusionRate,
decayRate,
swipeRadius,
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 { CommonParameters } from '../common-parameters';
import { RenderSettings } from './render-settings';
import shader from './render.wgsl';
export class RenderPipeline {
private static readonly UNIFORM_COUNT = 4;
private readonly pipeline: GPURenderPipeline;
private readonly uniforms: GPUBuffer;
private readonly quadVertexBuffer: GPUBuffer;
private bindGroup?: GPUBindGroup;
@ -34,6 +39,23 @@ export class RenderPipeline {
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) {
@ -64,13 +86,19 @@ export class RenderPipeline {
entries: [
{
binding: 0,
resource: {
buffer: this.uniforms,
},
},
{
binding: 1,
resource: this.device.createSampler({
magFilter: 'linear',
minFilter: 'linear',
}),
},
{
binding: 1,
binding: 2,
resource: colorTexture.createView(),
},
],

View file

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

View file

@ -1,8 +1,14 @@
@group(0) @binding(0) var mySampler: sampler;
@group(0) @binding(1) var TargetTexture : texture_2d<f32>;
struct Settings {
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
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 {
agentCount: number;
renderSpeed: number;
startingRadius: number;
trailWeight: number;
decayRate: number;
diffusionRate: number;
moveSpeed: number;
turnSpeed: number;
sensorAngleDegrees: number;
sensorOffsetDst: number;
swipeRadius: number;
swipeBlur: number;
}
import { GameLoopSettings } from './game-loop/game-loop-settings';
import { AgentSettings } from './pipelines/agents/agent-settings';
import { BrushSettings } from './pipelines/brush/brush-settings';
import { DiffusionSettings } from './pipelines/diffusion/diffusion-settings';
import { RenderSettings } from './pipelines/render/render-settings';
export const settings: Settings = {
export const settings: GameLoopSettings &
AgentSettings &
BrushSettings &
DiffusionSettings &
RenderSettings = {
agentCount: 1_000,
renderSpeed: 1,
startingRadius: 0.15,
decayRate: 0.02,
diffusionRate: 0.8,
trailWeight: 5,
moveSpeed: 0.025,
turnSpeed: 6,
sensorAngleDegrees: 30,
sensorOffsetDst: 0.025,
decayRate: 0.02,
diffusionRate: 0.8,
swipeRadius: 0.003,
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);
}
}