ok webgpu sucks lets jump back to webgl

This commit is contained in:
41666 2023-10-08 23:36:54 -04:00
parent 000f35f19d
commit 87563cb9e3
41 changed files with 587 additions and 6925 deletions

View file

@ -1,4 +1,5 @@
import { Telemetry } from "../renderer/telemetry";
import { mat4 } from "gl-matrix";
export class App {
constructor(
@ -35,21 +36,11 @@ export class App {
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 0.1;
const zFar = 100.0;
const projectionMatrix = glMatrix.mat4.create();
glMatrix.mat4.perspective(
projectionMatrix,
fieldOfView,
aspect,
zNear,
zFar
);
const projectionMatrix = mat4.create();
mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar);
const modelViewMatrix = glMatrix.mat4.create();
glMatrix.mat4.translate(
modelViewMatrix,
modelViewMatrix,
[-0.0, 0.0, -6.0]
);
const modelViewMatrix = mat4.create();
mat4.translate(modelViewMatrix, modelViewMatrix, [-0.0, 0.0, -6.0]);
this.projectionMatrix = projectionMatrix;
this.modelViewMatrix = modelViewMatrix;

View file

@ -0,0 +1,43 @@
import { MeshRenderer } from "../renderer/mesh-renderer";
import plane from "../meshes/plane";
import { WebGLApp } from "../renderer/webgl";
import { Renderable } from "../renderer/renderable";
import { Transform } from "../renderer/transform";
import { uvRainbow } from "../common-shaders/uv-rainbow";
import { quat } from "gl-matrix";
import torus from "../meshes/torus";
const app = new WebGLApp({ fov: 45 });
const camera = new Transform(
[0, 0, -6],
quat.fromEuler(quat.create(), 15, 0, 0)
);
(window as any).ANGLE_X = 15;
(window as any).ANGLE_Y = 0;
(window as any).ANGLE_Z = 0;
app.onUpdate((time: number) => {
const stride = 2;
const x = Math.sin(time * 0.0001) * (stride * 2 - stride * 0.5);
// const y = Math.tan(time * 0.001) * (stride * 2 - stride * 0.5);
camera.rotation = quat.fromEuler(quat.create(), x, 0, 0);
});
new Renderable(
app,
new Transform([0, 0, 4]),
new MeshRenderer(app, torus, uvRainbow(app), camera, {
drawMode: app.gl.TRIANGLE_FAN,
})
);
// new Renderable(
// app,
// new Transform([1, 0, 0]),
// new MeshRenderer(app, plane, uvRainbow(app))
// );
app.start();

View file

@ -1,12 +0,0 @@
import { WebGPUApp } from "../renderer/webgpu";
import { MeshRenderer } from "../renderer/mesh-renderer";
import plane from "../meshes/plane";
import rainbowPlane from "./rainbow-plane.wgsl";
import { Shader } from "../renderer/shader";
const app = new WebGPUApp({ fov: 20 });
const shader = new Shader(rainbowPlane);
const renderer = new MeshRenderer(app, plane, shader);
app.start();

View file

@ -1,17 +0,0 @@
#include "../color-conv.wgsl"
#include "../uniforms.wgsl"
#include "../basic-vert.wgsl"
@fragment
fn main(
@location(0) uv : vec2f,
) -> @location(0) vec4f {
f32 z = sin(uniforms.time) * 0.001 * 0.5 + 0.5;
vec3f hsv = vec3f(uv.x, uv.y, z);
hsv.x += uniforms.time * 0.0001;
hsv.y = 1.0;
hsv.z = 1.0;
vec3f rgb = hsv2rgb(hsv);
return saturate(vec4f(rgb, 1.0));
}

View file

@ -1,14 +0,0 @@
struct v2f {
@builtin(position) position : vec4f,
@location(0) color : vec4f,
@location(1) uv : vec2f,
}
@vertex
fn main(
@builtin(position) position : vec4f,
@location(0) color : vec4f,
@location(1) uv : vec2f,
) -> v2f {
return v2f(uniforms.modelViewProjectionMatrix * position, color, uv);
}

View file

@ -1,15 +0,0 @@
fn rgb2hsv(vec3f c) -> vec3f {
vec4f K = vec4f(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4f p = mix(vec4f(c.bg, K.wz), vec4f(c.gb, K.xy), step(c.b, c.g));
vec4f q = mix(vec4f(p.xyw, c.r), vec4f(c.r, p.yzx), step(p.x, c.r));
f32 d = q.x - min(q.w, q.y);
f32 e = 1.0e-10;
return vec3f(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
fn hsv2rgb(vec3f c) -> vec3f {
vec4f K = vec4f(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3f p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

View file

@ -0,0 +1,5 @@
precision highp float;
void main() {
gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);
}

View file

@ -0,0 +1,7 @@
import { Shader } from "../renderer/shader";
import { WebGLApp } from "../renderer/webgl";
import frag from "./error.frag";
import vert from "./error.vert";
export const errorShader = (app: WebGLApp) =>
new Shader().vertex(vert).fragment(frag).app(app);

View file

@ -0,0 +1,8 @@
attribute vec4 aVertexPosition;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
void main() {
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
}

View file

@ -0,0 +1,35 @@
precision highp float;
uniform float uTime;
uniform float uSinTime;
uniform float uCosTime;
varying highp vec2 vTextureCoord;
vec3 rgb2hsv(vec3 c) {
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void main() {
float zComponent = uSinTime * 0.001 * 0.5 + 0.5;
vec3 hsv = rgb2hsv(vec3(vTextureCoord, zComponent));
hsv.x += uTime * 0.0001;
hsv.y = 1.0;
hsv.z = 1.0;
vec3 rgb = hsv2rgb(hsv);
gl_FragColor = vec4(rgb, 1.0);
gl_FragColor = clamp(gl_FragColor, 0.0, 1.0);
}

View file

@ -0,0 +1,7 @@
import { Shader } from "../renderer/shader";
import { WebGLApp } from "../renderer/webgl";
import frag from "./uv-rainbow.frag";
import vert from "./uv-rainbow.vert";
export const uvRainbow = (app: WebGLApp) =>
new Shader({ time: true }).vertex(vert).fragment(frag).app(app);

View file

@ -0,0 +1,12 @@
attribute vec4 aVertexPosition;
attribute vec2 aTextureCoord;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying highp vec2 vTextureCoord;
void main() {
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
vTextureCoord = aTextureCoord;
}

View file

@ -11,7 +11,10 @@ const mesh = new Float32Array([
export default new Mesh({
mesh,
positionSize: 4 * 4,
colorSize: 4 * 4,
uvSize: 2 * 4,
positionSize: 4,
colorSize: 4,
uvSize: 2,
vertexCount: 4,
stride: 10,
name: "plane",
});

File diff suppressed because one or more lines are too long

View file

@ -1,11 +1,11 @@
import { WebGPUApp } from "./webgpu";
import { WebGLApp } from "./webgl";
export abstract class Behavior {
onStart?(...args: any[]): void;
onBeforeUpdate?(...args: any[]): void;
onUpdate?(...args: any[]): void;
onAfterUpdate?(...args: any[]): void;
constructor(public app: WebGPUApp) {
constructor(public app: WebGLApp) {
this.onStart && app.onStart(this.onStart.bind(this));
this.onUpdate && app.onUpdate(this.onUpdate.bind(this));
this.onAfterUpdate && app.onAfterUpdate(this.onAfterUpdate.bind(this));

View file

@ -1,146 +1,136 @@
import { Mat4, mat4, vec3 } from "wgpu-matrix";
import { mat4, vec3 } from "gl-matrix";
import { Behavior } from "./behavior";
import { Mesh } from "./mesh";
import { Shader } from "./shader";
import { WebGPUApp } from "./webgpu";
import { WebGLApp } from "./webgl";
import { Transform } from "./transform";
import { errorShader } from "../common-shaders/error";
export type MeshRendererConfig = {
drawMode?: number;
};
export class MeshRenderer extends Behavior {
private depthTexture?: GPUTexture;
private uniformBuffer?: GPUBuffer;
private vertexBuffer?: GPUBuffer;
private texture?: GPUTexture;
private sampler?: GPUSampler;
private uniformBindGroup?: GPUBindGroup;
private renderPassDescriptor?: GPURenderPassDescriptor;
private pipeline?: GPURenderPipeline;
private viewMatrix = mat4.translate(
mat4.identity(),
vec3.fromValues(0, 0, -4)
);
private projectionMatrix = mat4.perspective(
2 * Math.PI * 0.2,
1920 / 1080,
1,
100
);
private modelMatrix = mat4.create();
private projectionMatrix = mat4.create();
private _meshBuffer?: WebGLBuffer;
constructor(
public app: WebGPUApp,
public app: WebGLApp,
public mesh: Mesh,
public shader: Shader,
public textures?: any[]
public camera: Transform = new Transform([0, 0, -6]),
public config: MeshRendererConfig = {}
) {
super(app);
}
get meshBuffer() {
if (this._meshBuffer) {
return this._meshBuffer;
}
throw new Error("mesh buffer not ready");
}
initializeMeshBuffer() {
const gl = this.app.gl;
const buffer = gl.createBuffer();
if (!buffer) {
throw new Error("failed to create mesh buffer");
}
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, this.mesh.config.mesh, gl.STATIC_DRAW);
this._meshBuffer = buffer;
}
initializeAttributes() {
const gl = this.app.gl;
gl.bindBuffer(gl.ARRAY_BUFFER, this.meshBuffer);
const positionLocation = this.shader.attrib("aVertexPosition");
if (positionLocation !== -1) {
gl.vertexAttribPointer(
positionLocation,
this.mesh.config.positionSize,
gl.FLOAT,
false,
4 * this.mesh.config.stride,
0
);
gl.enableVertexAttribArray(positionLocation);
this.shader.bindAttrib(positionLocation, "aVertexPosition");
}
if (this.mesh.config.colorSize !== 0) {
const colorLocation = this.shader.attrib("aVertexColor");
if (colorLocation !== -1) {
gl.vertexAttribPointer(
colorLocation,
this.mesh.config.colorSize,
gl.FLOAT,
false,
4 * this.mesh.config.stride,
4 * this.mesh.config.positionSize
);
gl.enableVertexAttribArray(colorLocation);
this.shader.bindAttrib(colorLocation, "aVertexColor");
}
}
const uvLocation = this.shader.attrib("aTextureCoord");
if (uvLocation !== -1) {
gl.vertexAttribPointer(
uvLocation,
this.mesh.config.uvSize,
gl.FLOAT,
false,
4 * this.mesh.config.stride,
4 * (this.mesh.config.positionSize + this.mesh.config.colorSize)
);
gl.enableVertexAttribArray(uvLocation);
this.shader.bindAttrib(uvLocation, "aTextureCoord");
}
}
onStart() {
this.projectionMatrix = mat4.perspective(
2 * Math.PI * 0.2,
mat4.perspective(
this.projectionMatrix,
this.app.config.fov || 45,
this.app.canvas.width / this.app.canvas.height,
1,
100
this.app.config.zNear || 0.1,
this.app.config.zFar || 100
);
this.depthTexture = this.app.device.createTexture({
size: [this.app.canvas.width, this.app.canvas.height],
format: "depth24plus",
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
// float32x4x4 + float32
this.uniformBuffer = this.app.device.createBuffer({
size: 4 * 16 + 4,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
this.pipeline = this.mesh.pipeline(this.app, this.shader, {});
this.uniformBindGroup = this.app.device.createBindGroup({
layout: this.pipeline.getBindGroupLayout(0),
entries: [
{
binding: 0,
resource: {
buffer: this.uniformBuffer,
},
},
],
});
this.renderPassDescriptor = {
colorAttachments: [
// {
// view: undefined as any, // defined in onUpdate
// clearValue: { r: 1, g: 0, b: 1, a: 1 },
// loadOp: "clear",
// storeOp: "store",
// },
],
depthStencilAttachment: {
view: this.depthTexture.createView(),
depthClearValue: 1.0,
depthLoadOp: "clear",
depthStoreOp: "store",
},
};
this.vertexBuffer = this.mesh.buffer(this.app);
this.shader.compile();
this.initializeMeshBuffer();
this.initializeAttributes();
this.shader.link();
}
private writeUniforms(modelViewProjection: Mat4, time: number) {
if (!this.uniformBuffer) {
return;
}
const {
device: { queue },
} = this.app;
const mvpBuf = modelViewProjection as Float32Array;
queue.writeBuffer(
this.uniformBuffer,
onRenderableUpdate(time: number, transform: Transform) {
const gl = this.app.gl;
this.shader.use();
gl.bindBuffer(gl.ARRAY_BUFFER, this.meshBuffer);
this.shader.setupUniforms(
time,
this.projectionMatrix,
transform,
this.camera
);
gl.drawArrays(
this.config.drawMode ?? gl.TRIANGLE_STRIP,
0,
mvpBuf.buffer,
mvpBuf.byteOffset,
mvpBuf.length
this.mesh.config.vertexCount
);
const timeBuf = new Float32Array([time]);
queue.writeBuffer(
this.uniformBuffer,
mvpBuf.length + 1,
timeBuf.buffer,
timeBuf.byteOffset,
timeBuf.byteLength
);
}
onUpdate(time: number) {
if (
!this.renderPassDescriptor ||
!this.pipeline ||
!this.uniformBindGroup ||
!this.vertexBuffer
) {
return;
const err = gl.getError();
if (err !== 0) {
console.log({ err });
throw new Error(
`(MeshRenderer<Mesh#${this.mesh.name}>) webgl failure: ${err}`
);
}
const mvp = mat4.multiply(this.projectionMatrix, this.viewMatrix);
this.writeUniforms(mvp, time);
const { device } = this.app;
const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginRenderPass(
this.renderPassDescriptor
);
passEncoder.setPipeline(this.pipeline);
passEncoder.setBindGroup(0, this.uniformBindGroup);
passEncoder.setVertexBuffer(0, this.vertexBuffer);
passEncoder.draw(this.mesh.config.vertexCount);
passEncoder.end();
this.app.commit(commandEncoder.finish());
}
}

View file

@ -1,80 +1,17 @@
import { Oops, Shader } from "./shader";
import { WebGPUApp } from "./webgpu";
export type MeshConfig = {
mesh: Float32Array;
positionSize: number;
colorSize: number;
uvSize: number;
vertexCount: number;
stride: number;
name: string;
};
export class Mesh {
constructor(public config: MeshConfig) {}
buffer(app: WebGPUApp) {
const buffer = app.device.createBuffer({
size: this.config.mesh.byteLength,
usage: GPUBufferUsage.VERTEX,
mappedAtCreation: true,
});
new Float32Array(buffer.getMappedRange()).set(this.config.mesh);
buffer.unmap();
return buffer;
}
pipeline(app: WebGPUApp, shader: Shader, config: Record<string, any>) {
const module = shader.module(app);
return app.device.createRenderPipeline({
layout: "auto",
vertex: {
module,
entryPoint: "main",
buffers: [
{
arrayStride: 4,
attributes: [
{
// position
shaderLocation: 0,
offset: 0,
format: "float32x4",
},
{
// color
shaderLocation: 2,
offset: this.config.positionSize,
format: "float32x4",
},
{
// uv
shaderLocation: 1,
offset: this.config.positionSize + this.config.colorSize,
format: "float32x4",
},
],
},
],
},
fragment: {
module,
entryPoint: "main",
targets: [
{
format: "rgba8unorm",
},
],
},
primitive: {
topology: "triangle-list",
cullMode: config.cullMode ?? "back",
},
depthStencil: config.stencil && {
depthWriteEnabled: true,
depthCompare: "less",
format: "depth24plus",
},
});
get name() {
return this.config.name;
}
}

View file

@ -1,26 +0,0 @@
struct Uniforms {
modelViewProjectionMatrix: mat4x4<f32>,
time: f32,
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
struct v2f {
@builtin(position) position : vec4f,
@location(0) uv : vec2f,
}
@vertex
fn vertex_main(
@builtin(position) position : vec4f,
@location(0) uv : vec2f,
) -> v2f {
return v2f(uniforms.modelViewProjectionMatrix * position, uv);
}
@fragment
fn fragment_main(
@location(0) uv : vec2f,
) -> vec4f {
return vec4f(1.0, 0.0, 1.0, 1.0);
}

View file

@ -0,0 +1,18 @@
import { Behavior } from "./behavior";
import { MeshRenderer } from "./mesh-renderer";
import { Transform } from "./transform";
import { WebGLApp } from "./webgl";
export class Renderable extends Behavior {
constructor(
public app: WebGLApp,
public transform: Transform,
public renderer: MeshRenderer
) {
super(app);
}
onUpdate(time: number) {
this.renderer.onRenderableUpdate(time, this.transform);
}
}

View file

@ -1,38 +1,132 @@
import { WebGPUApp } from "./webgpu";
import oopsWsgl from "./oops.wgsl";
import { mat4, vec3 } from "gl-matrix";
import { Transform } from "./transform";
import { WebGLApp } from "./webgl";
const pragmaRegexp = new RegExp("#pragma ([a-z]+) ([a-zA-Z_0-9]+)", "g");
export type ShaderConfig = {
time?: boolean;
};
export class Shader {
private _module: GPUShaderModule | null = null;
public vertexMain: string = "main";
public fragmentMain: string = "main";
public code: string;
static VERTEX = 35633;
static FRAGMENT = 35632;
constructor(...code: string[]) {
this.code = code.join("\n");
constructor(private config: ShaderConfig = {}) {}
// pragma preprocessing
const matches = this.code.matchAll(pragmaRegexp);
for (const match of matches || []) {
switch (match[1]) {
case "fragment":
this.fragmentMain = match[2];
break;
case "vertex":
this.vertexMain = match[2];
break;
}
private vertexCode = "";
private fragmentCode = "";
private _app?: WebGLApp;
private _program: WebGLProgram | null = null;
get gl() {
const gl = this._app?.gl;
if (!gl) {
throw new Error("GL context not defined at shader compile time.");
}
return gl;
}
app(app: WebGLApp) {
this._app = app;
this._program = app.gl.createProgram();
return this;
}
vertex(code: string) {
this.vertexCode = code;
return this;
}
fragment(code: string) {
this.fragmentCode = code;
return this;
}
attrib(name: string) {
return this.gl.getAttribLocation(this._program as WebGLProgram, name);
}
uniform(name: string) {
return this.gl.getUniformLocation(this._program as WebGLProgram, name);
}
attach(which: number, source: string) {
const gl = this.gl;
const shader = gl.createShader(which);
if (!shader) {
throw new Error(`failed to init ${humanShaderType(which)} shader`);
}
gl.shaderSource(shader, source);
gl.compileShader(shader);
gl.attachShader(this._program as WebGLProgram, shader);
}
compile() {
const gl = this.gl;
this.attach(gl.FRAGMENT_SHADER, this.fragmentCode);
this.attach(gl.VERTEX_SHADER, this.vertexCode);
this.link();
}
link() {
this.gl.linkProgram(this._program as WebGLProgram);
if (
!this.gl.getProgramParameter(
this._program as WebGLProgram,
this.gl.LINK_STATUS
)
) {
throw new Error(
"Unable to initialize the shader program: " +
this.gl.getProgramInfoLog(this._program as WebGLProgram)
);
}
}
module(app: WebGPUApp) {
return (this._module =
this._module ||
(this._module = app.device.createShaderModule({
code: this.code,
})));
bindAttrib(attribLocation: number, name: string) {
this.gl.bindAttribLocation(
this._program as WebGLProgram,
attribLocation,
name
);
}
setupUniforms(
time: number,
projection: mat4,
model: Transform,
view: Transform
) {
const { gl } = this._app as WebGLApp;
gl.useProgram(this._program as WebGLProgram);
gl.uniformMatrix4fv(this.uniform("uProjectionMatrix"), false, projection);
if (this.config.time) {
gl.uniform1f(this.uniform("uTime"), time);
}
const modelMat = mat4.clone(model.toMat4());
mat4.fromQuat(modelMat, view.rotation);
mat4.translate(modelMat, modelMat, view.position);
gl.uniformMatrix4fv(this.uniform("uModelViewMatrix"), false, modelMat);
}
use() {
this._app?.gl.useProgram(this._program);
}
}
export const Oops = new Shader(oopsWsgl);
const humanShaderType = (which: number): string => {
switch (which) {
case Shader.FRAGMENT:
return "fragment";
case Shader.VERTEX:
return "vertex";
default:
return "some unknown type of";
}
};

View file

@ -1,5 +1,5 @@
import { Behavior } from "./behavior";
import { WebGPUApp } from "./webgpu";
import { WebGLApp } from "./webgl";
export class Telemetry extends Behavior {
public el: HTMLElement;
@ -7,7 +7,7 @@ export class Telemetry extends Behavior {
public maxFrameTimes: number = 100;
public lastFrameTime: number = 0;
constructor(
public app: WebGPUApp,
public app: WebGLApp,
selector = "#telemetry"
) {
super(app);
@ -35,14 +35,13 @@ export class Telemetry extends Behavior {
const framesPerSecond = 1000 / averageFrameTime;
this.el.innerHTML = `
${framesPerSecond.toFixed(1)} FPS (${averageFrameTime.toFixed(
3
)} ms)<br />
bU: ${this.app.registry.onBeforeUpdate.length} | U: ${
this.app.registry.onUpdate.length
} | aU: ${this.app.registry.onAfterUpdate.length}
`;
this.el.innerHTML = `${framesPerSecond.toFixed(
1
)} FPS (${averageFrameTime.toFixed(3)} ms)<br />bU: ${
this.app.registry.onBeforeUpdate.length
} | U: ${this.app.registry.onUpdate.length} | aU: ${
this.app.registry.onAfterUpdate.length
}`;
}, 1000);
}

18
src/renderer/transform.ts Normal file
View file

@ -0,0 +1,18 @@
import { mat4, vec3, quat } from "gl-matrix";
export class Transform {
constructor(
public position = vec3.create(),
public rotation = quat.create(),
public scale = vec3.fromValues(1, 1, 1)
) {}
toMat4() {
return mat4.fromRotationTranslationScale(
mat4.create(),
this.rotation,
this.position,
this.scale
);
}
}

120
src/renderer/webgl.ts Normal file
View file

@ -0,0 +1,120 @@
import { Telemetry } from "./telemetry";
export type WebGPUAppConfig = {
fov?: number;
context?: GPUCanvasConfiguration;
zNear?: number;
zFar?: number;
};
export type RenderHandle = (time: number, app: WebGLApp) => void;
export class WebGLApp {
public canvas: HTMLCanvasElement;
public telemetry?: Telemetry;
public gl: WebGL2RenderingContext;
public registry: {
onBeforeUpdate: RenderHandle[];
onAfterUpdate: RenderHandle[];
onUpdate: RenderHandle[];
onStart: RenderHandle[];
} = {
onBeforeUpdate: [],
onAfterUpdate: [],
onUpdate: [],
onStart: [],
};
constructor(public config: WebGPUAppConfig = {}) {
try {
this.canvas = document.querySelector("canvas") as HTMLCanvasElement;
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
const context = this.canvas.getContext("webgl2");
if (!context) {
throw new Error("Canvas was unable to get a webgl2 context");
}
this.gl = context;
if (location.search.includes("telemetry")) {
this.telemetry = new Telemetry(this);
}
} catch (e) {
const main = document.querySelector("main");
if (main) {
main.innerHTML = `<div><i>your browser didn't let me set up webgl.</i></div>`;
}
throw new Error(
`Unable to initialize WebGL. Your browser or machine may not support it.\n -> ${e}`
);
}
}
clear() {
const gl = this.gl;
gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear to black, fully opaque
gl.clearDepth(1.0); // Clear everything
gl.enable(gl.DEPTH_TEST); // Enable depth testing
gl.depthFunc(gl.LEQUAL); // Near things obscure far things
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
onBeforeUpdate(handle: RenderHandle) {
this.registry.onBeforeUpdate.push(handle);
}
onAfterUpdate(handle: RenderHandle) {
this.registry.onAfterUpdate.push(handle);
}
onUpdate(handle: RenderHandle) {
this.registry.onUpdate.push(handle);
}
onStart(handle: RenderHandle) {
this.registry.onStart.push(handle);
}
doUpdate(time: number) {
// this.jobsToSubmitThisFrame = [];
this.registry.onBeforeUpdate.forEach((handle) => handle(time, this));
this.registry.onUpdate.forEach((handle) => handle(time, this));
this.registry.onAfterUpdate.forEach((handle) => handle(time, this));
// if (this.jobsToSubmitThisFrame.length !== 0) {
// this.device.queue.submit(this.jobsToSubmitThisFrame);
// }
}
doStart(time: number = 0) {
this.clear();
this.registry.onStart.forEach((handle) => handle(time, this));
}
async oneShot(time: number = 0) {
// await this.awaitRendererReady();
this.doStart(time);
this.doUpdate(time);
}
async start() {
// await this.awaitRendererReady();
this.doStart();
const run = (time: number) => {
this.doUpdate(time);
requestAnimationFrame(run);
};
requestAnimationFrame(run);
}
// commit(commandEncoder: GPUCommandBuffer) {
// this.jobsToSubmitThisFrame.push(commandEncoder);
// }
}

View file

@ -1,177 +0,0 @@
import { Telemetry } from "./telemetry";
export type WebGPUAppConfig = {
fov?: number;
context?: GPUCanvasConfiguration;
};
export type RenderHandle = (time: number, app: WebGPUApp) => void;
export class WebGPUApp {
public canvas: HTMLCanvasElement;
private _adapter?: GPUAdapter;
private _device?: GPUDevice;
private _context?: GPUCanvasContext;
public telemetry?: Telemetry;
private jobsToSubmitThisFrame: GPUCommandBuffer[] = [];
private renderOK = false;
public registry: {
onBeforeUpdate: RenderHandle[];
onAfterUpdate: RenderHandle[];
onUpdate: RenderHandle[];
onStart: RenderHandle[];
} = {
onBeforeUpdate: [],
onAfterUpdate: [],
onUpdate: [],
onStart: [],
};
constructor(public config: WebGPUAppConfig = {}) {
this.config = {
fov: 45,
...config,
};
this.canvas = document.querySelector("canvas") as HTMLCanvasElement;
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
if (location.search.includes("telemetry")) {
this.telemetry = new Telemetry(this);
}
this.init().catch((e) => {
const main = document.querySelector("main");
if (main) {
main.innerHTML = `<div><i>your browser didn't let me set up webgpu. firefox nightly or enable <code>dom.webgpu.enable</code>.</i></div>`;
}
throw new Error(
"Unable to initialize WebGPU. Your browser or machine may not support it.",
e
);
});
}
async init() {
if (!navigator.gpu) {
throw new Error("WebGPU not supported");
}
this._adapter = (await navigator.gpu.requestAdapter()) as GPUAdapter;
if (!this._adapter) {
throw new Error("No GPU adapter found");
}
this._device = await this.adapter.requestDevice();
if (!this._device) {
throw new Error("No GPU device found");
}
this._context = this.canvas.getContext("webgpu") as GPUCanvasContext;
this.context.configure({
device: this.device,
format: "bgra8unorm",
alphaMode: "premultiplied",
...this.config.context,
});
this.renderOK = true;
}
awaitRendererReady(timeout: number = 5000) {
const start = Date.now();
return new Promise((resolve, reject) => {
const interval = setInterval(() => {
if (this.renderOK) {
return resolve(true);
}
if (Date.now() - start > timeout) {
return reject(`Renderer was not OK within ${timeout}ms`);
}
}, 10);
});
}
get context() {
if (!this._context) {
throw new Error("WebGPU context not initialized");
}
return this._context;
}
get adapter() {
if (!this._adapter) {
throw new Error("WebGPU adapter not initialized");
}
return this._adapter;
}
get device() {
if (!this._device) {
throw new Error("WebGPU device not initialized");
}
return this._device;
}
onBeforeUpdate(handle: RenderHandle) {
this.registry.onBeforeUpdate.push(handle);
}
onAfterUpdate(handle: RenderHandle) {
this.registry.onAfterUpdate.push(handle);
}
onUpdate(handle: RenderHandle) {
this.registry.onUpdate.push(handle);
}
onStart(handle: RenderHandle) {
this.registry.onStart.push(handle);
}
doUpdate(time: number) {
this.jobsToSubmitThisFrame = [];
this.registry.onBeforeUpdate.forEach((handle) => handle(time, this));
this.registry.onUpdate.forEach((handle) => handle(time, this));
this.registry.onAfterUpdate.forEach((handle) => handle(time, this));
if (this.jobsToSubmitThisFrame.length !== 0) {
this.device.queue.submit(this.jobsToSubmitThisFrame);
}
}
doStart(time: number = 0) {
this.registry.onStart.forEach((handle) => handle(time, this));
}
async oneShot(time: number = 0) {
await this.awaitRendererReady();
this.doStart(time);
this.doUpdate(time);
}
async start() {
await this.awaitRendererReady();
this.doStart();
const run = (time: number) => {
this.doUpdate(time);
requestAnimationFrame(run);
};
requestAnimationFrame(run);
}
commit(commandEncoder: GPUCommandBuffer) {
this.jobsToSubmitThisFrame.push(commandEncoder);
}
}

View file

@ -1,6 +0,0 @@
struct Uniforms {
modelViewProjectionMatrix: mat4x4<f32>,
time: f32,
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;