Compare commits

...

10 Commits

31 changed files with 1155 additions and 135 deletions

View File

@ -1,2 +1,11 @@
# oktaeder # oktaeder
3D rendering library for WebGPU 3D rendering library for WebGPU
[oktaeder.webm](https://github.com/iszn11/oktaeder/assets/7891270/5dbcb03a-608f-41b8-860e-8e9c8e09e242)
This project ships with [bun.lockb](https://bun.sh/docs/install/lockfile)
lockfile for the [Bun](https://bun.sh/) JavaScript runtime. You should be able
to install the dependencies with any JavaScript package manager, though.
To run the example, run `start:example` script with your JavaScript package
manager and visit [localhost:8000](http://localhost:8000).

BIN
bun.lockb Normal file

Binary file not shown.

11
example/index.html Normal file
View File

@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>oktaeder example</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="bundle.css">
<script type="module" src="bundle.js"></script>
</head>
<body></body>
</html>

104
example/script.ts Normal file
View File

@ -0,0 +1,104 @@
import { Color, DirectionalLight, Mesh, Node, PerspectiveCamera, PointLight, Quaternion, Scene, Submesh, Vector3 } from "../src/data/index";
import { Renderer, degToRad } 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: degToRad(50),
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 = renderer.createMaterial({
baseColor: Color.white(),
roughness: 0.5,
metallic: 1,
});
const node = new Node({ mesh, materials: [material] });
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({
rotation: Quaternion.fromRotationYZ(degToRad(-90)),
light: new DirectionalLight({ color: new Color(0.5, 0.5, 0.5) }),
}),
new Node({
translation: new Vector3(0, 0.8, -3),
rotation: Quaternion.fromRotationYZ(degToRad(15)),
camera,
}),
],
ambientLight: new Color(0.01, 0.01, 0.01),
});
function onResize(this: Window) {
canvas.width = this.innerWidth;
canvas.height = this.innerHeight;
}
const _quaternion = Quaternion.identity();
function draw(timeMs: number) {
const time = 0.001 * timeMs;
node.setRotation(_quaternion.setRotationZX(-0.5 * time));
renderer.render(scene, camera);
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
document.body.appendChild(canvas);

10
example/style.css Normal file
View File

@ -0,0 +1,10 @@
html, body {
margin: 0;
padding: 0;
}
html, body, canvas {
width: 100%;
height: 100%;
overflow: hidden;
}

View File

@ -18,6 +18,7 @@
"url": "https://github.com/iszn11/oktaeder.git" "url": "https://github.com/iszn11/oktaeder.git"
}, },
"scripts": { "scripts": {
"start:example": "esbuild example/script.ts --bundle --outfile=example/bundle.js --watch --servedir=example --format=esm --sourcemap",
"build": "tsc --build" "build": "tsc --build"
}, },
"dependencies": { "dependencies": {
@ -25,6 +26,7 @@
}, },
"devDependencies": { "devDependencies": {
"@webgpu/types": "^0.1.34", "@webgpu/types": "^0.1.34",
"esbuild": "^0.19.2",
"typescript": "5.1.6" "typescript": "5.1.6"
}, },
"exports": { "exports": {

View File

@ -1,34 +0,0 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
tslib:
specifier: ^2.6.1
version: 2.6.1
devDependencies:
'@webgpu/types':
specifier: ^0.1.34
version: 0.1.34
typescript:
specifier: 5.1.6
version: 5.1.6
packages:
/@webgpu/types@0.1.34:
resolution: {integrity: sha512-9mXtH+CC8q+Ku7Z+1XazNIte81FvfdXwR2lLRO7Ykzjd/hh1J1krJa0gtnkF1kvP11psUmKEPKo7iMTeEcUpNA==}
dev: true
/tslib@2.6.1:
resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==}
dev: false
/typescript@5.1.6:
resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==}
engines: {node: '>=14.17'}
hasBin: true
dev: true

View File

@ -53,18 +53,20 @@ export class _BinaryWriter {
} }
ensureUnusedCapacity(desiredUnusedCapacity: number): _BinaryWriter { ensureUnusedCapacity(desiredUnusedCapacity: number): _BinaryWriter {
return this.ensureCapacity(this._buffer.byteLength + desiredUnusedCapacity); return this.ensureCapacity(this._length + desiredUnusedCapacity);
} }
writeU32(value: number): _BinaryWriter { writeU32(value: number): _BinaryWriter {
this.ensureUnusedCapacity(4); this.ensureUnusedCapacity(4);
this._dataView.setUint32(this._length, value, true); this._dataView.setUint32(this._length, value, true);
this._length += 4;
return this; return this;
} }
writeF32(value: number): _BinaryWriter { writeF32(value: number): _BinaryWriter {
this.ensureUnusedCapacity(4); this.ensureUnusedCapacity(4);
this._dataView.setFloat32(this._length, value, true); this._dataView.setFloat32(this._length, value, true);
this._length += 4;
return this; return this;
} }
@ -116,6 +118,19 @@ export class _BinaryWriter {
return this; 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 { alloc(byteLength: number): DataView {
this.ensureUnusedCapacity(byteLength); this.ensureUnusedCapacity(byteLength);
const dataView = new DataView(this._buffer, this._length, byteLength); const dataView = new DataView(this._buffer, this._length, byteLength);

View File

@ -11,7 +11,7 @@ export type Camera = OrthographicCamera | PerspectiveCamera;
export interface OrthographicCameraProps { export interface OrthographicCameraProps {
readonly name?: string; readonly name?: string;
readonly verticalSize: number; readonly halfVerticalSize: number;
readonly nearPlane: number; readonly nearPlane: number;
readonly farPlane: number; readonly farPlane: number;
} }
@ -26,7 +26,7 @@ export interface PerspectiveCameraProps {
export class OrthographicCamera { export class OrthographicCamera {
readonly type!: "OrthographicCamera"; declare readonly type: "OrthographicCamera";
_name: string; _name: string;
@ -39,13 +39,13 @@ export class OrthographicCamera {
constructor({ constructor({
name = "", name = "",
verticalSize, halfVerticalSize,
nearPlane, nearPlane,
farPlane, farPlane,
}: OrthographicCameraProps) { }: OrthographicCameraProps) {
this._name = name; this._name = name;
this._halfVerticalSize = verticalSize; this._halfVerticalSize = halfVerticalSize;
this._nearPlane = nearPlane; this._nearPlane = nearPlane;
this._farPlane = farPlane; this._farPlane = farPlane;
@ -101,7 +101,7 @@ export class OrthographicCamera {
export class PerspectiveCamera { export class PerspectiveCamera {
readonly type!: "PerspectiveCamera"; declare readonly type: "PerspectiveCamera";
_name: string; _name: string;

View File

@ -56,7 +56,7 @@ export type ColorTuple = readonly [r: number, g: number, b: number];
export class Color { export class Color {
readonly type!: "Color"; declare readonly type: "Color";
r: number; r: number;
g: number; g: number;

View File

@ -6,35 +6,11 @@
import { Color, ColorObject } from "."; import { Color, ColorObject } from ".";
import { Texture2D } from "../resources"; import { Texture2D } from "../resources";
import { MaterialProps } from "./MaterialProps";
export interface MaterialProps { export class DynamicMaterial {
name?: string;
baseColor?: ColorObject; declare readonly type: "DynamicMaterial";
partialCoverage?: number;
transmission?: ColorObject;
collimation?: number;
occlusionTextureStrength?: number;
roughness?: number;
metallic?: number;
normalScale?: number;
emissive?: ColorObject;
ior?: number;
baseColorPartialCoverageTexture?: Texture2D | null;
occlusionTexture?: Texture2D | null;
roughnessMetallicTexture?: Texture2D | null;
normalTexture?: Texture2D | null;
emissiveTexture?: Texture2D | null;
transmissionCollimationTexture?: Texture2D | null;
transparent?: boolean;
doubleSided?: boolean;
}
export class Material {
readonly type!: "Material";
_name: string; _name: string;
@ -107,7 +83,7 @@ export class Material {
set name(value: string) { this._name = value; } set name(value: string) { this._name = value; }
get name(): string { return this._name; } get name(): string { return this._name; }
setBaseColor(value: ColorObject): Material { setBaseColor(value: ColorObject): DynamicMaterial {
this._baseColor.setObject(value); this._baseColor.setObject(value);
return this; return this;
} }
@ -130,7 +106,7 @@ export class Material {
set normalScale(value: number) { this._normalScale = value; } set normalScale(value: number) { this._normalScale = value; }
get normalScale(): number { return this._normalScale; } get normalScale(): number { return this._normalScale; }
setEmissive(value: ColorObject): Material { setEmissive(value: ColorObject): DynamicMaterial {
this._emissive.setObject(value); this._emissive.setObject(value);
return this; return this;
} }
@ -138,7 +114,7 @@ export class Material {
return res.setObject(this._emissive); return res.setObject(this._emissive);
} }
setTransmission(value: ColorObject): Material { setTransmission(value: ColorObject): DynamicMaterial {
this._transmission.setObject(value); this._transmission.setObject(value);
return this; return this;
} }
@ -152,22 +128,22 @@ export class Material {
set ior(value: number) { this._ior = value; } set ior(value: number) { this._ior = value; }
get ior(): number { return this._ior; } get ior(): number { return this._ior; }
set baseColorPartialCoverageTexture(value: Texture2D | null) { this._baseColorPartialCoverageTexture = value;} set baseColorPartialCoverageTexture(value: Texture2D | null) { this._baseColorPartialCoverageTexture = value; }
get baseColorPartialCoverageTexture(): Texture2D | null { return this._baseColorPartialCoverageTexture; } get baseColorPartialCoverageTexture(): Texture2D | null { return this._baseColorPartialCoverageTexture; }
set occlusionTexture(value: Texture2D | null) { this._occlusionTexture = value;} set occlusionTexture(value: Texture2D | null) { this._occlusionTexture = value; }
get occlusionTexture(): Texture2D | null { return this._occlusionTexture; } get occlusionTexture(): Texture2D | null { return this._occlusionTexture; }
set roughnessMetallicTexture(value: Texture2D | null) { this._roughnessMetallicTexture = value;} set roughnessMetallicTexture(value: Texture2D | null) { this._roughnessMetallicTexture = value; }
get roughnessMetallicTexture(): Texture2D | null { return this._roughnessMetallicTexture; } get roughnessMetallicTexture(): Texture2D | null { return this._roughnessMetallicTexture; }
set normalTexture(value: Texture2D | null) { this._normalTexture = value;} set normalTexture(value: Texture2D | null) { this._normalTexture = value; }
get normalTexture(): Texture2D | null { return this._normalTexture; } get normalTexture(): Texture2D | null { return this._normalTexture; }
set emissiveTexture(value: Texture2D | null) { this._emissiveTexture = value;} set emissiveTexture(value: Texture2D | null) { this._emissiveTexture = value; }
get emissiveTexture(): Texture2D | null { return this._emissiveTexture; } get emissiveTexture(): Texture2D | null { return this._emissiveTexture; }
set transmissionCollimationTexture(value: Texture2D | null) { this._transmissionCollimationTexture = value;} set transmissionCollimationTexture(value: Texture2D | null) { this._transmissionCollimationTexture = value; }
get transmissionCollimationTexture(): Texture2D | null { return this._transmissionCollimationTexture; } get transmissionCollimationTexture(): Texture2D | null { return this._transmissionCollimationTexture; }
set transparent(value: boolean) { this._transparent = value; } set transparent(value: boolean) { this._transparent = value; }
@ -177,8 +153,8 @@ export class Material {
get doubleSided(): boolean { return this._doubleSided; } get doubleSided(): boolean { return this._doubleSided; }
} }
Object.defineProperty(Material.prototype, "type", { value: "Material" }); Object.defineProperty(DynamicMaterial.prototype, "type", { value: "DynamicMaterial" });
export function isMaterial(value: unknown): value is Material { export function isDynamicMaterial(value: unknown): value is DynamicMaterial {
return Boolean(value) && (value as Material).type === "Material"; return Boolean(value) && (value as DynamicMaterial).type === "DynamicMaterial";
} }

View File

@ -22,7 +22,7 @@ export interface PointLightProps {
export class DirectionalLight { export class DirectionalLight {
readonly type!: "DirectionalLight"; declare readonly type: "DirectionalLight";
_name: string; _name: string;
@ -81,7 +81,7 @@ export class DirectionalLight {
export class PointLight { export class PointLight {
readonly type!: "PointLight"; declare readonly type: "PointLight";
_name: string; _name: string;

33
src/data/MaterialProps.ts Normal file
View File

@ -0,0 +1,33 @@
/*!
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/.
*/
import { ColorObject } from ".";
import { Texture2D } from "../resources";
export interface MaterialProps {
name?: string;
baseColor?: ColorObject;
partialCoverage?: number;
transmission?: ColorObject;
collimation?: number;
occlusionTextureStrength?: number;
roughness?: number;
metallic?: number;
normalScale?: number;
emissive?: ColorObject;
ior?: number;
baseColorPartialCoverageTexture?: Texture2D | null;
occlusionTexture?: Texture2D | null;
roughnessMetallicTexture?: Texture2D | null;
normalTexture?: Texture2D | null;
emissiveTexture?: Texture2D | null;
transmissionCollimationTexture?: Texture2D | null;
transparent?: boolean;
doubleSided?: boolean;
}

View File

@ -34,7 +34,7 @@ export type Matrix4x4Tuple = readonly [
export class Matrix4x4 { export class Matrix4x4 {
readonly type!: "Matrix4x4"; declare readonly type: "Matrix4x4";
ix: number; ix: number;
iy: number; iy: number;

View File

@ -45,6 +45,27 @@ export class Mesh {
get submeshCount(): number { get submeshCount(): number {
return this._submeshes.length; return this._submeshes.length;
} }
set name(value: string) { this._name = value; }
get name(): string { return this._name; }
set vertexBuffer(value: VertexBuffer) { this._vertexBuffer = value; }
get vertexBuffer(): VertexBuffer { return this._vertexBuffer; }
set indexBuffer(value: IndexBuffer) { this._indexBuffer = value; }
get indexBuffer(): IndexBuffer { return this._indexBuffer; }
setSubmeshes(value: readonly Submesh[]): Mesh {
this._submeshes.length = 0;
this._submeshes.push(...value);
return this;
}
getMaterials(res: Submesh[]): Submesh[] {
res.length = 0;
res.push(...this._submeshes);
return res;
}
} }
Object.defineProperty(Mesh.prototype, "type", { value: "Mesh" }); Object.defineProperty(Mesh.prototype, "type", { value: "Mesh" });

View File

@ -4,7 +4,8 @@
* obtain one at http://mozilla.org/MPL/2.0/. * obtain one at http://mozilla.org/MPL/2.0/.
*/ */
import { Camera, Light, Material, Matrix4x4, Mesh, Quaternion, QuaternionObject, Vector3, Vector3Object } from "."; import { Camera, DynamicMaterial, Light, Matrix4x4, Mesh, Quaternion, QuaternionObject, Vector3, Vector3Object } from ".";
import { Material } from "../resources";
export interface NodeProps { export interface NodeProps {
readonly name?: string; readonly name?: string;
@ -16,14 +17,14 @@ export interface NodeProps {
readonly camera?: Camera | null; readonly camera?: Camera | null;
readonly light?: Light | null; readonly light?: Light | null;
readonly mesh?: Mesh | null; readonly mesh?: Mesh | null;
readonly materials?: Material[]; readonly materials?: (Material | DynamicMaterial)[];
readonly children?: Node[]; readonly children?: Node[];
} }
export class Node { export class Node {
readonly type!: "Node"; declare readonly type: "Node";
_name: string; _name: string;
@ -38,7 +39,7 @@ export class Node {
/** shared */ /** shared */
_mesh: Mesh | null; _mesh: Mesh | null;
/** shared */ /** shared */
_materials: Material[]; _materials: (Material | DynamicMaterial)[];
/** unique */ /** unique */
_children: Node[]; _children: Node[];
@ -218,13 +219,13 @@ export class Node {
set mesh(value: Mesh | null) { this._mesh = value; } set mesh(value: Mesh | null) { this._mesh = value; }
get mesh(): Mesh | null { return this._mesh; } get mesh(): Mesh | null { return this._mesh; }
setMaterials(value: readonly Material[]): Node { setMaterials(value: readonly (Material | DynamicMaterial)[]): Node {
this._materials.length = 0; this._materials.length = 0;
this._materials.push(...value); this._materials.push(...value);
return this; return this;
} }
getMaterials(res: Material[]): Material[] { getMaterials(res: (Material | DynamicMaterial)[]): (Material | DynamicMaterial)[] {
res.length = 0; res.length = 0;
res.push(...this._materials); res.push(...this._materials);
return res; return res;

View File

@ -15,7 +15,7 @@ export type QuaternionTuple = readonly [x: number, y: number, z: number, w: numb
export class Quaternion { export class Quaternion {
readonly type!: "Quaternion"; declare readonly type: "Quaternion";
x: number; x: number;
y: number; y: number;
@ -41,6 +41,27 @@ export class Quaternion {
return new Quaternion(0, 0, 0, 1); return new Quaternion(0, 0, 0, 1);
} }
static fromRotationXY(angleRad: number): Quaternion {
const halfAngleRad = 0.5 * angleRad;
const c = Math.cos(halfAngleRad);
const s = Math.sin(halfAngleRad);
return new Quaternion(0, 0, s, c);
}
static fromRotationYZ(angleRad: number): Quaternion {
const halfAngleRad = 0.5 * angleRad;
const c = Math.cos(halfAngleRad);
const s = Math.sin(halfAngleRad);
return new Quaternion(s, 0, 0, c);
}
static fromRotationZX(angleRad: number): Quaternion {
const halfAngleRad = 0.5 * angleRad;
const c = Math.cos(halfAngleRad);
const s = Math.sin(halfAngleRad);
return new Quaternion(0, s, 0, c);
}
setObject(object: QuaternionObject): Quaternion { setObject(object: QuaternionObject): Quaternion {
this.x = object.x; this.x = object.x;
this.y = object.y; this.y = object.y;
@ -64,6 +85,33 @@ export class Quaternion {
this.w = 1; this.w = 1;
return this; return this;
} }
setRotationXY(angleRad: number): Quaternion {
const halfAngleRad = 0.5 * angleRad;
this.x = 0;
this.y = 0;
this.z = Math.sin(halfAngleRad);
this.w = Math.cos(halfAngleRad);
return this;
}
setRotationYZ(angleRad: number): Quaternion {
const halfAngleRad = 0.5 * angleRad;
this.x = Math.sin(halfAngleRad);
this.y = 0;
this.z = 0;
this.w = Math.cos(halfAngleRad);
return this;
}
setRotationZX(angleRad: number): Quaternion {
const halfAngleRad = 0.5 * angleRad;
this.x = 0;
this.y = Math.sin(halfAngleRad);
this.z = 0;
this.w = Math.cos(halfAngleRad);
return this;
}
} }
Object.defineProperty(Quaternion.prototype, "type", { value: "Quaternion" }); Object.defineProperty(Quaternion.prototype, "type", { value: "Quaternion" });

View File

@ -16,7 +16,7 @@ export interface SceneProps {
export class Scene { export class Scene {
readonly type!: "Scene"; declare readonly type: "Scene";
_name: string; _name: string;

View File

@ -13,7 +13,7 @@ export type Vector2Tuple = readonly [x: number, y: number];
export class Vector2 { export class Vector2 {
readonly type!: "Vector2"; declare readonly type: "Vector2";
x: number; x: number;
y: number; y: number;

View File

@ -14,7 +14,7 @@ export type Vector3Tuple = readonly [x: number, y: number, z: number];
export class Vector3 { export class Vector3 {
readonly type!: "Vector3"; declare readonly type: "Vector3";
x: number; x: number;
y: number; y: number;

View File

@ -15,7 +15,7 @@ export type Vector4Tuple = readonly [x: number, y: number, z: number, w: number]
export class Vector4 { export class Vector4 {
readonly type!: "Vector4"; declare readonly type: "Vector4";
x: number; x: number;
y: number; y: number;

View File

@ -6,8 +6,9 @@
export * from "./Camera"; export * from "./Camera";
export * from "./Color"; export * from "./Color";
export * from "./DynamicMaterial";
export * from "./Light"; export * from "./Light";
export * from "./Material"; export * from "./MaterialProps";
export * from "./Matrix4x4"; export * from "./Matrix4x4";
export * from "./Mesh"; export * from "./Mesh";
export * from "./Node"; export * from "./Node";

13
src/geometry.ts Normal file
View File

@ -0,0 +1,13 @@
/*!
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/.
*/
export function degToRad(angleDeg: number): number {
return angleDeg * Math.PI / 180;
}
export function radToDeg(angleRad: number): number {
return angleRad * 180 / Math.PI;
}

510
src/gltf.ts Normal file
View File

@ -0,0 +1,510 @@
/*!
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/.
*/
import * as data from "./data";
/* INITIAL SUPPORT PLAN
*
* Basic properties:
* - extensionsRequired:
* - issues error when any extension not supported at least partially
* - extensionsUsed: ignored
* - accessors: used indirectly
* - read when converting mesh
* - sparse: no support
* - issues error
* - animations: no support
* - issues warning
* - no animations emitted
* - asset:
* - version: verified
* - rest: ignored
* - buffers: used indirectly
* - read when converting mesh
* - uri: no support
* - issues error
* - bufferViews: used indirectly
* - read when converting mesh
* - cameras:
* - orthographic:
* - xmag: ignored
* - ymag: converted to halfVerticalSize
* - perspective:
* - aspectRatio: ignored
* - issues warning when provided
* - images:
* - uri: no support
* - issues error
* - materials:
* - name: full support
* - pbrMetallicRoughness:
* - baseColorFactor: full support
* - baseColorTexture: partial support
* - forced texCoord 0
* - issues error when different provided
* - metallicFactor: full support
* - roughnessFactor: full support
* - metallicRoughnessTexture: partial support
* - forced texCoord 0
* - issues error when different provided
* - normalTexture: partial support
* - scale: full support
* - forced texCoord 0
* - issues error when different provided
* - occlusionTexture: partial support
* - strength: full support
* - forced texCoord 1
* - issues error when different provided
* - emissiveTexture: partial support
* - forced texCoord 0
* - issues error when different provided
* - emissiveFactor: full support
* - alphaMode:
* - OPAQUE: full support
* - MASK: no support
* - issues error
* - BLEND: partial support
* - decoded, but not implemented
* - doubleSided: prtial support
* - decoded, but not implemented
*
* Extensions:
* - KHR_lights_punctual
* - name: full support
* - color/intensity: full support
* - converted to color = color * intensity
* - type:
* - directional: full support
* - point: full support
* - spot: no support
* - issues error
* - range: no support
* - issues warning
* - always infite range
* - KHR_materials_emissive_strength: full support
* - converted to emissive = emissive * strength
* - KHR_materials_ior: full support
* - when not provided, glTF's default is used (1.5) intead of oktaeder's (1.45)
* - KHR_materials_ior: full support
* - probably
*/
export interface ParseResult {
readonly cameras: readonly data.Camera[];
readonly materials: readonly data.Material[];
readonly lights: readonly data.Light[];
readonly scenes: readonly data.Scene[];
readonly scene: data.Scene | null;
readonly warnings: readonly ParseError[];
readonly errors: readonly ParseError[];
}
export interface ParseErrorProps {
message: string;
position?: JsonPosition | undefined;
severity: ParseErrorSeverity;
options?: ErrorOptions | undefined;
}
export type ParseErrorSeverity =
| "warning"
| "error"
;
export class ParseError extends Error {
override message: string;
position: JsonPosition | undefined;
severity: ParseErrorSeverity;
constructor({
message,
position,
severity,
options,
}: ParseErrorProps) {
super(message, options);
this.message = message;
this.position = position;
this.severity = severity;
}
}
export interface JsonPosition {
readonly line: number;
readonly column: number;
readonly path: number;
}
export interface ParseOptions {
/**
* When `true`, the parser will throw with a `ParseError` on the first error
* encountered. This includes warnings when `treatWarningsAsErrors` is
* `true`. When `false`, the parser will always return `ParseResult` and is
* never expected to throw. Failures are then communicated with the
* `ParseResult.errors` array.
*
* When this option is `true`, `stopOnFirstError` has no effect.
* @default true
*/
readonly throwOnError?: boolean;
/**
* When `true`, the parser will stop processing on the first error
* encountered. This includes warnings when `treatWarningsAsErrors` is
* `true`. When `false`, the parser will continue processing when it
* encounters an error that it consideres recoverable.
*
* This option has no effect when `throwOnError` is `true`.
* @default true
*/
readonly stopOnFirstError?: boolean;
/**
* When `true`, the parser will treat any encountered warning as a failure
* for the purpose of the other options. Note that regardless of this
* option, the warnings will always be returned in the
* `ParseResult.warnings` array and they will always have their `severity`
* property equal to `"warning"`.
* @default false
*/
readonly treatWarningsAsErrors?: boolean;
}
export async function parse(gltf: ArrayBufferView, {
throwOnError = true,
stopOnFirstError = true,
treatWarningsAsErrors = false,
}: ParseOptions = {}): Promise<ParseResult> {
const cameras: data.Camera[] = [];
const materials: data.Material[] = [];
const lights: data.Light[] = [];
const scenes: data.Scene[] = [];
const scene: data.Scene | null = null;
const warnings: ParseError[] = [];
const errors: ParseError[] = [];
function makeParseResult(): ParseResult {
return Object.freeze({
cameras: Object.freeze(cameras),
materials: Object.freeze(materials),
lights: Object.freeze(lights),
scenes: Object.freeze(scenes),
scene,
warnings: Object.freeze(warnings),
errors: Object.freeze(errors),
});
}
let gltfDataView = new DataView(gltf.buffer, gltf.byteOffset, gltf.byteLength);
// --- GLB HEADER ----------------------------------------------------------
if (gltfDataView.byteLength < 12) {
const message = `glTF buffer view is too short to be a valid binary glTF container. Binary glTF begins with a 12-byte header, but the provided buffer view has byte length of ${gltf.byteLength}`;
const error = new ParseError({ message, severity: "error" });
if (throwOnError) {
throw error;
}
errors.push(error);
// unrecoverable error
return makeParseResult();
}
const magic = gltfDataView.getUint32(0, true);
const version = gltfDataView.getUint32(4, true);
let length = gltfDataView.getUint32(8, true);
if (magic !== 0x46546C67) {
const message = `glTF container has invalid magic bytes. The first four bytes must have a value of 0x46546C67 when read as little endian unsigned integer, but in the provided buffer view they have the value of ${u32toHexString(magic)}`;
const error = new ParseError({ message, severity: "error" });
if (throwOnError) {
throw error;
}
errors.push(error);
/* NOTE This error is considered unrecoverable, because it is very
* likely that when the magic bytes are wrong, the provided buffer
* view points to a completely different format or garbage data and
* it would be pointless to continue parsing in this case.
*/
return makeParseResult();
}
if (version !== 2) {
const message = `Unsupported binary glTF container format. The bytes 4-8 define the binary glTF conatiner format version when read as little endian unsigned integer. Only version 2 is supported, but in the provided buffer they have the value of ${version}`;
const error = new ParseError({ message, severity: "error" });
if (throwOnError) {
throw error;
}
errors.push(error);
// unrecoverable error
return makeParseResult();
}
if (length !== gltf.byteLength) {
const message = `Invalid glTF container length. The bytes 8-12 define the length in bytes of the entirety of the binary glTF container when read as little endian unsigned integer. The container byte length is defined as ${length}, but the provided buffer view has byte length of ${gltf.byteLength}`;
const error = new ParseError({ message, severity: "error" });
if (throwOnError) {
throw error;
}
errors.push(error);
// recovery: use the lower length value and pretend its the actual length
length = Math.min(length, gltf.byteLength);
gltfDataView = new DataView(gltf.buffer, gltf.byteOffset, length);
}
let rest = new DataView(gltf.buffer, gltf.byteOffset + 12, gltf.byteLength - 12);
// --- JSON CHUNK ----------------------------------------------------------
throw new Error("TODO");
// --- BIN CHUNK -----------------------------------------------------------
}
function u32toHexString(value: number) {
return "0x" + ("00000000" + value.toString(16)).slice(-8);
}
// --- GLTF DATA STRUCTURES ----------------------------------------------------
export interface Gltf {
extensionsRequired?: [string, ...string[]];
accessors?: [Accessor, ...Accessor[]];
asset: Asset;
buffers?: [Buffer, ...Buffer[]];
bufferViews?: [BufferView, ...BufferView[]];
cameras?: [Camera, ...Camera[]];
images?: [Image, ...Image[]];
materials?: [Material, ...Material[]];
meshes?: [Mesh, ...Mesh[]];
nodes?: [Node, ...Node[]];
samplers?: [Sampler, ...Sampler[]];
scene?: number;
scenes?: [number, ...number[]];
textures?: [Texture, ...Texture[]];
}
export interface Accessor {
bufferView?: number;
/** @default 0 */
byteOffset?: number;
componentType: ComponentType;
/** @default false */
normalized?: boolean;
count: number;
type: AccessorType;
}
export enum ComponentType {
Byte = 5120,
UnsignedByte = 5121,
Short = 5122,
UnsignedShort = 5123,
UnsignedInt = 5125,
Float = 5126,
}
export type AccessorType =
| "SCALAR"
| "VEC2"
| "VEC3"
| "VEC4"
| "MAT2"
| "MAT3"
| "MAT4"
;
export interface Asset {
version: `${string}.${string}`;
minVersion: `${string}.${string}`;
}
export interface Buffer {
uri?: string;
byteLength: number;
}
export interface BufferView {
buffer: number;
/** @default 0 */
byteOffset?: number;
byteLength: number;
byteStride?: number;
}
export type Camera =
| CameraOrthographic
| CameraPerspective
;
export interface CameraOrthographic {
orthographic: Orthographic;
type: "orthographic";
}
export interface CameraPerspective {
orthographic: Perspective;
type: "perspective";
}
export interface Orthographic {
xmag: number;
ymag: number;
zfar: number;
znear: number;
}
export interface Perspective {
aspectRatio?: number;
yfov: number;
zfar?: number;
znear: number;
}
export interface Image {
uri?: string;
mimeType?: ImageMimeType;
bufferView?: number;
name?: string;
}
export type ImageMimeType =
| "image/jpeg"
| "image/png"
;
export interface Material {
name?: string;
extensions?: MaterialExtensions;
pbrMetallicRoughness?: MaterialPbrMetallicRoughness;
normalTexture?: NormalTextureInfo;
occlusionTexture?: OcclusionTextureInfo;
emissiveTexture?: TextureInfo;
emissiveFactor?: [r: number, g: number, b: number];
alphaMode?: AlphaMode;
/** @default false */
doubleSided?: boolean;
}
export interface MaterialExtensions {
KHR_materials_emissive_strength?: KHR_materials_emissive_strength;
KHR_materials_ior?: KHR_materials_ior;
}
export interface KHR_materials_emissive_strength {
/** @default 1 */
emissiveStrength?: number;
}
export interface KHR_materials_ior {
/** @default 1.5 */
ior?: number;
}
export interface MaterialPbrMetallicRoughness {
/** @default [1, 1, 1, 1] */
baseColorFactor?: [r: number, b: number, g: number, partialCoverage: number];
baseColorTexture?: TextureInfo;
/** @default 1 */
metallicFactor?: number;
/** @default 1 */
roughnessFactor?: number;
metallicRoughnessTexture?: TextureInfo;
}
export interface TextureInfo {
index: number;
/** @default 0 */
texCoord?: number;
}
export interface NormalTextureInfo extends TextureInfo {
/** @default 1 */
scale?: number;
}
export interface OcclusionTextureInfo extends TextureInfo {
/** @default 1 */
strength?: number;
}
export type AlphaMode =
| "OPAQUE"
| "MASK"
| "BLEND"
;
export interface Mesh {
primitives: [Primitive, ...Primitive[]];
name?: string;
}
export interface Primitive {
attributes: {
POSITION?: number,
NORMAL?: number,
TANGENT?: number,
TEXCOORD_0?: number,
TEXCOORD_1?: number,
};
indices?: number;
material?: number;
/** @default PrimitiveMode.Triangles */
mode?: PrimitiveMode;
}
export enum PrimitiveMode {
Points = 0,
Lines = 1,
LineLoop = 2,
LineStrip = 3,
Triangles = 4,
TriangleStrip = 5,
TriangleFan = 6,
}
export interface Sampler {
magFilter?: MagFilter;
minFilter?: MinFilter;
/** @default WrappingMode.Repeat */
wrapS?: WrappingMode;
/** @default WrappingMode.Repeat */
wrapT?: WrappingMode;
}
export enum MagFilter {
Nearest = 9728,
Linear = 9729,
}
export enum MinFilter {
Nearest = 9728,
Linear = 9729,
NearestMipmapNearest = 9984,
LinearMipmapNearest = 9985,
NearestMipmapLinear = 9986,
LinearMipmapLinear = 9987,
}
export enum WrappingMode {
ClampToEdge = 33071,
MirroredRepeat = 33648,
Repeat = 10497,
}
export interface Texture {
sampler?: number;
source?: number;
name?: string;
}

View File

@ -5,13 +5,14 @@
*/ */
export * from "./_BinaryWriter"; export * from "./_BinaryWriter";
export * from "./geometry";
export * from "./shader"; export * from "./shader";
import { _BinaryWriter as BinaryWriter } from "./_BinaryWriter"; import { _BinaryWriter as BinaryWriter } from "./_BinaryWriter";
import { _Mapping as Mapping } from "./_Mapping"; import { _Mapping as Mapping } from "./_Mapping";
import { Camera, Material, Matrix4x4, Node, Scene, Vector3, isDirectionalLight, isPointLight, preOrder } from "./data"; import { Camera, DynamicMaterial, MaterialProps, Matrix4x4, Node, Scene, Vector3, isDirectionalLight, isDynamicMaterial, isPointLight, preOrder } from "./data";
import { IndexBuffer, IndexBufferProps, Texture2D, Texture2DProps, VertexBuffer, VertexBufferProps } from "./resources"; import { IndexBuffer, IndexBufferProps, Material, Texture2D, Texture2DProps, VertexBuffer, VertexBufferProps, isMaterial } 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( const _matrixOStoWSNormal = new Matrix4x4(
NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN,
@ -117,6 +118,7 @@ export class Renderer {
width: framebufferTexture.width, width: framebufferTexture.width,
height: framebufferTexture.height, height: framebufferTexture.height,
format: "depth", format: "depth",
usage: GPUTextureUsage.RENDER_ATTACHMENT,
}); });
this._globalBindGroupLayout = device.createBindGroupLayout({ this._globalBindGroupLayout = device.createBindGroupLayout({
@ -266,7 +268,7 @@ export class Renderer {
this._globalBindGroup = device.createBindGroup({ this._globalBindGroup = device.createBindGroup({
layout: this._globalBindGroupLayout, layout: this._globalBindGroupLayout,
entries: [ entries: [
{ binding: 0, resource: { buffer: this._uniformBuffer } }, { binding: 0, resource: { buffer: this._uniformBuffer, size: GLOBAL_UNIFORMS_SIZE } },
{ binding: 1, resource: { buffer: this._pointLightBuffer } }, { binding: 1, resource: { buffer: this._pointLightBuffer } },
{ binding: 2, resource: { buffer: this._directionalLightBuffer } }, { binding: 2, resource: { buffer: this._directionalLightBuffer } },
], ],
@ -275,7 +277,7 @@ export class Renderer {
this._objectBindGroup = device.createBindGroup({ this._objectBindGroup = device.createBindGroup({
layout: this._objectBindGroupLayout, layout: this._objectBindGroupLayout,
entries: [ entries: [
{ binding: 0, resource: { buffer: this._uniformBuffer } }, { binding: 0, resource: { buffer: this._uniformBuffer, size: OBJECT_UNIFORMS_SIZE } },
], ],
label: "Object", label: "Object",
}); });
@ -327,6 +329,10 @@ export class Renderer {
return new IndexBuffer(this, props); return new IndexBuffer(this, props);
} }
createMaterial(props: MaterialProps): Material {
return new Material(this, props);
}
createTexture(props: Texture2DProps): Texture2D { createTexture(props: Texture2DProps): Texture2D {
return new Texture2D(this, props); return new Texture2D(this, props);
} }
@ -336,14 +342,14 @@ export class Renderer {
} }
_getOrCreatePipeline(flags: ShaderFlags): GPURenderPipeline { _getOrCreatePipeline(flags: ShaderFlags): GPURenderPipeline {
const key = shaderFlagsKey(flags); const key = _shaderFlagsKey(flags);
let pipeline = this._pipelineCache.get(key); let pipeline = this._pipelineCache.get(key);
if (pipeline !== undefined) { if (pipeline !== undefined) {
return pipeline; return pipeline;
} }
pipeline = createPipeline(this, flags); pipeline = _createPipeline(this, flags);
this._pipelineCache.set(key, pipeline); this._pipelineCache.set(key, pipeline);
return pipeline; return pipeline;
} }
@ -380,16 +386,18 @@ export class Renderer {
this._uniformWriter.clear(); this._uniformWriter.clear();
// gather materials // gather dynamic materials
const materialMapping = new Mapping<Material>(); const dynamicMaterialMapping = new Mapping<DynamicMaterial>();
for (const node of preOrder(scene._nodes)) { for (const node of preOrder(scene._nodes)) {
for (const material of node._materials) { for (const material of node._materials) {
materialMapping.add(material); if (isDynamicMaterial(material)) {
dynamicMaterialMapping.add(material);
}
} }
} }
const materialBindGroups = materialMapping.table.map((material) => { const dynamicMaterialBindGroups = dynamicMaterialMapping.table.map((material) => {
const offset = this._uniformWriter._length; const offset = this._uniformWriter._length;
this._uniformWriter.writeColorF32(material._baseColor); this._uniformWriter.writeColorF32(material._baseColor);
this._uniformWriter.writeF32(material._partialCoverage); this._uniformWriter.writeF32(material._partialCoverage);
@ -401,11 +409,12 @@ export class Renderer {
this._uniformWriter.writeF32(material._normalScale); this._uniformWriter.writeF32(material._normalScale);
this._uniformWriter.writeColorF32(material._emissive); this._uniformWriter.writeColorF32(material._emissive);
this._uniformWriter.writeF32(material._ior); this._uniformWriter.writeF32(material._ior);
this._uniformWriter.padToAlign(256);
const bindGroup = this._device.createBindGroup({ const bindGroup = this._device.createBindGroup({
layout: this._materialBindGroupLayout, layout: this._materialBindGroupLayout,
entries: [ entries: [
{ binding: 0, resource: { buffer: this._uniformBuffer } }, { binding: 0, resource: { buffer: this._uniformBuffer, size: MATERIAL_UNIFORMS_SIZE } },
{ binding: 1, resource: this._sampler }, { binding: 1, resource: this._sampler },
{ binding: 2, resource: material._baseColorPartialCoverageTexture?._textureView ?? this._textureWhite._textureView }, { binding: 2, resource: material._baseColorPartialCoverageTexture?._textureView ?? this._textureWhite._textureView },
{ binding: 3, resource: material._occlusionTexture?._textureView ?? this._textureWhite._textureView }, { binding: 3, resource: material._occlusionTexture?._textureView ?? this._textureWhite._textureView },
@ -433,6 +442,7 @@ export class Renderer {
object._updateWorldMatrix(); object._updateWorldMatrix();
this._uniformWriter.writeMatrix4x4(object._worldMatrix); this._uniformWriter.writeMatrix4x4(object._worldMatrix);
this._uniformWriter.writeMatrix4x4(_matrixOStoWSNormal.setObject(object._worldMatrix).inverseTransposeAffine()); this._uniformWriter.writeMatrix4x4(_matrixOStoWSNormal.setObject(object._worldMatrix).inverseTransposeAffine());
this._uniformWriter.padToAlign(256);
return offset; return offset;
}); });
@ -491,9 +501,7 @@ export class Renderer {
this._uniformWriter.writeColorF32(scene._ambientLight); this._uniformWriter.writeColorF32(scene._ambientLight);
this._uniformWriter.writeU32(pointLightCount); this._uniformWriter.writeU32(pointLightCount);
this._uniformWriter.writeU32(directionalLightCount); this._uniformWriter.writeU32(directionalLightCount);
this._uniformWriter.writeU32(0); this._uniformWriter.padToAlign(256);
this._uniformWriter.writeU32(0);
this._uniformWriter.writeU32(0);
// upload uniforms // upload uniforms
@ -520,11 +528,18 @@ export class Renderer {
pass.setPipeline(renderPipeline); 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(0, vertexBuffer._positionBuffer);
pass.setVertexBuffer(1, vertexBuffer._texCoordBuffer); if (vertexBuffer._texCoordBuffer !== null) pass.setVertexBuffer(1, vertexBuffer._texCoordBuffer);
pass.setVertexBuffer(2, vertexBuffer._lightTexCoordBuffer); if (vertexBuffer._lightTexCoordBuffer !== null) pass.setVertexBuffer(2, vertexBuffer._lightTexCoordBuffer);
pass.setVertexBuffer(3, vertexBuffer._normalBuffer); if (vertexBuffer._normalBuffer !== null) pass.setVertexBuffer(3, vertexBuffer._normalBuffer);
pass.setVertexBuffer(4, vertexBuffer._tangentBuffer); if (vertexBuffer._tangentBuffer !== null) pass.setVertexBuffer(4, vertexBuffer._tangentBuffer);
pass.setIndexBuffer(indexBuffer._buffer, indexBuffer._indexFormat); pass.setIndexBuffer(indexBuffer._buffer, indexBuffer._indexFormat);
pass.setBindGroup(2, this._objectBindGroup, [objectOffset]); pass.setBindGroup(2, this._objectBindGroup, [objectOffset]);
@ -532,9 +547,17 @@ export class Renderer {
for (let si = 0; si < mesh._submeshes.length; ++si) { for (let si = 0; si < mesh._submeshes.length; ++si) {
const submesh = mesh._submeshes[si]!; const submesh = mesh._submeshes[si]!;
const material = object._materials[si]!; const material = object._materials[si]!;
const { bindGroup: materialBindGroup, offset: materialOffset } = materialBindGroups[materialMapping.get(material)!]!;
if (isMaterial(material)) {
pass.setBindGroup(1, material._bindGroup, [0]);
} else if (isDynamicMaterial(material)) {
const {
bindGroup: materialBindGroup,
offset: materialOffset
} = dynamicMaterialBindGroups[dynamicMaterialMapping.get(material)!]!;
pass.setBindGroup(1, materialBindGroup, [materialOffset]); pass.setBindGroup(1, materialBindGroup, [materialOffset]);
}
pass.drawIndexed(submesh.length, 1, submesh.start, 0, 0); pass.drawIndexed(submesh.length, 1, submesh.start, 0, 0);
} }
} }

View File

@ -20,7 +20,7 @@ export interface IndexBufferResizeProps {
export class IndexBuffer { export class IndexBuffer {
readonly type!: "IndexBuffer"; declare readonly type: "IndexBuffer";
_renderer: Renderer; _renderer: Renderer;
_name: string; _name: string;

169
src/resources/Material.ts Normal file
View File

@ -0,0 +1,169 @@
/*!
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Texture2D } from ".";
import { Color, MaterialProps } from "../data";
import { Renderer, _BinaryWriter } from "../oktaeder";
export class Material {
declare readonly type: "Material";
_renderer: Renderer;
_uniformBuffer: GPUBuffer;
_bindGroup: GPUBindGroup;
_name: string;
readonly _baseColor: Color;
readonly _partialCoverage: number;
readonly _occlusionTextureStrength: number;
readonly _metallic: number;
readonly _roughness: number;
readonly _normalScale: number;
readonly _emissive: Color;
readonly _transmission: Color;
readonly _collimation: number;
readonly _ior: number;
readonly _baseColorPartialCoverageTexture: Texture2D | null;
readonly _occlusionTexture: Texture2D | null;
readonly _roughnessMetallicTexture: Texture2D | null;
readonly _normalTexture: Texture2D | null;
readonly _emissiveTexture: Texture2D | null;
readonly _transmissionCollimationTexture: Texture2D | null;
readonly _transparent: boolean;
readonly _doubleSided: boolean;
constructor(renderer: Renderer, {
name = "",
baseColor,
partialCoverage = 1,
occlusionTextureStrength = 1,
metallic = 1,
roughness = 1,
normalScale = 1,
emissive,
transmission,
collimation = 1,
ior = 1.45,
baseColorPartialCoverageTexture = null,
occlusionTexture = null,
roughnessMetallicTexture = null,
normalTexture = null,
emissiveTexture = null,
transmissionCollimationTexture = null,
transparent = false,
doubleSided = false,
}: MaterialProps) {
this._renderer = renderer;
this._name = name;
this._baseColor = baseColor !== undefined ? Color.fromObject(baseColor) : Color.white();
this._partialCoverage = partialCoverage;
this._occlusionTextureStrength = occlusionTextureStrength;
this._metallic = metallic;
this._roughness = roughness;
this._normalScale = normalScale;
this._emissive = emissive !== undefined ? Color.fromObject(emissive) : Color.black();
this._transmission = transmission !== undefined ? Color.fromObject(transmission) : Color.black();
this._collimation = collimation;
this._ior = ior;
this._baseColorPartialCoverageTexture = baseColorPartialCoverageTexture;
this._occlusionTexture = occlusionTexture;
this._roughnessMetallicTexture = roughnessMetallicTexture;
this._normalTexture = normalTexture;
this._emissiveTexture = emissiveTexture;
this._transmissionCollimationTexture = transmissionCollimationTexture;
this._transparent = transparent;
this._doubleSided = doubleSided;
this._uniformBuffer = renderer._device.createBuffer({
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM,
size: 64,
label: name,
});
const writer = new _BinaryWriter(64);
writer.writeColorF32(this._baseColor);
writer.writeF32(this._partialCoverage);
writer.writeColorF32(this._transmission);
writer.writeF32(this._collimation);
writer.writeF32(this._occlusionTextureStrength);
writer.writeF32(this._roughness);
writer.writeF32(this._metallic);
writer.writeF32(this._normalScale);
writer.writeColorF32(this._emissive);
writer.writeF32(this._ior);
renderer._device.queue.writeBuffer(this._uniformBuffer, 0, writer.subarray);
this._bindGroup = renderer._device.createBindGroup({
layout: renderer._materialBindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: this._uniformBuffer, size: 64 } },
{ binding: 1, resource: renderer._sampler },
{ binding: 2, resource: this._baseColorPartialCoverageTexture?._textureView ?? renderer._textureWhite._textureView },
{ binding: 3, resource: this._occlusionTexture?._textureView ?? renderer._textureWhite._textureView },
{ binding: 4, resource: this._roughnessMetallicTexture?._textureView ?? renderer._textureWhite._textureView },
{ binding: 5, resource: this._normalTexture?._textureView ?? renderer._textureNormal._textureView },
{ binding: 6, resource: this._emissiveTexture?._textureView ?? renderer._textureWhite._textureView },
{ binding: 7, resource: this._transmissionCollimationTexture?._textureView ?? renderer._textureBlack._textureView },
],
label: name,
});
}
/**
* Destroys owned GPU resources. The index buffer should not be used after
* calling this method.
* @returns `this` for chaining
*/
dispose(): Material {
this._uniformBuffer.destroy();
return this;
}
getBaseColor(res: Color): Color {
return res.setObject(this._baseColor);
}
get partialCoverage(): number { return this._partialCoverage; }
get occlusionTextureStrength(): number { return this._occlusionTextureStrength; }
get metallic(): number { return this._metallic; }
get roughness(): number { return this._roughness; }
get normalScale(): number { return this._normalScale; }
getEmissive(res: Color): Color {
return res.setObject(this._emissive);
}
getTransmission(res: Color): Color {
return res.setObject(this._transmission);
}
get collimation(): number { return this._collimation; }
get ior(): number { return this._ior; }
get baseColorPartialCoverageTexture(): Texture2D | null { return this._baseColorPartialCoverageTexture; }
get occlusionTexture(): Texture2D | null { return this._occlusionTexture; }
get roughnessMetallicTexture(): Texture2D | null { return this._roughnessMetallicTexture; }
get normalTexture(): Texture2D | null { return this._normalTexture; }
get emissiveTexture(): Texture2D | null { return this._emissiveTexture; }
get transmissionCollimationTexture(): Texture2D | null { return this._transmissionCollimationTexture; }
get transparent(): boolean { return this._transparent; }
get doubleSided(): boolean { return this._doubleSided; }
}
Object.defineProperty(Material.prototype, "type", { value: "Material" });
export function isMaterial(value: unknown): value is Material {
return Boolean(value) && (value as Material).type === "Material";
}

View File

@ -21,12 +21,15 @@ export interface Texture2DProps {
readonly height: number; readonly height: number;
readonly format: Texture2DFormat; readonly format: Texture2DFormat;
readonly usage?: GPUTextureUsageFlags;
} }
export interface Texture2DResizeProps { export interface Texture2DResizeProps {
readonly width?: number; readonly width?: number;
readonly height?: number; readonly height?: number;
readonly format?: Texture2DFormat; readonly format?: Texture2DFormat;
readonly usage?: GPUTextureUsageFlags;
} }
export interface Texture2DAdvancedWriteProps { export interface Texture2DAdvancedWriteProps {
@ -39,7 +42,7 @@ export interface Texture2DAdvancedWriteProps {
export class Texture2D { export class Texture2D {
readonly type!: "Texture2D"; declare readonly type: "Texture2D";
_renderer: Renderer; _renderer: Renderer;
_name: string; _name: string;
@ -53,6 +56,7 @@ export class Texture2D {
width, width,
height, height,
format, format,
usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
}: Texture2DProps) { }: Texture2DProps) {
this._renderer = renderer; this._renderer = renderer;
@ -62,7 +66,7 @@ export class Texture2D {
this._renderer = renderer; this._renderer = renderer;
this._texture = renderer._device.createTexture({ this._texture = renderer._device.createTexture({
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST, usage,
size: { width, height }, size: { width, height },
format: gpuFormat, format: gpuFormat,
label: name label: name
@ -147,13 +151,14 @@ export class Texture2D {
width = this._texture.width, width = this._texture.width,
height = this._texture.height, height = this._texture.height,
format = this._format, format = this._format,
usage = this._texture.usage,
}: Texture2DResizeProps): Texture2D { }: Texture2DResizeProps): Texture2D {
this._texture.destroy(); this._texture.destroy();
const gpuFormat = gpuTextureFormat(format); const gpuFormat = gpuTextureFormat(format);
this._texture = this._renderer._device.createTexture({ this._texture = this._renderer._device.createTexture({
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST, usage,
size: { width, height }, size: { width, height },
format: gpuFormat, format: gpuFormat,
label: this._name label: this._name

View File

@ -51,7 +51,7 @@ export interface VertexBufferWriteTypedArrayProps {
export class VertexBuffer { export class VertexBuffer {
readonly type!: "VertexBuffer"; declare readonly type: "VertexBuffer";
_renderer: Renderer; _renderer: Renderer;
_name: string; _name: string;

View File

@ -6,6 +6,13 @@
import { Renderer } from "./oktaeder"; 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 type ShaderFlagKey = number;
export interface ShaderFlags { export interface ShaderFlags {
@ -15,7 +22,7 @@ export interface ShaderFlags {
readonly tangent: boolean; readonly tangent: boolean;
} }
export function shaderFlagsKey({ export function _shaderFlagsKey({
texCoord, texCoord,
lightTexCoord, lightTexCoord,
normal, normal,
@ -29,13 +36,13 @@ export function shaderFlagsKey({
return key; return key;
} }
export function createPipeline(renderer: Renderer, { export function _createPipeline(renderer: Renderer, {
texCoord, texCoord,
lightTexCoord, lightTexCoord,
normal, normal,
tangent, tangent,
}: ShaderFlags): GPURenderPipeline { }: ShaderFlags): GPURenderPipeline {
const shaderCode = createShaderCode({ texCoord, lightTexCoord, normal, tangent }); const shaderCode = _createShaderCode({ texCoord, lightTexCoord, normal, tangent });
const shaderModule = renderer._device.createShaderModule({ const shaderModule = renderer._device.createShaderModule({
code: shaderCode, code: shaderCode,
@ -130,14 +137,12 @@ export function createPipeline(renderer: Renderer, {
return pipeline; return pipeline;
} }
export function createShaderCode({ export function _createShaderCode({
texCoord, texCoord,
lightTexCoord, lightTexCoord,
normal, normal,
tangent, tangent,
}: ShaderFlags): string { }: ShaderFlags): string {
let varyingLocation = 0;
return ` return `
struct Vertex { struct Vertex {
@location(0) positionOS: vec3<f32>, @location(0) positionOS: vec3<f32>,
@ -149,12 +154,12 @@ struct Vertex {
struct Varyings { struct Varyings {
@builtin(position) positionCS: vec4<f32>, @builtin(position) positionCS: vec4<f32>,
@location(${varyingLocation++}) positionVS: vec4<f32>, @location(0) positionVS: vec3<f32>,
${texCoord ? `@location(${varyingLocation++}) texCoord: vec2<f32>,` : ""} ${texCoord ? `@location(1) texCoord: vec2<f32>,` : ""}
${lightTexCoord ? `@location(${varyingLocation++}) lightTexCoord: vec2<f32>,` : ""} ${lightTexCoord ? `@location(2) lightTexCoord: vec2<f32>,` : ""}
${normal ? `@location(${varyingLocation++}) normalVS: vec3<f32>,` : ""} ${normal ? `@location(3) normalVS: vec3<f32>,` : ""}
${normal && tangent ? `@location(${varyingLocation++}) tangentVS: vec3<f32>,` : ""} ${normal && tangent ? `@location(4) tangentVS: vec3<f32>,` : ""}
${normal && tangent ? `@location(${varyingLocation++}) bitangentVS: vec3<f32>,` : ""} ${normal && tangent ? `@location(5) bitangentVS: vec3<f32>,` : ""}
} }
struct PointLight { struct PointLight {
@ -208,6 +213,57 @@ struct ObjectUniforms {
@group(1) @binding(6) var _EmissiveTexture: texture_2d<f32>; @group(1) @binding(6) var _EmissiveTexture: texture_2d<f32>;
@group(1) @binding(7) var _TransmissionCollimationTexture: texture_2d<f32>; @group(1) @binding(7) var _TransmissionCollimationTexture: texture_2d<f32>;
const INV_PI: f32 = 0.31830987;
fn fresnelSchlick(dotVH: f32, f0: vec3<f32>) -> vec3<f32> {
const f90 = vec3(1.0);
return f0 + (f90 - f0) * pow(1.0 - dotVH, 5.0);
}
fn visibilityGGX(dotNL: f32, dotNV: f32, alpha: f32) -> f32 {
let alphaSquared = alpha * alpha;
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 select(0.0, 0.5 / GGX, GGX > 0.0);
}
fn distributionGGX(dotNH: f32, alpha: f32) -> f32 {
let alphaSquared = alpha * alpha;
let tmp = dotNH * dotNH * (alphaSquared - 1.0) + 1.0;
return alphaSquared * INV_PI / (tmp * tmp);
}
fn toneMapAcesNarkowicz(color: vec3<f32>) -> vec3<f32> {
const A: f32 = 2.51;
const B: f32 = 0.03;
const C: f32 = 2.43;
const D: f32 = 0.59;
const E: f32 = 0.14;
return saturate((color * (A * color + B)) / (color * (C * color + D) + E));
}
fn lightOutgoingRadiance(
viewDirectionVS: vec3<f32>, actualNormalVS: vec3<f32>, dotNV: f32,
baseColor: vec3<f32>, alpha: f32, metallic: f32, f0: vec3<f32>,
incomingRadiance: vec3<f32>, lightDirectionVS: vec3<f32>,
) -> vec3<f32> {
let halfVectorVS = normalize(lightDirectionVS + viewDirectionVS);
let dotVH = saturate(dot(viewDirectionVS, halfVectorVS));
let dotNH = saturate(dot(actualNormalVS, halfVectorVS));
let dotNL = saturate(dot(actualNormalVS, lightDirectionVS));
let fresnel = fresnelSchlick(dotVH, f0);
let visibility = visibilityGGX(dotNL, dotNV, alpha);
let distribution = distributionGGX(dotNH, alpha);
let scatteredFactor = (1.0 - fresnel) * (1.0 - metallic) * baseColor * INV_PI;
let reflectedFactor = fresnel * visibility * distribution;
return (scatteredFactor + reflectedFactor) * incomingRadiance * dotNL;
}
fn screenSpaceMatrixTStoVS(positionVS: vec3<f32>, normalVS: vec3<f32>, texCoord: vec2<f32>) -> mat3x3<f32> { fn screenSpaceMatrixTStoVS(positionVS: vec3<f32>, normalVS: vec3<f32>, texCoord: vec2<f32>) -> mat3x3<f32> {
let q0 = dpdx(positionVS); let q0 = dpdx(positionVS);
let q1 = dpdy(positionVS); let q1 = dpdy(positionVS);
@ -221,7 +277,7 @@ fn screenSpaceMatrixTStoVS(positionVS: vec3<f32>, normalVS: vec3<f32>, texCoord:
let bitangentVS = q1perp * uv0.y + q0perp * uv1.y; let bitangentVS = q1perp * uv0.y + q0perp * uv1.y;
let det = max(dot(tangentVS, tangentVS), dot(bitangentVS, bitangentVS)); 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); return mat3x3(tangentVS * scale, bitangentVS * scale, normalVS);
} }
@ -248,10 +304,11 @@ fn vert(vertex: Vertex) -> Varyings {
` : ""} ` : ""}
${texCoord ? "output.texCoord = vertex.texCoord;" : ""} ${texCoord ? "output.texCoord = vertex.texCoord;" : ""}
${lightTexCoord ? "output.lightTexCoord = vertex.lightTexCoord;" : ""} ${lightTexCoord ? "output.lightTexCoord = vertex.lightTexCoord;" : ""}
return output;
} }
@fragment @fragment
fn frag(fragment: Varyings) -> @location(0) vec2<f32> { fn frag(fragment: Varyings) -> @location(0) vec4<f32> {
var baseColor = _Material.baseColor; var baseColor = _Material.baseColor;
var partialCoverage = _Material.partialCoverage; var partialCoverage = _Material.partialCoverage;
var occlusion = 1.0; var occlusion = 1.0;
@ -298,5 +355,50 @@ fn frag(fragment: Varyings) -> @location(0) vec2<f32> {
` : ` ` : `
let actualNormalVS = geometricNormalVS; let actualNormalVS = geometricNormalVS;
`} `}
let viewDirectionVS = normalize(-positionVS);
let dotNV = saturate(dot(actualNormalVS, viewDirectionVS));
let alpha = roughness * roughness;
var f0 = vec3(pow((ior - 1.0) / (ior + 1.0), 2.0));
f0 = mix(f0, baseColor, metallic);
var outgoingRadiance = vec3(0.0);
for (var i: u32 = 0; i < _Global.pointLightCount; i++) {
let light = _PointLights[i];
let lightPositionVS = (_Global.matrixWStoVS * vec4(light.positionWS, 1.0)).xyz;
let lightDirectionVS = normalize(lightPositionVS - positionVS);
let lightDistance = distance(positionVS, lightPositionVS);
let lightAttenuation = 1.0 / (lightDistance * lightDistance);
let incomingRadiance = light.color * lightAttenuation;
outgoingRadiance += lightOutgoingRadiance(
viewDirectionVS, actualNormalVS, dotNV,
baseColor, alpha, metallic, f0,
incomingRadiance, lightDirectionVS,
);
}
for (var i: u32 = 0; i < _Global.directionalLightCount; i++) {
let light = _DirectionalLights[i];
let lightDirectionVS = normalize((_Global.matrixWStoVS * vec4(light.directionWS, 0.0)).xyz);
let incomingRadiance = light.color;
outgoingRadiance += lightOutgoingRadiance(
viewDirectionVS, actualNormalVS, dotNV,
baseColor, alpha, metallic, f0,
incomingRadiance, lightDirectionVS,
);
}
outgoingRadiance += _Global.ambientLight * baseColor * occlusion;
let toneMappedLinearColor = toneMapAcesNarkowicz(outgoingRadiance);
let toneMappedSrgbColor = pow(toneMappedLinearColor, vec3(1.0 / 2.2));
return vec4(toneMappedSrgbColor, 1.0);
}`; }`;
} }

View File

@ -53,4 +53,5 @@
"skipLibCheck": false, "skipLibCheck": false,
}, },
"include": ["./src/**/*"],
} }