Compare commits
12 Commits
a7219eae86
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d28d7896de | ||
| 78683f6115 | |||
| 6e3d68e984 | |||
| da9361df15 | |||
| 38708060d8 | |||
| 1355c4e342 | |||
| ef7654024a | |||
| 7a509b654c | |||
| e26be8ee09 | |||
| 5002138070 | |||
| dcfd486dea | |||
| e70fcbc582 |
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.webm filter=lfs diff=lfs merge=lfs -text
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
10
README.md
10
README.md
@@ -1,2 +1,12 @@
|
||||
# oktaeder
|
||||
3D rendering library for WebGPU
|
||||
|
||||
<video src="https://gitea.renati.me/renati/oktaeder/media/branch/main/oktaeder.webm" autoplay controls loop>
|
||||
</video>
|
||||
|
||||
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).
|
||||
|
||||
11
example/index.html
Normal file
11
example/index.html
Normal 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>
|
||||
138
example/script.ts
Normal file
138
example/script.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/// <reference types="../node_modules/@webgpu/types" />
|
||||
|
||||
import { Color, 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: 12, texCoord: true });
|
||||
vertexBuffer.writeTypedArray(0, {
|
||||
position: new Float32Array([
|
||||
0, 0, 1,
|
||||
1, 0, 0,
|
||||
0, 1, 0,
|
||||
-1, 0, 0,
|
||||
0, 0, -1,
|
||||
0, 0, -1,
|
||||
0, 0, -1,
|
||||
1, 0, 0,
|
||||
0, -1, 0,
|
||||
-1, 0, 0,
|
||||
0, 0, -1,
|
||||
0, 0, 1,
|
||||
]),
|
||||
texCoord: new Float32Array([
|
||||
0.5, 0.7113,
|
||||
0.333333, 1,
|
||||
0.166666, 0.7113,
|
||||
0.333333, 0.4226,
|
||||
0, 0.4226,
|
||||
0, 1,
|
||||
1, 1,
|
||||
0.666666, 1,
|
||||
0.833333, 0.7113,
|
||||
0.666666, 0.4226,
|
||||
1, 0.4226,
|
||||
0.5, 0.7113,
|
||||
]),
|
||||
});
|
||||
|
||||
const indexBuffer = renderer.createIndexBuffer({ indexCount: 24, indexFormat: "uint16" });
|
||||
indexBuffer.writeArray(0, [
|
||||
0, 2, 1,
|
||||
3, 4, 2,
|
||||
5, 1, 2,
|
||||
2, 0, 3,
|
||||
6, 8, 7,
|
||||
9, 8, 10,
|
||||
7, 8, 11,
|
||||
11, 8, 9,
|
||||
]);
|
||||
|
||||
const submesh: Submesh = { start: 0, length: 24 };
|
||||
|
||||
const mesh = new Mesh({ vertexBuffer, indexBuffer, submeshes: [submesh] });
|
||||
|
||||
const imageBitmap = await loadImageBitmap("/uvmap.png");
|
||||
|
||||
const texture = renderer.createTexture({
|
||||
format: "srgb",
|
||||
width: imageBitmap.width,
|
||||
height: imageBitmap.height,
|
||||
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
});
|
||||
|
||||
renderer._device.queue.copyExternalImageToTexture(
|
||||
{ source: imageBitmap, flipY: false },
|
||||
{ texture: texture._texture },
|
||||
{ width: imageBitmap.width, height: imageBitmap.height },
|
||||
);
|
||||
|
||||
const material = renderer.createMaterial({
|
||||
baseColor: Color.white(),
|
||||
baseColorPartialCoverageTexture: texture,
|
||||
roughness: 0.5,
|
||||
metallic: 0,
|
||||
});
|
||||
|
||||
const node = new Node({ mesh, materials: [material] });
|
||||
|
||||
const scene = new Scene({
|
||||
nodes: [
|
||||
node,
|
||||
new Node({
|
||||
translation: new Vector3(0, 1, -1),
|
||||
light: new PointLight({ color: new Color(1, 1, 1) }),
|
||||
}),
|
||||
new Node({
|
||||
translation: new Vector3(0, -1, -1),
|
||||
light: new PointLight({ color: new Color(1, 1, 1) }),
|
||||
}),
|
||||
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();
|
||||
|
||||
async function loadImageBitmap(url: string) {
|
||||
const res = await fetch(url);
|
||||
const blob = await res.blob();
|
||||
const imageBitmap = await createImageBitmap(blob, { colorSpaceConversion: "none" });
|
||||
|
||||
return imageBitmap;
|
||||
}
|
||||
|
||||
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
10
example/style.css
Normal file
@@ -0,0 +1,10 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body, canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
BIN
example/uvmap.png
(Stored with Git LFS)
Normal file
BIN
example/uvmap.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
oktaeder.webm
(Stored with Git LFS)
Normal file
BIN
oktaeder.webm
(Stored with Git LFS)
Normal file
Binary file not shown.
@@ -18,14 +18,16 @@
|
||||
"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": {
|
||||
"tslib": "^2.6.1"
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@webgpu/types": "^0.1.34",
|
||||
"typescript": "5.1.6"
|
||||
"@webgpu/types": "^0.1.40",
|
||||
"esbuild": "^0.20.2",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
|
||||
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -11,7 +11,7 @@ export type Camera = OrthographicCamera | PerspectiveCamera;
|
||||
export interface OrthographicCameraProps {
|
||||
readonly name?: string;
|
||||
|
||||
readonly verticalSize: number;
|
||||
readonly halfVerticalSize: number;
|
||||
readonly nearPlane: number;
|
||||
readonly farPlane: number;
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export interface PerspectiveCameraProps {
|
||||
|
||||
export class OrthographicCamera {
|
||||
|
||||
readonly type!: "OrthographicCamera";
|
||||
declare readonly type: "OrthographicCamera";
|
||||
|
||||
_name: string;
|
||||
|
||||
@@ -39,13 +39,13 @@ export class OrthographicCamera {
|
||||
|
||||
constructor({
|
||||
name = "",
|
||||
verticalSize,
|
||||
halfVerticalSize,
|
||||
nearPlane,
|
||||
farPlane,
|
||||
}: OrthographicCameraProps) {
|
||||
this._name = name;
|
||||
|
||||
this._halfVerticalSize = verticalSize;
|
||||
this._halfVerticalSize = halfVerticalSize;
|
||||
this._nearPlane = nearPlane;
|
||||
this._farPlane = farPlane;
|
||||
|
||||
@@ -101,7 +101,7 @@ export class OrthographicCamera {
|
||||
|
||||
export class PerspectiveCamera {
|
||||
|
||||
readonly type!: "PerspectiveCamera";
|
||||
declare readonly type: "PerspectiveCamera";
|
||||
|
||||
_name: string;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,35 +6,11 @@
|
||||
|
||||
import { Color, ColorObject } from ".";
|
||||
import { Texture2D } from "../resources";
|
||||
import { MaterialProps } from "./MaterialProps";
|
||||
|
||||
export interface MaterialProps {
|
||||
name?: string;
|
||||
export class DynamicMaterial {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export class Material {
|
||||
|
||||
readonly type!: "Material";
|
||||
declare readonly type: "DynamicMaterial";
|
||||
|
||||
_name: string;
|
||||
|
||||
@@ -107,7 +83,7 @@ export class Material {
|
||||
set name(value: string) { this._name = value; }
|
||||
get name(): string { return this._name; }
|
||||
|
||||
setBaseColor(value: ColorObject): Material {
|
||||
setBaseColor(value: ColorObject): DynamicMaterial {
|
||||
this._baseColor.setObject(value);
|
||||
return this;
|
||||
}
|
||||
@@ -130,7 +106,7 @@ export class Material {
|
||||
set normalScale(value: number) { this._normalScale = value; }
|
||||
get normalScale(): number { return this._normalScale; }
|
||||
|
||||
setEmissive(value: ColorObject): Material {
|
||||
setEmissive(value: ColorObject): DynamicMaterial {
|
||||
this._emissive.setObject(value);
|
||||
return this;
|
||||
}
|
||||
@@ -138,7 +114,7 @@ export class Material {
|
||||
return res.setObject(this._emissive);
|
||||
}
|
||||
|
||||
setTransmission(value: ColorObject): Material {
|
||||
setTransmission(value: ColorObject): DynamicMaterial {
|
||||
this._transmission.setObject(value);
|
||||
return this;
|
||||
}
|
||||
@@ -152,22 +128,22 @@ export class Material {
|
||||
set ior(value: number) { this._ior = value; }
|
||||
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; }
|
||||
|
||||
set occlusionTexture(value: Texture2D | null) { this._occlusionTexture = value;}
|
||||
set occlusionTexture(value: Texture2D | null) { this._occlusionTexture = value; }
|
||||
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; }
|
||||
|
||||
set normalTexture(value: Texture2D | null) { this._normalTexture = value;}
|
||||
set normalTexture(value: Texture2D | null) { this._normalTexture = value; }
|
||||
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; }
|
||||
|
||||
set transmissionCollimationTexture(value: Texture2D | null) { this._transmissionCollimationTexture = value;}
|
||||
set transmissionCollimationTexture(value: Texture2D | null) { this._transmissionCollimationTexture = value; }
|
||||
get transmissionCollimationTexture(): Texture2D | null { return this._transmissionCollimationTexture; }
|
||||
|
||||
set transparent(value: boolean) { this._transparent = value; }
|
||||
@@ -177,8 +153,8 @@ export class Material {
|
||||
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 {
|
||||
return Boolean(value) && (value as Material).type === "Material";
|
||||
export function isDynamicMaterial(value: unknown): value is DynamicMaterial {
|
||||
return Boolean(value) && (value as DynamicMaterial).type === "DynamicMaterial";
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export interface PointLightProps {
|
||||
|
||||
export class DirectionalLight {
|
||||
|
||||
readonly type!: "DirectionalLight";
|
||||
declare readonly type: "DirectionalLight";
|
||||
|
||||
_name: string;
|
||||
|
||||
@@ -81,7 +81,7 @@ export class DirectionalLight {
|
||||
|
||||
export class PointLight {
|
||||
|
||||
readonly type!: "PointLight";
|
||||
declare readonly type: "PointLight";
|
||||
|
||||
_name: string;
|
||||
|
||||
|
||||
33
src/data/MaterialProps.ts
Normal file
33
src/data/MaterialProps.ts
Normal 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;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export type Matrix4x4Tuple = readonly [
|
||||
|
||||
export class Matrix4x4 {
|
||||
|
||||
readonly type!: "Matrix4x4";
|
||||
declare readonly type: "Matrix4x4";
|
||||
|
||||
ix: number;
|
||||
iy: number;
|
||||
|
||||
@@ -45,6 +45,27 @@ export class Mesh {
|
||||
get submeshCount(): number {
|
||||
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" });
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* 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 {
|
||||
readonly name?: string;
|
||||
@@ -16,14 +17,14 @@ export interface NodeProps {
|
||||
readonly camera?: Camera | null;
|
||||
readonly light?: Light | null;
|
||||
readonly mesh?: Mesh | null;
|
||||
readonly materials?: Material[];
|
||||
readonly materials?: (Material | DynamicMaterial)[];
|
||||
|
||||
readonly children?: Node[];
|
||||
}
|
||||
|
||||
export class Node {
|
||||
|
||||
readonly type!: "Node";
|
||||
declare readonly type: "Node";
|
||||
|
||||
_name: string;
|
||||
|
||||
@@ -38,7 +39,7 @@ export class Node {
|
||||
/** shared */
|
||||
_mesh: Mesh | null;
|
||||
/** shared */
|
||||
_materials: Material[];
|
||||
_materials: (Material | DynamicMaterial)[];
|
||||
|
||||
/** unique */
|
||||
_children: Node[];
|
||||
@@ -218,13 +219,13 @@ export class Node {
|
||||
set mesh(value: Mesh | null) { this._mesh = value; }
|
||||
get mesh(): Mesh | null { return this._mesh; }
|
||||
|
||||
setMaterials(value: readonly Material[]): Node {
|
||||
setMaterials(value: readonly (Material | DynamicMaterial)[]): Node {
|
||||
this._materials.length = 0;
|
||||
this._materials.push(...value);
|
||||
return this;
|
||||
}
|
||||
|
||||
getMaterials(res: Material[]): Material[] {
|
||||
getMaterials(res: (Material | DynamicMaterial)[]): (Material | DynamicMaterial)[] {
|
||||
res.length = 0;
|
||||
res.push(...this._materials);
|
||||
return res;
|
||||
|
||||
@@ -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;
|
||||
@@ -41,6 +41,27 @@ export class Quaternion {
|
||||
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 {
|
||||
this.x = object.x;
|
||||
this.y = object.y;
|
||||
@@ -64,6 +85,33 @@ export class Quaternion {
|
||||
this.w = 1;
|
||||
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" });
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface SceneProps {
|
||||
|
||||
export class Scene {
|
||||
|
||||
readonly type!: "Scene";
|
||||
declare readonly type: "Scene";
|
||||
|
||||
_name: string;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
|
||||
export * from "./Camera";
|
||||
export * from "./Color";
|
||||
export * from "./DynamicMaterial";
|
||||
export * from "./Light";
|
||||
export * from "./Material";
|
||||
export * from "./MaterialProps";
|
||||
export * from "./Matrix4x4";
|
||||
export * from "./Mesh";
|
||||
export * from "./Node";
|
||||
|
||||
13
src/geometry.ts
Normal file
13
src/geometry.ts
Normal 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;
|
||||
}
|
||||
515
src/gltf.ts
Normal file
515
src/gltf.ts
Normal file
@@ -0,0 +1,515 @@
|
||||
/*!
|
||||
* 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";
|
||||
import * as resources from "./resources";
|
||||
|
||||
/* 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 resources.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: resources.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 ----------------------------------------------------------
|
||||
|
||||
void(stopOnFirstError);
|
||||
void(treatWarningsAsErrors);
|
||||
void(rest);
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -5,13 +5,14 @@
|
||||
*/
|
||||
|
||||
export * from "./_BinaryWriter";
|
||||
export * from "./geometry";
|
||||
export * from "./shader";
|
||||
|
||||
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 { Camera, DynamicMaterial, MaterialProps, Matrix4x4, Node, Scene, Vector3, isDirectionalLight, isDynamicMaterial, isPointLight, preOrder } from "./data";
|
||||
import { IndexBuffer, IndexBufferProps, Material, Texture2D, Texture2DProps, VertexBuffer, VertexBufferProps, isMaterial } from "./resources";
|
||||
import { GLOBAL_UNIFORMS_SIZE, MATERIAL_UNIFORMS_SIZE, OBJECT_UNIFORMS_SIZE, ShaderFlagKey, ShaderFlags, _createPipeline, _shaderFlagsKey } from "./shader";
|
||||
|
||||
const _matrixOStoWSNormal = new Matrix4x4(
|
||||
NaN, NaN, NaN, NaN,
|
||||
@@ -48,7 +49,7 @@ export class Renderer {
|
||||
_textureWhite: Texture2D;
|
||||
/** 1×1 rgba8unorm texture of [0, 0, 0, 255] */
|
||||
_textureBlack: Texture2D;
|
||||
/** 1×1 rgba8unorm texture of [128, 128, 128, 255] */
|
||||
/** 1×1 rgba8unorm texture of [128, 128, 255, 255] */
|
||||
_textureNormal: Texture2D;
|
||||
|
||||
_depthBuffer: Texture2D;
|
||||
@@ -109,7 +110,7 @@ export class Renderer {
|
||||
height: 1,
|
||||
format: "linear",
|
||||
});
|
||||
this._textureNormal.writeFull(new Uint8Array([128, 128, 128, 255]));
|
||||
this._textureNormal.writeFull(new Uint8Array([128, 128, 255, 255]));
|
||||
|
||||
const framebufferTexture = this._context.getCurrentTexture();
|
||||
this._depthBuffer = new Texture2D(this, {
|
||||
@@ -117,6 +118,7 @@ export class Renderer {
|
||||
width: framebufferTexture.width,
|
||||
height: framebufferTexture.height,
|
||||
format: "depth",
|
||||
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
});
|
||||
|
||||
this._globalBindGroupLayout = device.createBindGroupLayout({
|
||||
@@ -266,7 +268,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 +277,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",
|
||||
});
|
||||
@@ -327,6 +329,10 @@ export class Renderer {
|
||||
return new IndexBuffer(this, props);
|
||||
}
|
||||
|
||||
createMaterial(props: MaterialProps): Material {
|
||||
return new Material(this, props);
|
||||
}
|
||||
|
||||
createTexture(props: Texture2DProps): Texture2D {
|
||||
return new Texture2D(this, props);
|
||||
}
|
||||
@@ -336,14 +342,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;
|
||||
}
|
||||
@@ -380,16 +386,18 @@ export class Renderer {
|
||||
|
||||
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 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;
|
||||
this._uniformWriter.writeColorF32(material._baseColor);
|
||||
this._uniformWriter.writeF32(material._partialCoverage);
|
||||
@@ -401,11 +409,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 +442,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 +501,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 +528,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]);
|
||||
@@ -532,9 +547,17 @@ export class Renderer {
|
||||
for (let si = 0; si < mesh._submeshes.length; ++si) {
|
||||
const submesh = mesh._submeshes[si]!;
|
||||
const material = object._materials[si]!;
|
||||
const { bindGroup: materialBindGroup, offset: materialOffset } = materialBindGroups[materialMapping.get(material)!]!;
|
||||
|
||||
pass.setBindGroup(1, materialBindGroup, [materialOffset]);
|
||||
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.drawIndexed(submesh.length, 1, submesh.start, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface IndexBufferResizeProps {
|
||||
|
||||
export class IndexBuffer {
|
||||
|
||||
readonly type!: "IndexBuffer";
|
||||
declare readonly type: "IndexBuffer";
|
||||
_renderer: Renderer;
|
||||
|
||||
_name: string;
|
||||
|
||||
169
src/resources/Material.ts
Normal file
169
src/resources/Material.ts
Normal 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";
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -51,7 +51,7 @@ export interface VertexBufferWriteTypedArrayProps {
|
||||
|
||||
export class VertexBuffer {
|
||||
|
||||
readonly type!: "VertexBuffer";
|
||||
declare readonly type: "VertexBuffer";
|
||||
_renderer: Renderer;
|
||||
|
||||
_name: string;
|
||||
|
||||
152
src/shader.ts
152
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,20 +36,20 @@ 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,
|
||||
hints: {
|
||||
"vert": { layout: renderer._pipelineLayout },
|
||||
"frag": { layout: renderer._pipelineLayout },
|
||||
},
|
||||
compilationHints: [
|
||||
{ entryPoint: "vert", layout: renderer._pipelineLayout },
|
||||
{ entryPoint: "frag", layout: renderer._pipelineLayout },
|
||||
],
|
||||
});
|
||||
|
||||
let vertexLocation = 0;
|
||||
@@ -130,14 +137,12 @@ export function createPipeline(renderer: Renderer, {
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
export function createShaderCode({
|
||||
export function _createShaderCode({
|
||||
texCoord,
|
||||
lightTexCoord,
|
||||
normal,
|
||||
tangent,
|
||||
}: ShaderFlags): string {
|
||||
let varyingLocation = 0;
|
||||
|
||||
return `
|
||||
struct Vertex {
|
||||
@location(0) positionOS: vec3<f32>,
|
||||
@@ -149,12 +154,12 @@ struct Vertex {
|
||||
|
||||
struct Varyings {
|
||||
@builtin(position) positionCS: vec4<f32>,
|
||||
@location(${varyingLocation++}) positionVS: vec4<f32>,
|
||||
${texCoord ? `@location(${varyingLocation++}) texCoord: vec2<f32>,` : ""}
|
||||
${lightTexCoord ? `@location(${varyingLocation++}) lightTexCoord: vec2<f32>,` : ""}
|
||||
${normal ? `@location(${varyingLocation++}) normalVS: vec3<f32>,` : ""}
|
||||
${normal && tangent ? `@location(${varyingLocation++}) tangentVS: vec3<f32>,` : ""}
|
||||
${normal && tangent ? `@location(${varyingLocation++}) bitangentVS: vec3<f32>,` : ""}
|
||||
@location(0) positionVS: vec3<f32>,
|
||||
${texCoord ? `@location(1) texCoord: vec2<f32>,` : ""}
|
||||
${lightTexCoord ? `@location(2) lightTexCoord: vec2<f32>,` : ""}
|
||||
${normal ? `@location(3) normalVS: vec3<f32>,` : ""}
|
||||
${normal && tangent ? `@location(4) tangentVS: vec3<f32>,` : ""}
|
||||
${normal && tangent ? `@location(5) bitangentVS: vec3<f32>,` : ""}
|
||||
}
|
||||
|
||||
struct PointLight {
|
||||
@@ -208,6 +213,57 @@ struct ObjectUniforms {
|
||||
@group(1) @binding(6) var _EmissiveTexture: 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> {
|
||||
let q0 = dpdx(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 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);
|
||||
}
|
||||
@@ -248,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<f32> {
|
||||
fn frag(fragment: Varyings) -> @location(0) vec4<f32> {
|
||||
var baseColor = _Material.baseColor;
|
||||
var partialCoverage = _Material.partialCoverage;
|
||||
var occlusion = 1.0;
|
||||
@@ -261,17 +318,17 @@ fn frag(fragment: Varyings) -> @location(0) vec2<f32> {
|
||||
var emissive = _Material.emissive;
|
||||
var ior = _Material.ior;
|
||||
${texCoord ? `
|
||||
let baseColorPartialCoverageTexel = texture(_BaseColorPartialCoverageTexture, _Sampler, fragment.texCoord);
|
||||
let baseColorPartialCoverageTexel = textureSample(_BaseColorPartialCoverageTexture, _Sampler, fragment.texCoord);
|
||||
baseColor *= baseColorPartialCoverageTexel.rgb;
|
||||
partialCoverage *= baseColorPartialCoverageTexel.a;
|
||||
let roughnessMetallicTexel = texture(_RoughnessMetallicTexture, _Sampler, fragment.texCoord);
|
||||
let roughnessMetallicTexel = textureSample(_RoughnessMetallicTexture, _Sampler, fragment.texCoord);
|
||||
roughness *= roughnessMetallicTexel.g;
|
||||
metallic *= roughnessMetallicTexel.b;
|
||||
let emissiveTexel = texture(_EmissiveTexture, _Sampler, fragment.texCoord);
|
||||
let emissiveTexel = textureSample(_EmissiveTexture, _Sampler, fragment.texCoord);
|
||||
emissive *= emissiveTexel.rgb;
|
||||
` : ""}
|
||||
${lightTexCoord ? `
|
||||
let occlusionTexel = texture(_OcclusionTexture, _Sampler, fragment.lightTexCoord);
|
||||
let occlusionTexel = textureSample(_OcclusionTexture, _Sampler, fragment.lightTexCoord);
|
||||
occlusion += _Material.occlusionTextureStrength * (occlusionTexel.r - 1.0);
|
||||
` : ""}
|
||||
|
||||
@@ -291,12 +348,57 @@ fn frag(fragment: Varyings) -> @location(0) vec2<f32> {
|
||||
` : `
|
||||
let matrixTStoVS = screenSpaceMatrixTStoVS(positionVS, geometricNormalVS, fragment.texCoord);
|
||||
`}
|
||||
let normalTextureTexel = texture(_NormalTexture, _Sampler, fragment.texCoord);
|
||||
let normalTextureTexel = textureSample(_NormalTexture, _Sampler, fragment.texCoord);
|
||||
var normalTS = normalTextureTexel.xyz * 2.0 - 1.0;
|
||||
normalTS.xy *= _Material.normalScale;
|
||||
let actualNormalVS = normalize(matrixTStoVS * geometricNormalVS);
|
||||
normalTS = vec3(normalTS.xy * _Material.normalScale, normalTS.z);
|
||||
let actualNormalVS = normalize(matrixTStoVS * normalTS);
|
||||
` : `
|
||||
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);
|
||||
}`;
|
||||
}
|
||||
|
||||
@@ -53,4 +53,5 @@
|
||||
|
||||
"skipLibCheck": false,
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user