Compare commits
	
		
			10 Commits
		
	
	
		
			a7219eae86
			...
			6e3d68e984
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6e3d68e984 | |||
| da9361df15 | |||
| 38708060d8 | |||
| 1355c4e342 | |||
| ef7654024a | |||
| 7a509b654c | |||
| e26be8ee09 | |||
| 5002138070 | |||
| dcfd486dea | |||
| e70fcbc582 | 
| @@ -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). | ||||||
|   | |||||||
							
								
								
									
										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> | ||||||
							
								
								
									
										104
									
								
								example/script.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								example/script.ts
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										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; | ||||||
|  | } | ||||||
| @@ -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": { | ||||||
|   | |||||||
							
								
								
									
										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 { | 	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); | ||||||
|   | |||||||
| @@ -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; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -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"; | ||||||
| } | } | ||||||
| @@ -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
									
								
							
							
						
						
									
										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 { | export class Matrix4x4 { | ||||||
|  |  | ||||||
| 	readonly type!: "Matrix4x4"; | 	declare readonly type: "Matrix4x4"; | ||||||
|  |  | ||||||
| 	ix: number; | 	ix: number; | ||||||
| 	iy: number; | 	iy: number; | ||||||
|   | |||||||
| @@ -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" }); | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -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" }); | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ export interface SceneProps { | |||||||
|  |  | ||||||
| export class Scene { | export class Scene { | ||||||
|  |  | ||||||
| 	readonly type!: "Scene"; | 	declare readonly type: "Scene"; | ||||||
|  |  | ||||||
| 	_name: string; | 	_name: string; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -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
									
								
							
							
						
						
									
										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; | ||||||
|  | } | ||||||
							
								
								
									
										510
									
								
								src/gltf.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										510
									
								
								src/gltf.ts
									
									
									
									
									
										Normal 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; | ||||||
|  | } | ||||||
| @@ -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); | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -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
									
								
							
							
						
						
									
										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 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 | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
							
								
								
									
										130
									
								
								src/shader.ts
									
									
									
									
									
								
							
							
						
						
									
										130
									
								
								src/shader.ts
									
									
									
									
									
								
							| @@ -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); | ||||||
| }`; | }`; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -53,4 +53,5 @@ | |||||||
|  |  | ||||||
| 		"skipLibCheck": false, | 		"skipLibCheck": false, | ||||||
| 	}, | 	}, | ||||||
|  | 	"include": ["./src/**/*"], | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user