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
- [./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

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.yellow(` Found ${works.length} works.`));
console.log(chalk.yellow(` Running Bun.build()`));
await Bun.build({
const results = await Bun.build({
entrypoints: works,
outdir: "html",
splitting: true,
loader: {
".glsl": "text",
".wgsl": "text",
".vert": "text",
".frag": "text",
},
minify: true,
minify: process.env.MINIFY === "false" ? false : true,
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 ...`);
await generate(works);

View file

@ -9,6 +9,16 @@ export const convertMeshes = async () => {
const [header, body] = ply.split("end_header");
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[] = [];
@ -43,13 +53,16 @@ export const convertMeshes = async () => {
import { Mesh } from "../renderer/mesh";
// prettier-ignore
const mesh = new Float32Array(${JSON.stringify(values, null, 2)});
const mesh = new Float32Array(${JSON.stringify(values)});
export default new Mesh({
mesh,
positionSize: 4 * 4,
colorSize: ${colorSize} * 4,
uvSize: 2 * 4,
positionSize: 4,
colorSize: ${colorSize},
uvSize: 2,
vertexCount: ${vertexCount},
stride: ${4 + colorSize + 2},
name: ${JSON.stringify(file)}
});
`;

View file

@ -1,5 +1,5 @@
import glsl from "esbuild-plugin-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>
<div id="telemetry">XX.X FPS (XX.X ms)</div>
</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>

View file

@ -22,10 +22,4 @@
<canvas id="canvas" width="1280" height="720"></canvas>
<div id="telemetry">XX.X FPS (XX.X ms)</div>
</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>

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
<!DOCTYPE html>
<meta charset="utf-8" />
<title>com.mekanoe.art // 002-webgpu-instead</title>
<title>com.mekanoe.art // 002-webgl-engine</title>
<style>
html,
body {
@ -22,10 +22,4 @@
<canvas id="canvas" width="1280" height="720"></canvas>
<div id="telemetry">XX.X FPS (XX.X ms)</div>
</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="/002-webgpu-instead/main.js" type="module"></script>
<script src="/002-webgl-engine/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>
<section id="works">
<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>
</ul>
</section>

10
index.d.ts vendored
View file

@ -10,3 +10,13 @@ declare module "*.wgsl" {
const content: string;
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"
},
"devDependencies": {
"@webgpu/types": "^0.1.37",
"bun-types": "latest",
"npm-run-all2": "^6.1.1",
"prettier": "^3.0.3"
@ -20,9 +19,9 @@
"dependencies": {
"chalk": "^5.3.0",
"esbuild-plugin-glsl": "^1.2.2",
"gl-matrix": "^3.4.3",
"glob": "^10.3.10",
"serve": "^14.2.1",
"typescript": "^5.2.2",
"wgpu-matrix": "^2.5.0"
"typescript": "^5.2.2"
}
}

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;