diff --git a/package.json b/package.json index 7a4c5c6..e8837d8 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,14 @@ ".": { "types": "./dist/oktaeder.d.ts", "import": "./dist/oktaeder.js" + }, + "./data": { + "types": "./dist/data/index.d.ts", + "import": "./dist/data/index.js" + }, + "./resources": { + "types": "./dist/resources/index.d.ts", + "import": "./dist/resources/index.js" } } } diff --git a/src/IndexBuffer.ts b/src/IndexBuffer.ts deleted file mode 100644 index 7bd9ff7..0000000 --- a/src/IndexBuffer.ts +++ /dev/null @@ -1,52 +0,0 @@ -/*! - * 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 { Renderer } from "./Renderer"; - -export const INDEX_SIZE = 2; - -export class IndexBuffer { - - readonly type!: "IndexBuffer"; - _renderer: Renderer; - - _buffer: GPUBuffer; - - constructor(renderer: Renderer, indexCount: number) { - Object.defineProperty(this, "type", { value: "IndexBuffer" }); - - this._renderer = renderer; - - this._buffer = renderer._device.createBuffer({ - usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.INDEX, - size: indexCount * INDEX_SIZE, - }); - } - - dispose(): IndexBuffer { - this._buffer.destroy(); - return this; - } - - get vertexCount(): number { - return this._buffer.size / INDEX_SIZE | 0; - } - - writeArray(offset: number, indices: readonly number[]): IndexBuffer { - const array = new Uint16Array(indices); - this._renderer._device.queue.writeBuffer(this._buffer, offset * INDEX_SIZE | 0, array); - return this; - } - - writeTypedArray(offset: number, indices: Uint16Array): IndexBuffer { - this._renderer._device.queue.writeBuffer(this._buffer, offset * INDEX_SIZE | 0, indices); - return this; - } -} - -export function isIndexBuffer(value: unknown): value is IndexBuffer { - return Boolean(value) && (value as IndexBuffer).type === "IndexBuffer"; -} diff --git a/src/Renderer.ts b/src/Renderer.ts deleted file mode 100644 index 2193e78..0000000 --- a/src/Renderer.ts +++ /dev/null @@ -1,91 +0,0 @@ -/*! - * 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 { IndexBuffer } from "./IndexBuffer"; -import { Material, MaterialProps } from "./Material"; -import { Texture2D, Texture2DProps } from "./Texture2D"; -import { VertexBuffer } from "./VertexBuffer"; - -export class Renderer { - - _adapter: GPUAdapter; - _device: GPUDevice; - _context: GPUCanvasContext; - _format: GPUTextureFormat; - - _textureWhite: Texture2D; // 1×1 rgba8unorm of [255, 255, 255, 255] - _textureBlack: Texture2D; // 1×1 rgba8unorm of [0, 0, 0, 255] - _textureNormal: Texture2D; // 1×1 rgba8unorm of [128, 128, 128, 255] - - private constructor ( - adapter: GPUAdapter, - device: GPUDevice, - context: GPUCanvasContext, - format: GPUTextureFormat, - ) { - this._adapter = adapter; - this._device = device; - this._context = context; - this._format = format; - - this._textureWhite = new Texture2D(this, { - width: 1, - height: 1, - }); - this._textureWhite.writeTypedArray(new Uint8Array([255, 255, 255, 255])); - - this._textureBlack = new Texture2D(this, { - width: 1, - height: 1, - }); - this._textureBlack.writeTypedArray(new Uint8Array([0, 0, 0, 255])); - - this._textureNormal = new Texture2D(this, { - width: 1, - height: 1, - }); - this._textureNormal.writeTypedArray(new Uint8Array([128, 128, 128, 255])); - } - - static async init(canvas: HTMLCanvasElement) { - if (!navigator.gpu) { - throw new Error("WebGPU is not supported"); - } - - const adapter = await navigator.gpu.requestAdapter({ - powerPreference: "high-performance", - }); - if (adapter === null) { - throw new Error("GPUAdapter is not available"); - } - - const device = await adapter.requestDevice(); - - const context = canvas.getContext("webgpu"); - if (context === null) { - throw new Error("GPUCanvasContext is not available"); - } - - const format = navigator.gpu.getPreferredCanvasFormat(); - context.configure({ device, format }); - } - - createTexture(props: Texture2DProps): Texture2D { - return new Texture2D(this, props); - } - - createIndexBuffer(indexCount: number): IndexBuffer { - return new IndexBuffer(this, indexCount); - } - - createMaterial(props: MaterialProps): Material { - return new Material(this, props); - } - - createVertexBuffer(vertexCount: number): VertexBuffer { - return new VertexBuffer(this, vertexCount); - } -} diff --git a/src/Texture2D.ts b/src/Texture2D.ts deleted file mode 100644 index 33a9ff0..0000000 --- a/src/Texture2D.ts +++ /dev/null @@ -1,78 +0,0 @@ -/*! - * 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 { Renderer } from "./Renderer"; - -export interface Texture2DProps { - name?: string; - - width: number; - height: number; - - sRGB?: boolean; -} - -export class Texture2D { - - readonly type!: "Texture2D"; - _renderer: Renderer; - - _name: string; - - _texture: GPUTexture; - _textureView: GPUTextureView; - - constructor(renderer: Renderer, { - name = "", - width, - height, - sRGB = false, - }: Texture2DProps) { - Object.defineProperty(this, "type", { value: "Texture2D" }); - - this._renderer = renderer; - - this._name = name; - - this._renderer = renderer; - this._texture = renderer._device.createTexture({ - usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST, - size: { width, height }, - format: sRGB ? "rgba8unorm-srgb" : "rgba8unorm", - }); - this._textureView = this._texture.createView({ - format: sRGB ? "rgba8unorm-srgb" : "rgba8unorm", - dimension: "2d", - }); - } - - dispose(): Texture2D { - this._texture.destroy(); - return this; - } - - get width(): number { - return this._texture.width; - } - - get height(): number { - return this._texture.height; - } - - writeTypedArray(data: Uint8Array): Texture2D { - this._renderer._device.queue.writeTexture( - { texture: this._texture }, - data, - { bytesPerRow: 4 * this._texture.width }, - { width: this._texture.width, height: this._texture.height }, - ); - return this; - } -} - -export function isTexture2D(value: unknown): value is Texture2D { - return Boolean(value) && (value as Texture2D).type === "Texture2D"; -} diff --git a/src/VertexBuffer.ts b/src/VertexBuffer.ts deleted file mode 100644 index 09797ee..0000000 --- a/src/VertexBuffer.ts +++ /dev/null @@ -1,59 +0,0 @@ -/*! - * 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 { Renderer } from "./Renderer"; -import { Vector3Object } from "./Vector3"; - -export const VERTEX_SIZE = 12; - -export class VertexBuffer { - - readonly type!: "VertexBuffer"; - _renderer: Renderer; - - _buffer: GPUBuffer; - - constructor(renderer: Renderer, vertexCount: number) { - Object.defineProperty(this, "type", { value: "VertexBuffer" }); - - this._renderer = renderer; - - this._buffer = renderer._device.createBuffer({ - usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, - size: vertexCount * VERTEX_SIZE, - }); - } - - dispose(): VertexBuffer { - this._buffer.destroy(); - return this; - } - - get vertexCount(): number { - return this._buffer.size / VERTEX_SIZE | 0; - } - - writeArray(offset: number, vertices: readonly Vector3Object[]): VertexBuffer { - const array = new Float32Array(vertices.length * 3); - for (let vi = 0, ptr = 0; vi < vertices.length; ++vi) { - const vertex = vertices[vi]!; - array[ptr++] = vertex.x; - array[ptr++] = vertex.y; - array[ptr++] = vertex.z; - } - this._renderer._device.queue.writeBuffer(this._buffer, offset * VERTEX_SIZE | 0, array); - return this; - } - - writeTypedArray(offset: number, vertices: Float32Array): VertexBuffer { - this._renderer._device.queue.writeBuffer(this._buffer, offset * VERTEX_SIZE | 0, vertices); - return this; - } -} - -export function isVertexBuffer(value: unknown): value is VertexBuffer { - return Boolean(value) && (value as VertexBuffer).type === "VertexBuffer"; -} diff --git a/src/Camera.ts b/src/data/Camera.ts similarity index 90% rename from src/Camera.ts rename to src/data/Camera.ts index 8ffc9a1..a033b61 100644 --- a/src/Camera.ts +++ b/src/data/Camera.ts @@ -4,7 +4,7 @@ * obtain one at http://mozilla.org/MPL/2.0/. */ -import { Node } from "./Node"; +import { Node } from "."; export type Camera = CameraOrthographic | CameraPerspective; @@ -43,8 +43,6 @@ export class CameraOrthographic { nearPlane, farPlane, }: CameraOrthographicProps) { - Object.defineProperty(this, "type", { value: "CameraOrthographic" }); - this._name = name; this._verticalSize = verticalSize; @@ -65,6 +63,8 @@ export class CameraOrthographic { } } +Object.defineProperty(CameraOrthographic.prototype, "type", { value: "CameraOrthographic" }); + export class CameraPerspective { readonly type!: "CameraPerspective"; @@ -84,8 +84,6 @@ export class CameraPerspective { nearPlane, farPlane, }: CameraPerspectiveProps) { - Object.defineProperty(this, "type", { value: "CameraPerspective" }); - this._name = name; this._verticalFovRad = verticalFovRad; @@ -106,6 +104,8 @@ export class CameraPerspective { } } +Object.defineProperty(CameraPerspective.prototype, "type", { value: "CameraPerspective" }); + export function isCameraOrthographic(value: unknown): value is CameraOrthographic { return Boolean(value) && (value as CameraOrthographic).type === "CameraOrthographic"; } diff --git a/src/Color.ts b/src/data/Color.ts similarity index 98% rename from src/Color.ts rename to src/data/Color.ts index b8c485d..b91d524 100644 --- a/src/Color.ts +++ b/src/data/Color.ts @@ -4,7 +4,7 @@ * obtain one at http://mozilla.org/MPL/2.0/. */ -import { Vector3Object } from "./Vector3"; +import { Vector3Object } from "."; /* Named colors * black #000000 (0, 0, 0, 1) @@ -63,8 +63,6 @@ export class Color { b: number; constructor(r: number, g: number, b: number) { - Object.defineProperty(this, "type", { value: "Color" }); - this.r = r; this.g = g; this.b = b; @@ -261,6 +259,8 @@ export class Color { } } +Object.defineProperty(Color.prototype, "type", { value: "Color" }); + export function isColor(value: unknown): value is Color { return Boolean(value) && (value as Color).type === "Color"; } diff --git a/src/Matrix4x4.ts b/src/data/Matrix4x4.ts similarity index 97% rename from src/Matrix4x4.ts rename to src/data/Matrix4x4.ts index e9fc07c..be65b6d 100644 --- a/src/Matrix4x4.ts +++ b/src/data/Matrix4x4.ts @@ -4,8 +4,7 @@ * obtain one at http://mozilla.org/MPL/2.0/. */ -import { QuaternionObject } from "./Quaternion"; -import { Vector3Object } from "./Vector3"; +import { QuaternionObject, Vector3Object } from "."; export interface Matrix4x4Object { readonly ix: number; @@ -60,8 +59,6 @@ export class Matrix4x4 { kx: number, ky: number, kz: number, kw: number, tx: number, ty: number, tz: number, tw: number ) { - Object.defineProperty(this, "type", { value: "Matrix4x4" }); - this.ix = ix; this.iy = iy; this.iz = iz; @@ -241,6 +238,8 @@ export class Matrix4x4 { } } +Object.defineProperty(Matrix4x4.prototype, "type", { value: "Matrix4x4" }); + export function isMatrix4x4(value: unknown): value is Matrix4x4 { return Boolean(value) && (value as Matrix4x4).type === "Matrix4x4"; } diff --git a/src/Mesh.ts b/src/data/Mesh.ts similarity index 86% rename from src/Mesh.ts rename to src/data/Mesh.ts index 39ecaf4..4bdbf5d 100644 --- a/src/Mesh.ts +++ b/src/data/Mesh.ts @@ -4,8 +4,7 @@ * obtain one at http://mozilla.org/MPL/2.0/. */ -import { IndexBuffer } from "./IndexBuffer"; -import { VertexBuffer } from "./VertexBuffer"; +import { IndexBuffer, VertexBuffer } from "../resources"; export type Submesh = { start: number, @@ -36,8 +35,6 @@ export class Mesh { indexBuffer, submeshes, }: MeshProps) { - Object.defineProperty(this, "type", { value: "Mesh" }); - this._name = name; this._vertexBuffer = vertexBuffer; @@ -50,6 +47,8 @@ export class Mesh { } } +Object.defineProperty(Mesh.prototype, "type", { value: "Mesh" }); + export function isMesh(value: unknown): value is Mesh { return Boolean(value) && (value as Mesh).type === "Mesh"; } diff --git a/src/Node.ts b/src/data/Node.ts similarity index 86% rename from src/Node.ts rename to src/data/Node.ts index cf0db3c..7ef7413 100644 --- a/src/Node.ts +++ b/src/data/Node.ts @@ -4,11 +4,8 @@ * obtain one at http://mozilla.org/MPL/2.0/. */ -import { Camera } from "./Camera"; -import { Material } from "./Material"; -import { Mesh } from "./Mesh"; -import { Quaternion, QuaternionObject } from "./Quaternion"; -import { Vector3, Vector3Object } from "./Vector3"; +import { Camera, Mesh, Quaternion, QuaternionObject, Vector3, Vector3Object } from "."; +import { Material } from "../resources"; export interface NodeProps { readonly name?: string; @@ -57,8 +54,6 @@ export class Node { materials = [], children = [], }: NodeProps) { - Object.defineProperty(this, "type", { value: "Node" }); - this._name = name; this._translation = translation !== undefined ? Vector3.fromObject(translation) : Vector3.zero(); @@ -85,6 +80,8 @@ export class Node { } } +Object.defineProperty(Node.prototype, "type", { value: "Node" }); + export function isNode(value: unknown): value is Node { return Boolean(value) && (value as Node).type === "Node"; } diff --git a/src/Quaternion.ts b/src/data/Quaternion.ts similarity index 95% rename from src/Quaternion.ts rename to src/data/Quaternion.ts index 2030942..f35992c 100644 --- a/src/Quaternion.ts +++ b/src/data/Quaternion.ts @@ -23,8 +23,6 @@ export class Quaternion { w: number; constructor(x: number, y: number, z: number, w: number) { - Object.defineProperty(this, "type", { value: "Quaternion" }); - this.x = x; this.y = y; this.z = z; @@ -68,6 +66,8 @@ export class Quaternion { } } +Object.defineProperty(Quaternion.prototype, "type", { value: "Quaternion" }); + export function isQuaternion(value: unknown): value is Quaternion { return Boolean(value) && (value as Quaternion).type === "Quaternion"; } diff --git a/src/Scene.ts b/src/data/Scene.ts similarity index 86% rename from src/Scene.ts rename to src/data/Scene.ts index 6970b30..614fa21 100644 --- a/src/Scene.ts +++ b/src/data/Scene.ts @@ -4,7 +4,7 @@ * obtain one at http://mozilla.org/MPL/2.0/. */ -import { Node } from "./Node"; +import { Node } from "."; export interface SceneProps { readonly name?: string; @@ -24,14 +24,14 @@ export class Scene { name = "", nodes = [], }: SceneProps) { - Object.defineProperty(this, "type", { value: "Scene" }); - this._name = name; this._nodes = nodes; } } +Object.defineProperty(Scene.prototype, "type", { value: "Scene" }); + export function isScene(value: unknown): value is Scene { return Boolean(value) && (value as Scene).type === "Scene"; } diff --git a/src/data/Vector2.ts b/src/data/Vector2.ts new file mode 100644 index 0000000..33d7f75 --- /dev/null +++ b/src/data/Vector2.ts @@ -0,0 +1,87 @@ +/*! + * 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 interface Vector2Object { + readonly x: number; + readonly y: number; +} + +export type Vector2Tuple = readonly [x: number, y: number]; + +export class Vector2 { + + readonly type!: "Vector2"; + + x: number; + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + + static fromObject(object: Vector2Object): Vector2 { + return new Vector2(object.x, object.y); + } + + static fromTuple(tuple: Vector2Tuple): Vector2 { + return new Vector2(...tuple); + } + + static zero(): Vector2 { + return new Vector2(0, 0); + } + + static one(): Vector2 { + return new Vector2(1, 1); + } + + set(x: number, y: number): Vector2 { + this.x = x; + this.y = y; + return this; + } + + setObject(object: Vector2Object): Vector2 { + this.x = object.x; + this.y = object.y; + return this; + } + + setTuple(tuple: Vector2Tuple): Vector2 { + this.x = tuple[0]; + this.y = tuple[1]; + return this; + } + + setZero(): Vector2 { + this.x = 0; + this.y = 0; + return this; + } + + setOne(): Vector2 { + this.x = 1; + this.y = 1; + return this; + } + + setX(x: number): Vector2 { + this.x = x; + return this; + } + + setY(y: number): Vector2 { + this.y = y; + return this; + } +} + +Object.defineProperty(Vector2.prototype, "type", { value: "Vector2" }); + +export function isVector2(value: unknown): value is Vector2 { + return Boolean(value) && (value as Vector2).type === "Vector2"; +} diff --git a/src/Vector3.ts b/src/data/Vector3.ts similarity index 95% rename from src/Vector3.ts rename to src/data/Vector3.ts index 3db78fb..4cbb6dc 100644 --- a/src/Vector3.ts +++ b/src/data/Vector3.ts @@ -21,8 +21,6 @@ export class Vector3 { z: number; constructor(x: number, y: number, z: number) { - Object.defineProperty(this, "type", { value: "Vector3" }); - this.x = x; this.y = y; this.z = z; @@ -95,6 +93,8 @@ export class Vector3 { } } +Object.defineProperty(Vector3.prototype, "type", { value: "Vector3" }); + export function isVector3(value: unknown): value is Vector3 { return Boolean(value) && (value as Vector3).type === "Vector3"; } diff --git a/src/data/Vector4.ts b/src/data/Vector4.ts new file mode 100644 index 0000000..339b097 --- /dev/null +++ b/src/data/Vector4.ts @@ -0,0 +1,113 @@ +/*! + * 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 interface Vector4Object { + readonly x: number; + readonly y: number; + readonly z: number; + readonly w: number; +} + +export type Vector4Tuple = readonly [x: number, y: number, z: number, w: number]; + +export class Vector4 { + + readonly type!: "Vector4"; + + x: number; + y: number; + z: number; + w: number; + + constructor(x: number, y: number, z: number, w: number) { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + static fromObject(object: Vector4Object): Vector4 { + return new Vector4(object.x, object.y, object.z, object.w); + } + + static fromTuple(tuple: Vector4Tuple): Vector4 { + return new Vector4(...tuple); + } + + static zero(): Vector4 { + return new Vector4(0, 0, 0, 0); + } + + static one(): Vector4 { + return new Vector4(1, 1, 1, 1); + } + + set(x: number, y: number, z: number, w: number): Vector4 { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + return this; + } + + setObject(object: Vector4Object): Vector4 { + this.x = object.x; + this.y = object.y; + this.z = object.z; + this.w = object.w; + return this; + } + + setTuple(tuple: Vector4Tuple): Vector4 { + this.x = tuple[0]; + this.y = tuple[1]; + this.z = tuple[2]; + this.w = tuple[3]; + return this; + } + + setZero(): Vector4 { + this.x = 0; + this.y = 0; + this.z = 0; + this.w = 0; + return this; + } + + setOne(): Vector4 { + this.x = 1; + this.y = 1; + this.z = 1; + this.w = 1; + return this; + } + + setX(x: number): Vector4 { + this.x = x; + return this; + } + + setY(y: number): Vector4 { + this.y = y; + return this; + } + + setZ(z: number): Vector4 { + this.z = z; + return this; + } + + setW(w: number): Vector4 { + this.w = w; + return this; + } +} + +Object.defineProperty(Vector4.prototype, "type", { value: "Vector4" }); + +export function isVector4(value: unknown): value is Vector4 { + return Boolean(value) && (value as Vector4).type === "Vector4"; +} diff --git a/src/data/index.ts b/src/data/index.ts new file mode 100644 index 0000000..0f41f51 --- /dev/null +++ b/src/data/index.ts @@ -0,0 +1,16 @@ +/*! + * 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 * from "./Camera"; +export * from "./Color"; +export * from "./Matrix4x4"; +export * from "./Mesh"; +export * from "./Node"; +export * from "./Quaternion"; +export * from "./Scene"; +export * from "./Vector2"; +export * from "./Vector3"; +export * from "./Vector4"; diff --git a/src/oktaeder.ts b/src/oktaeder.ts index 2d3eaf1..d9d4ae6 100644 --- a/src/oktaeder.ts +++ b/src/oktaeder.ts @@ -4,16 +4,108 @@ * obtain one at http://mozilla.org/MPL/2.0/. */ -export * from "./Camera"; -export * from "./Color"; -export * from "./IndexBuffer"; -export * from "./Material"; -export * from "./Matrix4x4"; -export * from "./Mesh"; -export * from "./Node"; -export * from "./Quaternion"; -export * from "./Renderer"; -export * from "./Scene"; -export * from "./Texture2D"; -export * from "./Vector3"; -export * from "./VertexBuffer"; +import { IndexBuffer, IndexBufferProps, Material, MaterialProps, Texture2D, Texture2DProps, VertexBuffer, VertexBufferProps } from "./resources"; + +export class Renderer { + + _adapter: GPUAdapter; + _device: GPUDevice; + _context: GPUCanvasContext; + _format: GPUTextureFormat; + + /** 1×1 rgba8unorm texture of [255, 255, 255, 255] */ + _textureWhite: Texture2D; + /** 1×1 rgba8unorm texture of [0, 0, 0, 255] */ + _textureBlack: Texture2D; + /** 1×1 rgba8unorm texture of [128, 128, 128, 255] */ + _textureNormal: Texture2D; + + /** + * This constructor is intended primarily for internal use. Consider using + * `Renderer.createIndexBuffer` instead. + */ + private constructor ( + adapter: GPUAdapter, + device: GPUDevice, + context: GPUCanvasContext, + format: GPUTextureFormat, + ) { + this._adapter = adapter; + this._device = device; + this._context = context; + this._format = format; + + this._textureWhite = new Texture2D(this, { + width: 1, + height: 1, + format: "linear", + }); + this._textureWhite.writeFull(new Uint8Array([255, 255, 255, 255])); + + this._textureBlack = new Texture2D(this, { + width: 1, + height: 1, + format: "linear", + }); + this._textureBlack.writeFull(new Uint8Array([0, 0, 0, 255])); + + this._textureNormal = new Texture2D(this, { + width: 1, + height: 1, + format: "linear", + }); + this._textureNormal.writeFull(new Uint8Array([128, 128, 128, 255])); + } + + static async init(canvas: HTMLCanvasElement) { + if (!navigator.gpu) { + throw new Error("WebGPU is not supported"); + } + + const adapter = await navigator.gpu.requestAdapter({ + powerPreference: "high-performance", + }); + if (adapter === null) { + throw new Error("GPUAdapter is not available"); + } + + const device = await adapter.requestDevice(); + + const context = canvas.getContext("webgpu"); + if (context === null) { + throw new Error("GPUCanvasContext is not available"); + } + + const format = navigator.gpu.getPreferredCanvasFormat(); + context.configure({ device, format }); + } + + /** + * Disposes resources internal to the renderer. Doesn't dispose any objects + * created with this renderer. The renderer should not be used after calling + * this method. + * @returns `this` for chaining + */ + dispose(): Renderer { + this._textureWhite.dispose(); + this._textureBlack.dispose(); + this._textureNormal.dispose(); + return this; + } + + createIndexBuffer(props: IndexBufferProps): IndexBuffer { + return new IndexBuffer(this, props); + } + + createMaterial(props: MaterialProps): Material { + return new Material(this, props); + } + + createTexture(props: Texture2DProps): Texture2D { + return new Texture2D(this, props); + } + + createVertexBuffer(props: VertexBufferProps): VertexBuffer { + return new VertexBuffer(this, props); + } +} diff --git a/src/resources/IndexBuffer.ts b/src/resources/IndexBuffer.ts new file mode 100644 index 0000000..1e17c42 --- /dev/null +++ b/src/resources/IndexBuffer.ts @@ -0,0 +1,132 @@ +/*! + * 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 { Renderer } from "../oktaeder"; + +export interface IndexBufferProps { + readonly name?: string; + + readonly indexFormat: "uint16" | "uint32"; + readonly indexCount: number; +} + +export interface IndexBufferResizeProps { + readonly indexFormat?: "uint16" | "uint32"; + readonly indexCount?: number; +} + +export class IndexBuffer { + + readonly type!: "IndexBuffer"; + _renderer: Renderer; + + _name: string; + + _buffer: GPUBuffer; + _indexFormat: "uint16" | "uint32"; + + constructor(renderer: Renderer, { + name = "", + indexFormat, + indexCount, + }: IndexBufferProps) { + this._renderer = renderer; + + this._name = name; + + this._buffer = renderer._device.createBuffer({ + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.INDEX, + size: indexCount * indexSize(indexFormat), + label: name, + }); + this._indexFormat = indexFormat; + } + + /** + * Destroys owned GPU resources. The index buffer should not be used after + * calling this method. + * @returns `this` for chaining + */ + dispose(): IndexBuffer { + this._buffer.destroy(); + return this; + } + + get indexCount(): number { + return this._buffer.size / indexSize(this._indexFormat) | 0; + } + + writeArray(offset: number, indices: readonly number[]): IndexBuffer { + const array = this._indexFormat === "uint16" ? new Uint16Array(indices) : new Uint32Array(indices); + return this.writeTypedArray(offset, array); + } + + writeTypedArray(offset: number, indices: Uint16Array | Uint32Array): IndexBuffer { + if ( + this._indexFormat === "uint16" && !(indices instanceof Uint16Array) + || this._indexFormat === "uint32" && !(indices instanceof Uint32Array) + ) { + throw new Error(`Cannot write typed array to a mismatched index type. Typed array is of type ${indices.constructor.name}. Index buffer [${this._name}] uses format ${this._indexFormat}`); + } + this._renderer._device.queue.writeBuffer(this._buffer, offset * indexSize(this._indexFormat) | 0, indices); + return this; + } + + /** + * Resize the index buffer, discarding currently stored data. + * @param props Desired buffer properties. Any unspecified property will + * stay unchanged. + * @returns `this` for chaining + */ + resizeDiscard({ + indexFormat = this._indexFormat, + indexCount = this.indexCount, + }: IndexBufferResizeProps): IndexBuffer { + this._buffer.destroy(); + + this._buffer = this._renderer._device.createBuffer({ + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.INDEX, + size: indexCount * indexSize(indexFormat), + label: this._name, + }); + this._indexFormat = indexFormat; + return this; + } + + /** + * Resize the index buffer if it can't hold provided number of indices or + * its index format is smaller than provided, potentially discarding + * currently stored data. + * @param props Desired buffer properties. Any unspecified property will + * be ignored. + * @returns `this` for chaining + */ + ensureSizeDiscard({ + indexFormat = this._indexFormat, + indexCount = this.indexCount, + }): IndexBuffer { + if (this.indexCount >= indexCount && indexSize(this._indexFormat) >= indexSize(indexFormat)) { + return this; + } + return this.resizeDiscard({ + indexFormat, + indexCount, + }); + } +} + +Object.defineProperty(IndexBuffer.prototype, "type", { value: "IndexBuffer" }); + +export function isIndexBuffer(value: unknown): value is IndexBuffer { + return Boolean(value) && (value as IndexBuffer).type === "IndexBuffer"; +} + +function indexSize(indexFormat: "uint16" | "uint32"): number { + switch (indexFormat) { + case "uint16": return 2; + case "uint32": return 4; + } +} diff --git a/src/Material.ts b/src/resources/Material.ts similarity index 90% rename from src/Material.ts rename to src/resources/Material.ts index e419300..ef98c07 100644 --- a/src/Material.ts +++ b/src/resources/Material.ts @@ -4,9 +4,9 @@ * obtain one at http://mozilla.org/MPL/2.0/. */ -import { Color, ColorObject } from "./Color"; -import { Renderer } from "./Renderer"; -import { Texture2D } from "./Texture2D"; +import { Texture2D } from "."; +import { Color, ColorObject } from "../data"; +import { Renderer } from "../oktaeder"; export const UNIFORM_BUFFER_SIZE = 64; @@ -81,8 +81,6 @@ export class Material { transparent = false, doubleSided = false, }: MaterialProps) { - Object.defineProperty(this, "type", { value: "Material" }); - this._renderer = renderer; this._name = name; @@ -108,11 +106,18 @@ export class Material { this._doubleSided = doubleSided; } + /** + * Destroys owned GPU resources. The material should not be used after + * calling this method. + * @returns `this` for chaining + */ dispose(): Material { return this; } } +Object.defineProperty(Material.prototype, "type", { value: "Material" }); + export function isMaterial(value: unknown): value is Material { return Boolean(value) && (value as Material).type === "Material"; } diff --git a/src/resources/Texture2D.ts b/src/resources/Texture2D.ts new file mode 100644 index 0000000..373c24c --- /dev/null +++ b/src/resources/Texture2D.ts @@ -0,0 +1,160 @@ +/*! + * 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 { Vector2Object, Vector2Tuple } from "../data"; +import { Renderer } from "../oktaeder"; + +export type Texture2DFormat = + | "linear" + | "srgb" + | "hdr" + | "depth" + ; + +export interface Texture2DProps { + readonly name?: string; + + readonly width: number; + readonly height: number; + + readonly format: Texture2DFormat; +} + +export interface Texture2DAdvancedWriteProps { + readonly origin: Vector2Object | Vector2Tuple, + readonly data: BufferSource | SharedArrayBuffer, + readonly bytesPerRow: number, + readonly width: number, + readonly height: number, +} + +export class Texture2D { + + readonly type!: "Texture2D"; + _renderer: Renderer; + + _name: string; + + _texture: GPUTexture; + _textureView: GPUTextureView; + _format: Texture2DFormat; + + constructor(renderer: Renderer, { + name = "", + width, + height, + format, + }: Texture2DProps) { + this._renderer = renderer; + + this._name = name; + + const gpuFormat = gpuTextureFormat(format); + + this._renderer = renderer; + this._texture = renderer._device.createTexture({ + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST, + size: { width, height }, + format: gpuFormat, + label: name + }); + this._textureView = this._texture.createView({ + format: gpuFormat, + dimension: "2d", + label: `${name}.textureView`, + }); + this._format = format; + } + + dispose(): Texture2D { + this._texture.destroy(); + return this; + } + + get width(): number { + return this._texture.width; + } + + get height(): number { + return this._texture.height; + } + + get bytesPerSample(): number { + return sampleSize(this._format); + } + + get samplesPerPixel(): number { + return sampleCount(this._format); + } + + writeFull(data: BufferSource | SharedArrayBuffer): Texture2D { + const bytesPerSample = this.bytesPerSample; + const samplesPerPixel = this.samplesPerPixel; + const bytesPerRow = this.width * samplesPerPixel * bytesPerSample; + const byteLength = this.height * bytesPerRow; + + if (data.byteLength !== byteLength) { + throw new Error(`Cannot fully write to a texture with different byte length. Source data has byte length of ${data.byteLength}. Texture [${this._name}] is ${this.width}×${this.height} pixels in size, uses ${this._format} format at ${bytesPerSample} ${bytesPerSample === 1 ? "byte" : "bytes"} per sample and ${samplesPerPixel} ${samplesPerPixel === 1 ? "sample" : "samples"} per pixel, which makes its byte length equal to ${byteLength}.`); + } + + this._renderer._device.queue.writeTexture( + { texture: this._texture }, + data, + { bytesPerRow }, + { width: this.width, height: this.height }, + ); + return this; + } + + writePartial({ + origin, + data, + bytesPerRow, + width, + height, + }: Texture2DAdvancedWriteProps): Texture2D { + this._renderer._device.queue.writeTexture( + { texture: this._texture, origin }, + data, + { bytesPerRow }, + { width, height }, + ); + return this; + } +} + +Object.defineProperty(Texture2D.prototype, "type", { value: "Texture2D" }); + +export function isTexture2D(value: unknown): value is Texture2D { + return Boolean(value) && (value as Texture2D).type === "Texture2D"; +} + +function gpuTextureFormat(format: Texture2DFormat): GPUTextureFormat { + switch (format) { + case "linear": return "rgba8unorm"; + case "srgb": return "rgba8unorm-srgb"; + case "hdr": return "rgba16float"; + case "depth": return "depth32float"; + } +} + +function sampleCount(format: Texture2DFormat): number { + switch (format) { + case "linear": return 4; + case "srgb": return 4; + case "hdr": return 4; + case "depth": return 1; + } +} + +function sampleSize(format: Texture2DFormat): number { + switch (format) { + case "linear": return 1; + case "srgb": return 1; + case "hdr": return 2; + case "depth": return 4; + } +} diff --git a/src/resources/VertexBuffer.ts b/src/resources/VertexBuffer.ts new file mode 100644 index 0000000..1065911 --- /dev/null +++ b/src/resources/VertexBuffer.ts @@ -0,0 +1,386 @@ +/*! + * 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 { Vector2Object, Vector3Object, Vector4Object } from "../data"; +import { Renderer } from "../oktaeder"; + +export const POSITION_SIZE = 12; +export const TEX_COORD_SIZE = 8; +export const LIGHT_TEX_COORD_SIZE = 8; +export const NORMAL_SIZE = 12; +export const TANGENT_SIZE = 16; + +export interface VertexBufferProps { + readonly name?: string; + + readonly vertexCount: number; + + readonly texCoord?: boolean; + readonly lightTexCoord?: boolean; + readonly normal?: boolean; + readonly tangent?: boolean; +} + +export interface VertexBufferResizeProps { + readonly vertexCount?: number; + + readonly texCoord?: boolean; + readonly lightTexCoord?: boolean; + readonly normal?: boolean; + readonly tangent?: boolean; +} + +export interface VertexBufferWriteArrayProps { + readonly position?: readonly Vector3Object[]; + readonly texCoord?: readonly Vector2Object[]; + readonly lightTexCoord?: readonly Vector2Object[]; + readonly normal?: readonly Vector3Object[]; + readonly tangent?: readonly Vector4Object[]; +} + +export interface VertexBufferWriteTypedArrayProps { + readonly position?: Float32Array; + readonly texCoord?: Float32Array; + readonly lightTexCoord?: Float32Array; + readonly normal?: Float32Array; + readonly tangent?: Float32Array; +} + +export class VertexBuffer { + + readonly type!: "VertexBuffer"; + _renderer: Renderer; + + _name: string; + + _positionBuffer: GPUBuffer; + _texCoordBuffer: GPUBuffer | null; + _lightTexCoordBuffer: GPUBuffer | null; + _normalBuffer: GPUBuffer | null; + _tangentBuffer: GPUBuffer | null; + + constructor(renderer: Renderer, { + name = "", + vertexCount, + texCoord = false, + lightTexCoord = false, + normal = false, + tangent = false, + }: VertexBufferProps) { + this._renderer = renderer; + + this._name = name; + + this._positionBuffer = renderer._device.createBuffer({ + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, + size: vertexCount * POSITION_SIZE, + label: `${this._name}.position`, + }); + + this._texCoordBuffer = texCoord ? renderer._device.createBuffer({ + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, + size: vertexCount * TEX_COORD_SIZE, + label: `${this._name}.texCoord`, + }) : null; + + this._lightTexCoordBuffer = lightTexCoord ? renderer._device.createBuffer({ + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, + size: vertexCount * LIGHT_TEX_COORD_SIZE, + label: `${this._name}.lightTexCoord`, + }) : null; + + this._normalBuffer = normal ? renderer._device.createBuffer({ + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, + size: vertexCount * NORMAL_SIZE, + label: `${this._name}.normal`, + }) : null; + + this._tangentBuffer = tangent ? renderer._device.createBuffer({ + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, + size: vertexCount * TANGENT_SIZE, + label: `${this._name}.tangent`, + }) : null; + } + + /** + * Destroys owned GPU resources. The vertex buffer should not be used after + * calling this method. + * @returns `this` for chaining + */ + dispose(): VertexBuffer { + this._positionBuffer.destroy(); + this._texCoordBuffer?.destroy(); + this._lightTexCoordBuffer?.destroy(); + this._normalBuffer?.destroy(); + this._tangentBuffer?.destroy(); + return this; + } + + get vertexCount(): number { + return this._positionBuffer.size / POSITION_SIZE | 0; + } + + get hasTexCoord(): boolean { + return this._texCoordBuffer !== null; + } + + get hasLightTexCoord(): boolean { + return this._lightTexCoordBuffer !== null; + } + + get hasNormal(): boolean { + return this._normalBuffer !== null; + } + + get hasTangent(): boolean { + return this._tangentBuffer !== null; + } + + writeArray(offset: number, { + position, + texCoord, + lightTexCoord, + normal, + tangent, + }: VertexBufferWriteArrayProps): VertexBuffer { + + if (position !== undefined) { + const array = new Float32Array(position.length * 3); + for (let vi = 0, ptr = 0; vi < position.length; ++vi) { + const vertex = position[vi]!; + array[ptr++] = vertex.x; + array[ptr++] = vertex.y; + array[ptr++] = vertex.z; + } + this._renderer._device.queue.writeBuffer(this._positionBuffer, offset * POSITION_SIZE | 0, array); + } + + if (texCoord !== undefined) { + if (this._texCoordBuffer === null) { + throw new Error(`Cannot write array to a missing vertex attribute. Tried writing texture coordinates and vertex buffer [${this._name}] does not have texture coordinates.`); + } + const array = new Float32Array(texCoord.length * 2); + for (let vi = 0, ptr = 0; vi < texCoord.length; ++vi) { + const vertex = texCoord[vi]!; + array[ptr++] = vertex.x; + array[ptr++] = vertex.y; + } + this._renderer._device.queue.writeBuffer(this._texCoordBuffer, offset * TEX_COORD_SIZE | 0, array); + } + + if (lightTexCoord !== undefined) { + if (this._lightTexCoordBuffer === null) { + throw new Error(`Cannot write array to a missing vertex attribute. Tried writing light texture coordinates and vertex buffer [${this._name}] does not have light texture coordinates.`); + } + const array = new Float32Array(lightTexCoord.length * 2); + for (let vi = 0, ptr = 0; vi < lightTexCoord.length; ++vi) { + const vertex = lightTexCoord[vi]!; + array[ptr++] = vertex.x; + array[ptr++] = vertex.y; + } + this._renderer._device.queue.writeBuffer(this._lightTexCoordBuffer, offset * LIGHT_TEX_COORD_SIZE | 0, array); + } + + if (normal !== undefined) { + if (this._normalBuffer === null) { + throw new Error(`Cannot write array to a missing vertex attribute. Tried writing normals and vertex buffer [${this._name}] does not have normals.`); + } + const array = new Float32Array(normal.length * 3); + for (let vi = 0, ptr = 0; vi < normal.length; ++vi) { + const vertex = normal[vi]!; + array[ptr++] = vertex.x; + array[ptr++] = vertex.y; + array[ptr++] = vertex.z; + } + this._renderer._device.queue.writeBuffer(this._normalBuffer, offset * NORMAL_SIZE | 0, array); + } + + if (tangent !== undefined) { + if (this._tangentBuffer === null) { + throw new Error(`Cannot write array to a missing vertex attribute. Tried writing tangents and vertex buffer [${this._name}] does not have tangents.`); + } + const array = new Float32Array(tangent.length * 4); + for (let vi = 0, ptr = 0; vi < tangent.length; ++vi) { + const vertex = tangent[vi]!; + array[ptr++] = vertex.x; + array[ptr++] = vertex.y; + array[ptr++] = vertex.z; + array[ptr++] = vertex.w; + } + this._renderer._device.queue.writeBuffer(this._tangentBuffer, offset * TANGENT_SIZE | 0, array); + } + + return this; + } + + writeTypedArray(offset: number, { + position, + texCoord, + lightTexCoord, + normal, + tangent, + }: VertexBufferWriteTypedArrayProps): VertexBuffer { + + if (position !== undefined) { + this._renderer._device.queue.writeBuffer(this._positionBuffer, offset * POSITION_SIZE | 0, position); + } + + if (texCoord !== undefined) { + if (this._texCoordBuffer === null) { + throw new Error(`Cannot write typed array to a missing vertex attribute. Tried writing texture coordinates and vertex buffer [${this._name}] does not have texture coordinates.`); + } + this._renderer._device.queue.writeBuffer(this._texCoordBuffer, offset * TEX_COORD_SIZE | 0, texCoord); + } + + if (lightTexCoord !== undefined) { + if (this._lightTexCoordBuffer === null) { + throw new Error(`Cannot write typed array to a missing vertex attribute. Tried writing light texture coordinates and vertex buffer [${this._name}] does not have light texture coordinates.`); + } + this._renderer._device.queue.writeBuffer(this._lightTexCoordBuffer, offset * LIGHT_TEX_COORD_SIZE | 0, lightTexCoord); + } + + if (normal !== undefined) { + if (this._normalBuffer === null) { + throw new Error(`Cannot write typed array to a missing vertex attribute. Tried writing normals and vertex buffer [${this._name}] does not have normals.`); + } + this._renderer._device.queue.writeBuffer(this._normalBuffer, offset * NORMAL_SIZE | 0, normal); + } + + if (tangent !== undefined) { + if (this._tangentBuffer === null) { + throw new Error(`Cannot write typed array to a missing vertex attribute. Tried writing tangents and vertex buffer [${this._name}] does not have tangents.`); + } + this._renderer._device.queue.writeBuffer(this._tangentBuffer, offset * TANGENT_SIZE | 0, tangent); + } + + return this; + } + + /** + * Resize the vertex buffer and/or add or remove vertex attributes, + * discarding currently stored data. + * @param props Desired buffer properties. Any unspecified property will + * stay unchanged. + * @returns `this` for chaining + */ + resizeDiscard({ + vertexCount = this.vertexCount, + texCoord = this.hasTexCoord, + lightTexCoord = this.hasLightTexCoord, + normal = this.hasNormal, + tangent = this.hasTangent, + }: VertexBufferResizeProps): VertexBuffer { + + this._positionBuffer.destroy(); + this._texCoordBuffer?.destroy(); + this._lightTexCoordBuffer?.destroy(); + this._normalBuffer?.destroy(); + this._tangentBuffer?.destroy(); + + this._positionBuffer = this._renderer._device.createBuffer({ + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, + size: vertexCount * POSITION_SIZE, + label: `${this._name}.position`, + }); + + this._texCoordBuffer = texCoord ? this._renderer._device.createBuffer({ + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, + size: vertexCount * TEX_COORD_SIZE, + label: `${this._name}.texCoord`, + }) : null; + + this._lightTexCoordBuffer = lightTexCoord ? this._renderer._device.createBuffer({ + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, + size: vertexCount * LIGHT_TEX_COORD_SIZE, + label: `${this._name}.lightTexCoord`, + }) : null; + + this._normalBuffer = normal ? this._renderer._device.createBuffer({ + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, + size: vertexCount * NORMAL_SIZE, + label: `${this._name}.normal`, + }) : null; + + this._tangentBuffer = tangent ? this._renderer._device.createBuffer({ + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, + size: vertexCount * TANGENT_SIZE, + label: `${this._name}.tangent`, + }) : null; + + return this; + } + + /** + * Resize the vertex buffer and/or add vertex attributes if it can't hold + * provided number of vertices or doesn't have provided attributes, + * potentially discarding currently stored data. + * @param props Desired buffer properties. Any unspecified property will be + * ignored. + * @returns `this` for chaining + */ + ensureSizeDiscard({ + vertexCount = this.vertexCount, + texCoord = this.hasTexCoord, + lightTexCoord = this.hasLightTexCoord, + normal = this.hasNormal, + tangent = this.hasTangent, + }): VertexBuffer { + const currentVertexCount = this.vertexCount; + + if (currentVertexCount < vertexCount) { + this._positionBuffer.destroy(); + this._positionBuffer = this._renderer._device.createBuffer({ + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, + size: vertexCount * POSITION_SIZE, + label: `${this._name}.position`, + }); + } + + if (currentVertexCount < vertexCount || texCoord && !this.hasTexCoord) { + this._texCoordBuffer?.destroy(); + this._texCoordBuffer = this._renderer._device.createBuffer({ + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, + size: vertexCount * TEX_COORD_SIZE, + label: `${this._name}.texCoord`, + }); + } + + if (currentVertexCount < vertexCount || lightTexCoord && !this.hasLightTexCoord) { + this._lightTexCoordBuffer?.destroy(); + this._lightTexCoordBuffer = this._renderer._device.createBuffer({ + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, + size: vertexCount * LIGHT_TEX_COORD_SIZE, + label: `${this._name}.lightTexCoord`, + }); + } + + if (currentVertexCount < vertexCount || normal && !this.hasNormal) { + this._normalBuffer?.destroy(); + this._normalBuffer = this._renderer._device.createBuffer({ + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, + size: vertexCount * NORMAL_SIZE, + label: `${this._name}.normal`, + }); + } + + if (currentVertexCount < vertexCount || tangent && !this.hasTangent) { + this._tangentBuffer?.destroy(); + this._tangentBuffer = this._renderer._device.createBuffer({ + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, + size: vertexCount * TANGENT_SIZE, + label: `${this._name}.tangent`, + }); + } + + return this; + } +} + +Object.defineProperty(VertexBuffer.prototype, "type", { value: "VertexBuffer" }); + +export function isVertexBuffer(value: unknown): value is VertexBuffer { + return Boolean(value) && (value as VertexBuffer).type === "VertexBuffer"; +} diff --git a/src/resources/index.ts b/src/resources/index.ts new file mode 100644 index 0000000..c526276 --- /dev/null +++ b/src/resources/index.ts @@ -0,0 +1,10 @@ +/*! + * 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 * from "./IndexBuffer"; +export * from "./Material"; +export * from "./Texture2D"; +export * from "./VertexBuffer";