ok webgpu sucks lets jump back to webgl
This commit is contained in:
parent
000f35f19d
commit
87563cb9e3
41 changed files with 587 additions and 6925 deletions
|
@ -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
BIN
bun.lockb
Binary file not shown.
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)}
|
||||
});
|
||||
`;
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import glsl from "esbuild-plugin-glsl";
|
||||
|
||||
export default glsl({
|
||||
minify: true,
|
||||
minify: process.env.MINIFY === "false" ? false : true,
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
@ -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>
|
1
html/002-webgl-engine/main.js
Normal file
1
html/002-webgl-engine/main.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
html/chunk-1da54319833c650d.js
Normal file
2
html/chunk-1da54319833c650d.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -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};
|
|
@ -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
10
index.d.ts
vendored
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
43
src/002-webgl-engine/main.ts
Normal file
43
src/002-webgl-engine/main.ts
Normal 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();
|
|
@ -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();
|
|
@ -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));
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
5
src/common-shaders/error.frag
Normal file
5
src/common-shaders/error.frag
Normal file
|
@ -0,0 +1,5 @@
|
|||
precision highp float;
|
||||
|
||||
void main() {
|
||||
gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);
|
||||
}
|
7
src/common-shaders/error.ts
Normal file
7
src/common-shaders/error.ts
Normal 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);
|
8
src/common-shaders/error.vert
Normal file
8
src/common-shaders/error.vert
Normal file
|
@ -0,0 +1,8 @@
|
|||
attribute vec4 aVertexPosition;
|
||||
|
||||
uniform mat4 uModelViewMatrix;
|
||||
uniform mat4 uProjectionMatrix;
|
||||
|
||||
void main() {
|
||||
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
|
||||
}
|
35
src/common-shaders/uv-rainbow.frag
Normal file
35
src/common-shaders/uv-rainbow.frag
Normal 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);
|
||||
}
|
7
src/common-shaders/uv-rainbow.ts
Normal file
7
src/common-shaders/uv-rainbow.ts
Normal 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);
|
12
src/common-shaders/uv-rainbow.vert
Normal file
12
src/common-shaders/uv-rainbow.vert
Normal 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;
|
||||
}
|
|
@ -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",
|
||||
});
|
||||
|
|
6382
src/meshes/torus.ts
6382
src/meshes/torus.ts
File diff suppressed because one or more lines are too long
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
const err = gl.getError();
|
||||
if (err !== 0) {
|
||||
console.log({ err });
|
||||
throw new Error(
|
||||
`(MeshRenderer<Mesh#${this.mesh.name}>) webgl failure: ${err}`
|
||||
);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
18
src/renderer/renderable.ts
Normal file
18
src/renderer/renderable.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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
18
src/renderer/transform.ts
Normal 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
120
src/renderer/webgl.ts
Normal 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);
|
||||
// }
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
struct Uniforms {
|
||||
modelViewProjectionMatrix: mat4x4<f32>,
|
||||
time: f32,
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
Loading…
Add table
Reference in a new issue