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);