diff --git a/README.md b/README.md
index acc6b1c..7e4013a 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,9 @@
# oktaeder
3D rendering library for WebGPU
+
+This project uses [pnpm](https://pnpm.io/). If you don't have it, you can get it
+using npm by running `npm install -g pnpm`. Then, run `pnpm install` to install
+all dependencies.
+
+To run the example, run `pnpm start:example` and visit
+[localhost:8000](http://localhost:8000).
diff --git a/example/index.html b/example/index.html
new file mode 100644
index 0000000..b0b7d08
--- /dev/null
+++ b/example/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+ oktaeder example
+
+
+
+
+
+
diff --git a/example/script.ts b/example/script.ts
new file mode 100644
index 0000000..9f39509
--- /dev/null
+++ b/example/script.ts
@@ -0,0 +1,102 @@
+import { Color, Material, Mesh, Node, PerspectiveCamera, PointLight, Quaternion, Scene, Submesh, Vector3 } from "../src/data/index";
+import { Renderer } from "../src/oktaeder";
+import "./style.css";
+
+new EventSource("/esbuild").addEventListener("change", () => location.reload());
+
+const canvas = document.createElement("canvas");
+window.addEventListener("resize", onResize);
+onResize.call(window);
+
+const renderer = await Renderer.init(canvas);
+
+const camera = new PerspectiveCamera({
+ verticalFovRad: 50 * (Math.PI / 180),
+ nearPlane: 0.001,
+ farPlane: Infinity,
+});
+
+const vertexBuffer = renderer.createVertexBuffer({ vertexCount: 6 });
+vertexBuffer.writeTypedArray(0, {
+ position: new Float32Array([
+ -1, 0, 0,
+ 1, 0, 0,
+ 0, -1, 0,
+ 0, 1, 0,
+ 0, 0, -1,
+ 0, 0, 1,
+ ]),
+});
+
+const indexBuffer = renderer.createIndexBuffer({ indexCount: 24, indexFormat: "uint16" });
+indexBuffer.writeArray(0, [
+ 0, 4, 3,
+ 4, 1, 3,
+ 1, 5, 3,
+ 5, 0, 3,
+ 4, 0, 2,
+ 1, 4, 2,
+ 5, 1, 2,
+ 0, 5, 2,
+]);
+
+const submesh: Submesh = { start: 0, length: 24 };
+
+const mesh = new Mesh({ vertexBuffer, indexBuffer, submeshes: [submesh] });
+
+const material = new Material({
+ baseColor: Color.white(),
+ roughness: 0.5,
+ metallic: 0,
+});
+
+const node = new Node({ mesh, materials: [material] });
+
+const cameraPitchRad = 15 * (Math.PI / 180);
+const scene = new Scene({
+ nodes: [
+ node,
+ new Node({
+ translation: new Vector3(-1, 1, 0),
+ light: new PointLight({ color: new Color(1, 0, 0) }),
+ }),
+ new Node({
+ translation: new Vector3(0, 1, -1),
+ light: new PointLight({ color: new Color(0, 1, 0) }),
+ }),
+ new Node({
+ translation: new Vector3(1, 1, 0),
+ light: new PointLight({ color: new Color(0, 0, 1) }),
+ }),
+ new Node({
+ translation: new Vector3(0, 1, 1),
+ light: new PointLight({ color: new Color(1, 1, 0) }),
+ }),
+ new Node({
+ translation: new Vector3(0, 0.8, -3),
+ rotation: new Quaternion(Math.sin(0.5 * cameraPitchRad), 0, 0, Math.cos(0.5 * cameraPitchRad)),
+ camera,
+ }),
+ ],
+ ambientLight: new Color(0.01, 0.01, 0.01),
+});
+
+function onResize(this: Window) {
+ canvas.width = this.innerWidth;
+ canvas.height = this.innerHeight;
+}
+
+const rotation = Quaternion.identity();
+
+function draw(time: number) {
+ rotation.y = Math.cos(0.001 * time);
+ rotation.w = Math.sin(0.001 * time);
+ node.setRotation(rotation);
+
+ renderer.render(scene, camera);
+ requestAnimationFrame(draw);
+}
+
+requestAnimationFrame(draw);
+
+document.body.appendChild(canvas);
diff --git a/example/style.css b/example/style.css
new file mode 100644
index 0000000..57df82b
--- /dev/null
+++ b/example/style.css
@@ -0,0 +1,10 @@
+html, body {
+ margin: 0;
+ padding: 0;
+}
+
+html, body, canvas {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+}
diff --git a/package.json b/package.json
index e8837d8..92a24ce 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"url": "https://github.com/iszn11/oktaeder.git"
},
"scripts": {
+ "start:example": "esbuild example/script.ts --bundle --outfile=example/bundle.js --watch --servedir=example --format=esm --sourcemap",
"build": "tsc --build"
},
"dependencies": {
@@ -25,6 +26,7 @@
},
"devDependencies": {
"@webgpu/types": "^0.1.34",
+ "esbuild": "^0.19.2",
"typescript": "5.1.6"
},
"exports": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c0537c7..01e2b11 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -13,16 +13,247 @@ devDependencies:
'@webgpu/types':
specifier: ^0.1.34
version: 0.1.34
+ esbuild:
+ specifier: ^0.19.2
+ version: 0.19.2
typescript:
specifier: 5.1.6
version: 5.1.6
packages:
+ /@esbuild/android-arm64@0.19.2:
+ resolution: {integrity: sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/android-arm@0.19.2:
+ resolution: {integrity: sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/android-x64@0.19.2:
+ resolution: {integrity: sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/darwin-arm64@0.19.2:
+ resolution: {integrity: sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/darwin-x64@0.19.2:
+ resolution: {integrity: sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/freebsd-arm64@0.19.2:
+ resolution: {integrity: sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/freebsd-x64@0.19.2:
+ resolution: {integrity: sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-arm64@0.19.2:
+ resolution: {integrity: sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-arm@0.19.2:
+ resolution: {integrity: sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-ia32@0.19.2:
+ resolution: {integrity: sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-loong64@0.19.2:
+ resolution: {integrity: sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw==}
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-mips64el@0.19.2:
+ resolution: {integrity: sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-ppc64@0.19.2:
+ resolution: {integrity: sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-riscv64@0.19.2:
+ resolution: {integrity: sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-s390x@0.19.2:
+ resolution: {integrity: sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-x64@0.19.2:
+ resolution: {integrity: sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/netbsd-x64@0.19.2:
+ resolution: {integrity: sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/openbsd-x64@0.19.2:
+ resolution: {integrity: sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/sunos-x64@0.19.2:
+ resolution: {integrity: sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/win32-arm64@0.19.2:
+ resolution: {integrity: sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/win32-ia32@0.19.2:
+ resolution: {integrity: sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/win32-x64@0.19.2:
+ resolution: {integrity: sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@webgpu/types@0.1.34:
resolution: {integrity: sha512-9mXtH+CC8q+Ku7Z+1XazNIte81FvfdXwR2lLRO7Ykzjd/hh1J1krJa0gtnkF1kvP11psUmKEPKo7iMTeEcUpNA==}
dev: true
+ /esbuild@0.19.2:
+ resolution: {integrity: sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg==}
+ engines: {node: '>=12'}
+ hasBin: true
+ requiresBuild: true
+ optionalDependencies:
+ '@esbuild/android-arm': 0.19.2
+ '@esbuild/android-arm64': 0.19.2
+ '@esbuild/android-x64': 0.19.2
+ '@esbuild/darwin-arm64': 0.19.2
+ '@esbuild/darwin-x64': 0.19.2
+ '@esbuild/freebsd-arm64': 0.19.2
+ '@esbuild/freebsd-x64': 0.19.2
+ '@esbuild/linux-arm': 0.19.2
+ '@esbuild/linux-arm64': 0.19.2
+ '@esbuild/linux-ia32': 0.19.2
+ '@esbuild/linux-loong64': 0.19.2
+ '@esbuild/linux-mips64el': 0.19.2
+ '@esbuild/linux-ppc64': 0.19.2
+ '@esbuild/linux-riscv64': 0.19.2
+ '@esbuild/linux-s390x': 0.19.2
+ '@esbuild/linux-x64': 0.19.2
+ '@esbuild/netbsd-x64': 0.19.2
+ '@esbuild/openbsd-x64': 0.19.2
+ '@esbuild/sunos-x64': 0.19.2
+ '@esbuild/win32-arm64': 0.19.2
+ '@esbuild/win32-ia32': 0.19.2
+ '@esbuild/win32-x64': 0.19.2
+ dev: true
+
/tslib@2.6.1:
resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==}
dev: false
diff --git a/src/_BinaryWriter.ts b/src/_BinaryWriter.ts
index f93028c..e4c9adb 100644
--- a/src/_BinaryWriter.ts
+++ b/src/_BinaryWriter.ts
@@ -53,18 +53,20 @@ export class _BinaryWriter {
}
ensureUnusedCapacity(desiredUnusedCapacity: number): _BinaryWriter {
- return this.ensureCapacity(this._buffer.byteLength + desiredUnusedCapacity);
+ return this.ensureCapacity(this._length + desiredUnusedCapacity);
}
writeU32(value: number): _BinaryWriter {
this.ensureUnusedCapacity(4);
this._dataView.setUint32(this._length, value, true);
+ this._length += 4;
return this;
}
writeF32(value: number): _BinaryWriter {
this.ensureUnusedCapacity(4);
this._dataView.setFloat32(this._length, value, true);
+ this._length += 4;
return this;
}
@@ -116,6 +118,19 @@ export class _BinaryWriter {
return this;
}
+ padToAlign(alignment: number): _BinaryWriter {
+ const alignedLength = (this._length + alignment - 1) & ~(alignment - 1);
+ const padding = alignedLength - this._length;
+ if (padding === 0) {
+ return this;
+ }
+
+ this.ensureUnusedCapacity(padding);
+ this._typedArray.fill(0, this._length, alignedLength);
+ this._length = alignedLength;
+ return this;
+ }
+
alloc(byteLength: number): DataView {
this.ensureUnusedCapacity(byteLength);
const dataView = new DataView(this._buffer, this._length, byteLength);
diff --git a/src/data/Camera.ts b/src/data/Camera.ts
index e656542..40f91c7 100644
--- a/src/data/Camera.ts
+++ b/src/data/Camera.ts
@@ -26,7 +26,7 @@ export interface PerspectiveCameraProps {
export class OrthographicCamera {
- readonly type!: "OrthographicCamera";
+ declare readonly type: "OrthographicCamera";
_name: string;
@@ -101,7 +101,7 @@ export class OrthographicCamera {
export class PerspectiveCamera {
- readonly type!: "PerspectiveCamera";
+ declare readonly type: "PerspectiveCamera";
_name: string;
diff --git a/src/data/Color.ts b/src/data/Color.ts
index b91d524..1075b04 100644
--- a/src/data/Color.ts
+++ b/src/data/Color.ts
@@ -56,7 +56,7 @@ export type ColorTuple = readonly [r: number, g: number, b: number];
export class Color {
- readonly type!: "Color";
+ declare readonly type: "Color";
r: number;
g: number;
diff --git a/src/data/Light.ts b/src/data/Light.ts
index 4a564a0..3c63495 100644
--- a/src/data/Light.ts
+++ b/src/data/Light.ts
@@ -22,7 +22,7 @@ export interface PointLightProps {
export class DirectionalLight {
- readonly type!: "DirectionalLight";
+ get type(): "DirectionalLight" {};
_name: string;
@@ -81,7 +81,7 @@ export class DirectionalLight {
export class PointLight {
- readonly type!: "PointLight";
+ declare readonly type: "PointLight";
_name: string;
diff --git a/src/data/Material.ts b/src/data/Material.ts
index f012d2d..3a8ec5f 100644
--- a/src/data/Material.ts
+++ b/src/data/Material.ts
@@ -34,7 +34,7 @@ export interface MaterialProps {
export class Material {
- readonly type!: "Material";
+ declare readonly type: "Material";
_name: string;
diff --git a/src/data/Matrix4x4.ts b/src/data/Matrix4x4.ts
index 4e7790b..fd93e14 100644
--- a/src/data/Matrix4x4.ts
+++ b/src/data/Matrix4x4.ts
@@ -34,7 +34,7 @@ export type Matrix4x4Tuple = readonly [
export class Matrix4x4 {
- readonly type!: "Matrix4x4";
+ declare readonly type: "Matrix4x4";
ix: number;
iy: number;
diff --git a/src/data/Node.ts b/src/data/Node.ts
index 8b9da74..79c8546 100644
--- a/src/data/Node.ts
+++ b/src/data/Node.ts
@@ -23,7 +23,7 @@ export interface NodeProps {
export class Node {
- readonly type!: "Node";
+ declare readonly type: "Node";
_name: string;
diff --git a/src/data/Quaternion.ts b/src/data/Quaternion.ts
index f35992c..6105f00 100644
--- a/src/data/Quaternion.ts
+++ b/src/data/Quaternion.ts
@@ -15,7 +15,7 @@ export type QuaternionTuple = readonly [x: number, y: number, z: number, w: numb
export class Quaternion {
- readonly type!: "Quaternion";
+ declare readonly type: "Quaternion";
x: number;
y: number;
diff --git a/src/data/Scene.ts b/src/data/Scene.ts
index f39ed5d..ee6ec99 100644
--- a/src/data/Scene.ts
+++ b/src/data/Scene.ts
@@ -16,7 +16,7 @@ export interface SceneProps {
export class Scene {
- readonly type!: "Scene";
+ declare readonly type: "Scene";
_name: string;
diff --git a/src/data/Vector2.ts b/src/data/Vector2.ts
index 335274a..9db0cc8 100644
--- a/src/data/Vector2.ts
+++ b/src/data/Vector2.ts
@@ -13,7 +13,7 @@ export type Vector2Tuple = readonly [x: number, y: number];
export class Vector2 {
- readonly type!: "Vector2";
+ declare readonly type: "Vector2";
x: number;
y: number;
diff --git a/src/data/Vector3.ts b/src/data/Vector3.ts
index f5d6aac..1fd3c77 100644
--- a/src/data/Vector3.ts
+++ b/src/data/Vector3.ts
@@ -14,7 +14,7 @@ export type Vector3Tuple = readonly [x: number, y: number, z: number];
export class Vector3 {
- readonly type!: "Vector3";
+ declare readonly type: "Vector3";
x: number;
y: number;
diff --git a/src/data/Vector4.ts b/src/data/Vector4.ts
index a556829..7d4957f 100644
--- a/src/data/Vector4.ts
+++ b/src/data/Vector4.ts
@@ -15,7 +15,7 @@ export type Vector4Tuple = readonly [x: number, y: number, z: number, w: number]
export class Vector4 {
- readonly type!: "Vector4";
+ declare readonly type: "Vector4";
x: number;
y: number;
diff --git a/src/oktaeder.ts b/src/oktaeder.ts
index 63de207..cf4f721 100644
--- a/src/oktaeder.ts
+++ b/src/oktaeder.ts
@@ -11,7 +11,7 @@ import { _BinaryWriter as BinaryWriter } from "./_BinaryWriter";
import { _Mapping as Mapping } from "./_Mapping";
import { Camera, Material, Matrix4x4, Node, Scene, Vector3, isDirectionalLight, isPointLight, preOrder } from "./data";
import { IndexBuffer, IndexBufferProps, Texture2D, Texture2DProps, VertexBuffer, VertexBufferProps } from "./resources";
-import { ShaderFlagKey, ShaderFlags, createPipeline, shaderFlagsKey } from "./shader";
+import { GLOBAL_UNIFORMS_SIZE, MATERIAL_UNIFORMS_SIZE, OBJECT_UNIFORMS_SIZE, ShaderFlagKey, ShaderFlags, _createPipeline, _shaderFlagsKey } from "./shader";
const _matrixOStoWSNormal = new Matrix4x4(
NaN, NaN, NaN, NaN,
@@ -117,6 +117,7 @@ export class Renderer {
width: framebufferTexture.width,
height: framebufferTexture.height,
format: "depth",
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
this._globalBindGroupLayout = device.createBindGroupLayout({
@@ -266,7 +267,7 @@ export class Renderer {
this._globalBindGroup = device.createBindGroup({
layout: this._globalBindGroupLayout,
entries: [
- { binding: 0, resource: { buffer: this._uniformBuffer } },
+ { binding: 0, resource: { buffer: this._uniformBuffer, size: GLOBAL_UNIFORMS_SIZE } },
{ binding: 1, resource: { buffer: this._pointLightBuffer } },
{ binding: 2, resource: { buffer: this._directionalLightBuffer } },
],
@@ -275,7 +276,7 @@ export class Renderer {
this._objectBindGroup = device.createBindGroup({
layout: this._objectBindGroupLayout,
entries: [
- { binding: 0, resource: { buffer: this._uniformBuffer } },
+ { binding: 0, resource: { buffer: this._uniformBuffer, size: OBJECT_UNIFORMS_SIZE } },
],
label: "Object",
});
@@ -336,14 +337,14 @@ export class Renderer {
}
_getOrCreatePipeline(flags: ShaderFlags): GPURenderPipeline {
- const key = shaderFlagsKey(flags);
+ const key = _shaderFlagsKey(flags);
let pipeline = this._pipelineCache.get(key);
if (pipeline !== undefined) {
return pipeline;
}
- pipeline = createPipeline(this, flags);
+ pipeline = _createPipeline(this, flags);
this._pipelineCache.set(key, pipeline);
return pipeline;
}
@@ -401,11 +402,12 @@ export class Renderer {
this._uniformWriter.writeF32(material._normalScale);
this._uniformWriter.writeColorF32(material._emissive);
this._uniformWriter.writeF32(material._ior);
+ this._uniformWriter.padToAlign(256);
const bindGroup = this._device.createBindGroup({
layout: this._materialBindGroupLayout,
entries: [
- { binding: 0, resource: { buffer: this._uniformBuffer } },
+ { binding: 0, resource: { buffer: this._uniformBuffer, size: MATERIAL_UNIFORMS_SIZE } },
{ binding: 1, resource: this._sampler },
{ binding: 2, resource: material._baseColorPartialCoverageTexture?._textureView ?? this._textureWhite._textureView },
{ binding: 3, resource: material._occlusionTexture?._textureView ?? this._textureWhite._textureView },
@@ -433,6 +435,7 @@ export class Renderer {
object._updateWorldMatrix();
this._uniformWriter.writeMatrix4x4(object._worldMatrix);
this._uniformWriter.writeMatrix4x4(_matrixOStoWSNormal.setObject(object._worldMatrix).inverseTransposeAffine());
+ this._uniformWriter.padToAlign(256);
return offset;
});
@@ -491,9 +494,7 @@ export class Renderer {
this._uniformWriter.writeColorF32(scene._ambientLight);
this._uniformWriter.writeU32(pointLightCount);
this._uniformWriter.writeU32(directionalLightCount);
- this._uniformWriter.writeU32(0);
- this._uniformWriter.writeU32(0);
- this._uniformWriter.writeU32(0);
+ this._uniformWriter.padToAlign(256);
// upload uniforms
@@ -520,11 +521,18 @@ export class Renderer {
pass.setPipeline(renderPipeline);
+ /* WORKAROUND
+ *
+ * As of writing, Chrome doesn't support passing null as the second
+ * argument. We could (and should) bind the buffers unconditionally
+ * for increased safety. For now, we only do this when they are not
+ * null.
+ */
pass.setVertexBuffer(0, vertexBuffer._positionBuffer);
- pass.setVertexBuffer(1, vertexBuffer._texCoordBuffer);
- pass.setVertexBuffer(2, vertexBuffer._lightTexCoordBuffer);
- pass.setVertexBuffer(3, vertexBuffer._normalBuffer);
- pass.setVertexBuffer(4, vertexBuffer._tangentBuffer);
+ if (vertexBuffer._texCoordBuffer !== null) pass.setVertexBuffer(1, vertexBuffer._texCoordBuffer);
+ if (vertexBuffer._lightTexCoordBuffer !== null) pass.setVertexBuffer(2, vertexBuffer._lightTexCoordBuffer);
+ if (vertexBuffer._normalBuffer !== null) pass.setVertexBuffer(3, vertexBuffer._normalBuffer);
+ if (vertexBuffer._tangentBuffer !== null) pass.setVertexBuffer(4, vertexBuffer._tangentBuffer);
pass.setIndexBuffer(indexBuffer._buffer, indexBuffer._indexFormat);
pass.setBindGroup(2, this._objectBindGroup, [objectOffset]);
diff --git a/src/resources/IndexBuffer.ts b/src/resources/IndexBuffer.ts
index d4b897a..fd3ed5f 100644
--- a/src/resources/IndexBuffer.ts
+++ b/src/resources/IndexBuffer.ts
@@ -20,7 +20,7 @@ export interface IndexBufferResizeProps {
export class IndexBuffer {
- readonly type!: "IndexBuffer";
+ declare readonly type: "IndexBuffer";
_renderer: Renderer;
_name: string;
diff --git a/src/resources/Texture2D.ts b/src/resources/Texture2D.ts
index cf2acec..25c6afc 100644
--- a/src/resources/Texture2D.ts
+++ b/src/resources/Texture2D.ts
@@ -21,12 +21,15 @@ export interface Texture2DProps {
readonly height: number;
readonly format: Texture2DFormat;
+
+ readonly usage?: GPUTextureUsageFlags;
}
export interface Texture2DResizeProps {
readonly width?: number;
readonly height?: number;
readonly format?: Texture2DFormat;
+ readonly usage?: GPUTextureUsageFlags;
}
export interface Texture2DAdvancedWriteProps {
@@ -39,7 +42,7 @@ export interface Texture2DAdvancedWriteProps {
export class Texture2D {
- readonly type!: "Texture2D";
+ declare readonly type: "Texture2D";
_renderer: Renderer;
_name: string;
@@ -53,6 +56,7 @@ export class Texture2D {
width,
height,
format,
+ usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
}: Texture2DProps) {
this._renderer = renderer;
@@ -62,7 +66,7 @@ export class Texture2D {
this._renderer = renderer;
this._texture = renderer._device.createTexture({
- usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
+ usage,
size: { width, height },
format: gpuFormat,
label: name
@@ -147,13 +151,14 @@ export class Texture2D {
width = this._texture.width,
height = this._texture.height,
format = this._format,
+ usage = this._texture.usage,
}: Texture2DResizeProps): Texture2D {
this._texture.destroy();
const gpuFormat = gpuTextureFormat(format);
this._texture = this._renderer._device.createTexture({
- usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
+ usage,
size: { width, height },
format: gpuFormat,
label: this._name
diff --git a/src/resources/VertexBuffer.ts b/src/resources/VertexBuffer.ts
index 1065911..dfaea2e 100644
--- a/src/resources/VertexBuffer.ts
+++ b/src/resources/VertexBuffer.ts
@@ -51,7 +51,7 @@ export interface VertexBufferWriteTypedArrayProps {
export class VertexBuffer {
- readonly type!: "VertexBuffer";
+ declare readonly type: "VertexBuffer";
_renderer: Renderer;
_name: string;
diff --git a/src/shader.ts b/src/shader.ts
index 66532d8..99cd009 100644
--- a/src/shader.ts
+++ b/src/shader.ts
@@ -6,6 +6,13 @@
import { Renderer } from "./oktaeder";
+// 152 bytes padded to 256
+export const GLOBAL_UNIFORMS_SIZE = 256;
+// 64 bytes padded to 256
+export const MATERIAL_UNIFORMS_SIZE = 256;
+// 128 bytes padded to 256
+export const OBJECT_UNIFORMS_SIZE = 256;
+
export type ShaderFlagKey = number;
export interface ShaderFlags {
@@ -15,7 +22,7 @@ export interface ShaderFlags {
readonly tangent: boolean;
}
-export function shaderFlagsKey({
+export function _shaderFlagsKey({
texCoord,
lightTexCoord,
normal,
@@ -29,13 +36,13 @@ export function shaderFlagsKey({
return key;
}
-export function createPipeline(renderer: Renderer, {
+export function _createPipeline(renderer: Renderer, {
texCoord,
lightTexCoord,
normal,
tangent,
}: ShaderFlags): GPURenderPipeline {
- const shaderCode = createShaderCode({ texCoord, lightTexCoord, normal, tangent });
+ const shaderCode = _createShaderCode({ texCoord, lightTexCoord, normal, tangent });
const shaderModule = renderer._device.createShaderModule({
code: shaderCode,
@@ -130,7 +137,7 @@ export function createPipeline(renderer: Renderer, {
return pipeline;
}
-export function createShaderCode({
+export function _createShaderCode({
texCoord,
lightTexCoord,
normal,
@@ -147,7 +154,7 @@ struct Vertex {
struct Varyings {
@builtin(position) positionCS: vec4,
- @location(0) positionVS: vec4,
+ @location(0) positionVS: vec3,
${texCoord ? `@location(1) texCoord: vec2,` : ""}
${lightTexCoord ? `@location(2) lightTexCoord: vec2,` : ""}
${normal ? `@location(3) normalVS: vec3,` : ""}
@@ -219,7 +226,7 @@ fn visibilityGGX(dotNL: f32, dotNV: f32, alpha: f32) -> f32 {
let vGGX = dotNL * sqrt(dotNV * dotNV * (1.0 - alphaSquared) + alphaSquared);
let lGGX = dotNV * sqrt(dotNL * dotNL * (1.0 - alphaSquared) + alphaSquared);
let GGX = vGGX + lGGX;
- return GGX > 0.0 ? 0.5 / GGX : 0.0;
+ return select(0.0, 0.5 / GGX, GGX > 0.0);
}
fn distributionGGX(dotNH: f32, alpha: f32) -> f32 {
@@ -270,7 +277,7 @@ fn screenSpaceMatrixTStoVS(positionVS: vec3, normalVS: vec3, texCoord:
let bitangentVS = q1perp * uv0.y + q0perp * uv1.y;
let det = max(dot(tangentVS, tangentVS), dot(bitangentVS, bitangentVS));
- let scale = (det == 0.0) ? 0.0 : inserseSqrt(det);
+ let scale = select(0.0, inverseSqrt(det), det != 0.0);
return mat3x3(tangentVS * scale, bitangentVS * scale, normalVS);
}
@@ -297,10 +304,11 @@ fn vert(vertex: Vertex) -> Varyings {
` : ""}
${texCoord ? "output.texCoord = vertex.texCoord;" : ""}
${lightTexCoord ? "output.lightTexCoord = vertex.lightTexCoord;" : ""}
+ return output;
}
@fragment
-fn frag(fragment: Varyings) -> @location(0) vec2 {
+fn frag(fragment: Varyings) -> @location(0) vec4 {
var baseColor = _Material.baseColor;
var partialCoverage = _Material.partialCoverage;
var occlusion = 1.0;
@@ -357,7 +365,7 @@ fn frag(fragment: Varyings) -> @location(0) vec2 {
var outgoingRadiance = vec3(0.0);
- for (var i: u32 = 0; i < _Global.pointLightCount; ++i) {
+ for (var i: u32 = 0; i < _Global.pointLightCount; i++) {
let light = _PointLights[i];
let lightPositionVS = (_Global.matrixWStoVS * vec4(light.positionWS, 1.0)).xyz;
@@ -373,7 +381,7 @@ fn frag(fragment: Varyings) -> @location(0) vec2 {
);
}
- for (var i: u32 = 0; i < _Global.directionalLightCount; ++i) {
+ for (var i: u32 = 0; i < _Global.directionalLightCount; i++) {
let light = _DirectionalLights[i];
let lightDirectionVS = normalize((_Global.matrixWStoVS * vec4(light.directionWS, 0.0)).xyz);