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

@ -9,7 +9,7 @@ https://art.mekanoe.com
## Artworks ## Artworks
- [./001-platform-provenance](https://art.mekanoe.com/001-platform-provenance) - [./001-platform-provenance](https://art.mekanoe.com/001-platform-provenance)
- [./002-webgpu-instead](https://art.mekanoe.com/002-webgpu-instead) - [./002-webgl-engine](https://art.mekanoe.com/002-webgl-engine)
## Development ## Development

BIN
bun.lockb

Binary file not shown.

View file

@ -13,19 +13,28 @@ const works = globSync("src/*/main.ts");
console.log(chalk.green`>> Building ...`); console.log(chalk.green`>> Building ...`);
console.log(chalk.yellow(` Found ${works.length} works.`)); console.log(chalk.yellow(` Found ${works.length} works.`));
console.log(chalk.yellow(` Running Bun.build()`));
await Bun.build({ const results = await Bun.build({
entrypoints: works, entrypoints: works,
outdir: "html", outdir: "html",
splitting: true, splitting: true,
loader: { loader: {
".glsl": "text", ".glsl": "text",
".wgsl": "text", ".wgsl": "text",
".vert": "text",
".frag": "text",
}, },
minify: true, minify: process.env.MINIFY === "false" ? false : true,
plugins: [glslPlugin], plugins: [glslPlugin],
}); });
if (!results.success) {
console.error(chalk.red("XX Bun.build() Failed."));
console.error(chalk.red(JSON.stringify(results.logs, null, 2)));
process.exit(1);
}
console.log(chalk.green`>> Generating HTML and Markdown ...`); console.log(chalk.green`>> Generating HTML and Markdown ...`);
await generate(works); await generate(works);

View file

@ -9,6 +9,16 @@ export const convertMeshes = async () => {
const [header, body] = ply.split("end_header"); const [header, body] = ply.split("end_header");
const colorSize = header.includes("red") ? 4 : 0; const colorSize = header.includes("red") ? 4 : 0;
const headerLines = header.split("\n");
const vertexCount = Number(
headerLines
.find((header) => header.startsWith("element vertex"))
?.replace("element vertex ", "")
);
if (!vertexCount) {
throw new Error("couldn't get vertex count...");
}
const values: number[] = []; const values: number[] = [];
@ -43,13 +53,16 @@ export const convertMeshes = async () => {
import { Mesh } from "../renderer/mesh"; import { Mesh } from "../renderer/mesh";
// prettier-ignore // prettier-ignore
const mesh = new Float32Array(${JSON.stringify(values, null, 2)}); const mesh = new Float32Array(${JSON.stringify(values)});
export default new Mesh({ export default new Mesh({
mesh, mesh,
positionSize: 4 * 4, positionSize: 4,
colorSize: ${colorSize} * 4, colorSize: ${colorSize},
uvSize: 2 * 4, uvSize: 2,
vertexCount: ${vertexCount},
stride: ${4 + colorSize + 2},
name: ${JSON.stringify(file)}
}); });
`; `;

View file

@ -1,5 +1,5 @@
import glsl from "esbuild-plugin-glsl"; import glsl from "esbuild-plugin-glsl";
export default glsl({ export default glsl({
minify: true, minify: process.env.MINIFY === "false" ? false : true,
}); });

View file

@ -22,10 +22,4 @@
<canvas id="canvas" width="1280" height="720"></canvas> <canvas id="canvas" width="1280" height="720"></canvas>
<div id="telemetry">XX.X FPS (XX.X ms)</div> <div id="telemetry">XX.X FPS (XX.X ms)</div>
</main> </main>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/3.4.2/gl-matrix-min.min.js"
integrity="sha512-cR3oS5mKRWD+38vYi1CNJk1DLpi104ovuQBuVv9p7nNxeqzSNiHzlboK2BZQybmpTi1QNnQ5unYajpURcMjeZQ=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<script src="/##name##/main.js" type="module"></script> <script src="/##name##/main.js" type="module"></script>

View file

@ -22,10 +22,4 @@
<canvas id="canvas" width="1280" height="720"></canvas> <canvas id="canvas" width="1280" height="720"></canvas>
<div id="telemetry">XX.X FPS (XX.X ms)</div> <div id="telemetry">XX.X FPS (XX.X ms)</div>
</main> </main>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/3.4.2/gl-matrix-min.min.js"
integrity="sha512-cR3oS5mKRWD+38vYi1CNJk1DLpi104ovuQBuVv9p7nNxeqzSNiHzlboK2BZQybmpTi1QNnQ5unYajpURcMjeZQ=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<script src="/001-platform-provenance/main.js" type="module"></script> <script src="/001-platform-provenance/main.js" type="module"></script>

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>com.mekanoe.art // 002-webgpu-instead</title> <title>com.mekanoe.art // 002-webgl-engine</title>
<style> <style>
html, html,
body { body {
@ -22,10 +22,4 @@
<canvas id="canvas" width="1280" height="720"></canvas> <canvas id="canvas" width="1280" height="720"></canvas>
<div id="telemetry">XX.X FPS (XX.X ms)</div> <div id="telemetry">XX.X FPS (XX.X ms)</div>
</main> </main>
<script <script src="/002-webgl-engine/main.js" type="module"></script>
src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/3.4.2/gl-matrix-min.min.js"
integrity="sha512-cR3oS5mKRWD+38vYi1CNJk1DLpi104ovuQBuVv9p7nNxeqzSNiHzlboK2BZQybmpTi1QNnQ5unYajpURcMjeZQ=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<script src="/002-webgpu-instead/main.js" type="module"></script>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,5 +0,0 @@
class L{d;constructor(d){this.app=d;this.onStart&&d.onStart(this.onStart.bind(this)),this.onUpdate&&d.onUpdate(this.onUpdate.bind(this)),this.onAfterUpdate&&d.onAfterUpdate(this.onAfterUpdate.bind(this)),this.onBeforeUpdate&&d.onBeforeUpdate(this.onBeforeUpdate.bind(this))}}class h extends L{d;el;frameTimes=[];maxFrameTimes=100;lastFrameTime=0;constructor(d,n="#telemetry"){super(d);this.app=d;if(this.el=document.querySelector(n),this.el&&location.search.includes("telemetry"))this.el.style.display="block"}insertTime(d){if(this.frameTimes.push(d),this.frameTimes.length>this.maxFrameTimes)this.frameTimes.shift()}onStart(){this.lastFrameTime=0,this.frameTimes=[],setInterval(()=>{const d=this.frameTimes.reduce((U,c)=>U+c,0)/this.frameTimes.length,n=1000/d;this.el.innerHTML=`
${n.toFixed(1)} FPS (${d.toFixed(3)} ms)<br />
bU: ${this.app.registry.onBeforeUpdate.length} | U: ${this.app.registry.onUpdate.length} | aU: ${this.app.registry.onAfterUpdate.length}
`},1000)}onAfterUpdate(d){const n=d-this.lastFrameTime;this.insertTime(n),this.lastFrameTime=d}}
export{L as a,h as b};

View file

@ -31,7 +31,7 @@
</header> </header>
<section id="works"> <section id="works">
<ul> <ul>
<li><a href="/002-webgpu-instead">./002-webgpu-instead</a></li> <li><a href="/002-webgl-engine">./002-webgl-engine</a></li>
<li><a href="/001-platform-provenance">./001-platform-provenance</a></li> <li><a href="/001-platform-provenance">./001-platform-provenance</a></li>
</ul> </ul>
</section> </section>

10
index.d.ts vendored
View file

@ -10,3 +10,13 @@ declare module "*.wgsl" {
const content: string; const content: string;
export default content; export default content;
} }
declare module "*.vert" {
const content: string;
export default content;
}
declare module "*.frag" {
const content: string;
export default content;
}

View file

@ -9,7 +9,6 @@
"dev": "run-p serve build:watch" "dev": "run-p serve build:watch"
}, },
"devDependencies": { "devDependencies": {
"@webgpu/types": "^0.1.37",
"bun-types": "latest", "bun-types": "latest",
"npm-run-all2": "^6.1.1", "npm-run-all2": "^6.1.1",
"prettier": "^3.0.3" "prettier": "^3.0.3"
@ -20,9 +19,9 @@
"dependencies": { "dependencies": {
"chalk": "^5.3.0", "chalk": "^5.3.0",
"esbuild-plugin-glsl": "^1.2.2", "esbuild-plugin-glsl": "^1.2.2",
"gl-matrix": "^3.4.3",
"glob": "^10.3.10", "glob": "^10.3.10",
"serve": "^14.2.1", "serve": "^14.2.1",
"typescript": "^5.2.2", "typescript": "^5.2.2"
"wgpu-matrix": "^2.5.0"
} }
} }

View file

@ -1,4 +1,5 @@
import { Telemetry } from "../renderer/telemetry"; import { Telemetry } from "../renderer/telemetry";
import { mat4 } from "gl-matrix";
export class App { export class App {
constructor( constructor(
@ -35,21 +36,11 @@ export class App {
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 0.1; const zNear = 0.1;
const zFar = 100.0; const zFar = 100.0;
const projectionMatrix = glMatrix.mat4.create(); const projectionMatrix = mat4.create();
glMatrix.mat4.perspective( mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar);
projectionMatrix,
fieldOfView,
aspect,
zNear,
zFar
);
const modelViewMatrix = glMatrix.mat4.create(); const modelViewMatrix = mat4.create();
glMatrix.mat4.translate( mat4.translate(modelViewMatrix, modelViewMatrix, [-0.0, 0.0, -6.0]);
modelViewMatrix,
modelViewMatrix,
[-0.0, 0.0, -6.0]
);
this.projectionMatrix = projectionMatrix; this.projectionMatrix = projectionMatrix;
this.modelViewMatrix = modelViewMatrix; 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({ export default new Mesh({
mesh, mesh,
positionSize: 4 * 4, positionSize: 4,
colorSize: 4 * 4, colorSize: 4,
uvSize: 2 * 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 { export abstract class Behavior {
onStart?(...args: any[]): void; onStart?(...args: any[]): void;
onBeforeUpdate?(...args: any[]): void; onBeforeUpdate?(...args: any[]): void;
onUpdate?(...args: any[]): void; onUpdate?(...args: any[]): void;
onAfterUpdate?(...args: any[]): void; onAfterUpdate?(...args: any[]): void;
constructor(public app: WebGPUApp) { constructor(public app: WebGLApp) {
this.onStart && app.onStart(this.onStart.bind(this)); this.onStart && app.onStart(this.onStart.bind(this));
this.onUpdate && app.onUpdate(this.onUpdate.bind(this)); this.onUpdate && app.onUpdate(this.onUpdate.bind(this));
this.onAfterUpdate && app.onAfterUpdate(this.onAfterUpdate.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 { Behavior } from "./behavior";
import { Mesh } from "./mesh"; import { Mesh } from "./mesh";
import { Shader } from "./shader"; 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 { export class MeshRenderer extends Behavior {
private depthTexture?: GPUTexture; private modelMatrix = mat4.create();
private uniformBuffer?: GPUBuffer; private projectionMatrix = mat4.create();
private vertexBuffer?: GPUBuffer; private _meshBuffer?: WebGLBuffer;
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
);
constructor( constructor(
public app: WebGPUApp, public app: WebGLApp,
public mesh: Mesh, public mesh: Mesh,
public shader: Shader, public shader: Shader,
public textures?: any[] public camera: Transform = new Transform([0, 0, -6]),
public config: MeshRendererConfig = {}
) { ) {
super(app); 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() { onStart() {
this.projectionMatrix = mat4.perspective( mat4.perspective(
2 * Math.PI * 0.2, this.projectionMatrix,
this.app.config.fov || 45,
this.app.canvas.width / this.app.canvas.height, this.app.canvas.width / this.app.canvas.height,
1, this.app.config.zNear || 0.1,
100 this.app.config.zFar || 100
); );
this.depthTexture = this.app.device.createTexture({ this.shader.compile();
size: [this.app.canvas.width, this.app.canvas.height], this.initializeMeshBuffer();
format: "depth24plus", this.initializeAttributes();
usage: GPUTextureUsage.RENDER_ATTACHMENT, this.shader.link();
});
// 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);
} }
private writeUniforms(modelViewProjection: Mat4, time: number) { onRenderableUpdate(time: number, transform: Transform) {
if (!this.uniformBuffer) { const gl = this.app.gl;
return; this.shader.use();
} gl.bindBuffer(gl.ARRAY_BUFFER, this.meshBuffer);
this.shader.setupUniforms(
const { time,
device: { queue }, this.projectionMatrix,
} = this.app; transform,
this.camera
const mvpBuf = modelViewProjection as Float32Array; );
queue.writeBuffer( gl.drawArrays(
this.uniformBuffer, this.config.drawMode ?? gl.TRIANGLE_STRIP,
0, 0,
mvpBuf.buffer, this.mesh.config.vertexCount
mvpBuf.byteOffset,
mvpBuf.length
); );
const timeBuf = new Float32Array([time]); const err = gl.getError();
queue.writeBuffer( if (err !== 0) {
this.uniformBuffer, console.log({ err });
mvpBuf.length + 1, throw new Error(
timeBuf.buffer, `(MeshRenderer<Mesh#${this.mesh.name}>) webgl failure: ${err}`
timeBuf.byteOffset, );
timeBuf.byteLength
);
}
onUpdate(time: number) {
if (
!this.renderPassDescriptor ||
!this.pipeline ||
!this.uniformBindGroup ||
!this.vertexBuffer
) {
return;
} }
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 = { export type MeshConfig = {
mesh: Float32Array; mesh: Float32Array;
positionSize: number; positionSize: number;
colorSize: number; colorSize: number;
uvSize: number; uvSize: number;
vertexCount: number; vertexCount: number;
stride: number;
name: string;
}; };
export class Mesh { export class Mesh {
constructor(public config: MeshConfig) {} constructor(public config: MeshConfig) {}
buffer(app: WebGPUApp) { get name() {
const buffer = app.device.createBuffer({ return this.config.name;
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",
},
});
} }
} }

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 { mat4, vec3 } from "gl-matrix";
import oopsWsgl from "./oops.wgsl"; 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 { export class Shader {
private _module: GPUShaderModule | null = null; static VERTEX = 35633;
public vertexMain: string = "main"; static FRAGMENT = 35632;
public fragmentMain: string = "main";
public code: string;
constructor(...code: string[]) { constructor(private config: ShaderConfig = {}) {}
this.code = code.join("\n");
// pragma preprocessing private vertexCode = "";
const matches = this.code.matchAll(pragmaRegexp); private fragmentCode = "";
for (const match of matches || []) { private _app?: WebGLApp;
switch (match[1]) { private _program: WebGLProgram | null = null;
case "fragment":
this.fragmentMain = match[2]; get gl() {
break; const gl = this._app?.gl;
case "vertex": if (!gl) {
this.vertexMain = match[2]; throw new Error("GL context not defined at shader compile time.");
break; }
}
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) { bindAttrib(attribLocation: number, name: string) {
return (this._module = this.gl.bindAttribLocation(
this._module || this._program as WebGLProgram,
(this._module = app.device.createShaderModule({ attribLocation,
code: this.code, 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 { Behavior } from "./behavior";
import { WebGPUApp } from "./webgpu"; import { WebGLApp } from "./webgl";
export class Telemetry extends Behavior { export class Telemetry extends Behavior {
public el: HTMLElement; public el: HTMLElement;
@ -7,7 +7,7 @@ export class Telemetry extends Behavior {
public maxFrameTimes: number = 100; public maxFrameTimes: number = 100;
public lastFrameTime: number = 0; public lastFrameTime: number = 0;
constructor( constructor(
public app: WebGPUApp, public app: WebGLApp,
selector = "#telemetry" selector = "#telemetry"
) { ) {
super(app); super(app);
@ -35,14 +35,13 @@ export class Telemetry extends Behavior {
const framesPerSecond = 1000 / averageFrameTime; const framesPerSecond = 1000 / averageFrameTime;
this.el.innerHTML = ` this.el.innerHTML = `${framesPerSecond.toFixed(
${framesPerSecond.toFixed(1)} FPS (${averageFrameTime.toFixed( 1
3 )} FPS (${averageFrameTime.toFixed(3)} ms)<br />bU: ${
)} ms)<br /> this.app.registry.onBeforeUpdate.length
bU: ${this.app.registry.onBeforeUpdate.length} | U: ${ } | U: ${this.app.registry.onUpdate.length} | aU: ${
this.app.registry.onUpdate.length this.app.registry.onAfterUpdate.length
} | aU: ${this.app.registry.onAfterUpdate.length} }`;
`;
}, 1000); }, 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;