Refactor
This commit is contained in:
parent
de7fcc15d0
commit
9e582110ea
27 changed files with 248 additions and 123 deletions
19
README.md
19
README.md
|
|
@ -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/
|
||||
-
|
||||
|
|
|
|||
5
src/game-loop/game-loop-settings.ts
Normal file
5
src/game-loop/game-loop-settings.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export interface GameLoopSettings {
|
||||
agentCount: number;
|
||||
renderSpeed: number;
|
||||
startingRadius: number;
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
7
src/pipelines/agents/agent-settings.ts
Normal file
7
src/pipelines/agents/agent-settings.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export interface AgentSettings {
|
||||
trailWeight: number;
|
||||
moveSpeed: number;
|
||||
turnSpeed: number;
|
||||
sensorAngleDegrees: number;
|
||||
sensorOffsetDst: number;
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
1
src/pipelines/brush/brush-settings.ts
Normal file
1
src/pipelines/brush/brush-settings.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export interface BrushSettings {}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
7
src/pipelines/common-parameters.ts
Normal file
7
src/pipelines/common-parameters.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
export interface CommonParameters {
|
||||
canvasSize: vec2;
|
||||
deltaTime: number;
|
||||
time: number;
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
])
|
||||
|
|
|
|||
6
src/pipelines/diffusion/diffusion-settings.ts
Normal file
6
src/pipelines/diffusion/diffusion-settings.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export interface DiffusionSettings {
|
||||
diffusionRate: number;
|
||||
decayRate: number;
|
||||
swipeRadius: number;
|
||||
swipeBlur: number;
|
||||
}
|
||||
|
|
@ -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(),
|
||||
},
|
||||
],
|
||||
|
|
|
|||
1
src/pipelines/render/render-settings.ts
Normal file
1
src/pipelines/render/render-settings.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export interface RenderSettings {}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
25
src/styles/mixins.scss
Normal 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
4
src/utils/clamp.ts
Normal 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));
|
||||
48
src/utils/handle-full-screen.ts
Normal file
48
src/utils/handle-full-screen.ts
Normal 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
3
src/utils/last.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function last<T>(a: Array<T>): T | null {
|
||||
return a.length > 0 ? a[a.length - 1] : null;
|
||||
}
|
||||
|
|
@ -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);
|
||||
4
src/utils/pretty-print.ts
Normal file
4
src/utils/pretty-print.ts
Normal 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');
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export const randomBetween = (min: number, max: number) => {
|
||||
return Math.random() * (max - min) + min;
|
||||
};
|
||||
20
src/utils/random.ts
Normal file
20
src/utils/random.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue