Reorganize stuff, vertex attribute types

This commit is contained in:
Szymon Nowakowski 2023-07-29 02:01:22 +02:00 committed by Szymon Nowakowski
parent 174201afc1
commit ee52894f52
22 changed files with 1052 additions and 328 deletions

View File

@ -31,6 +31,14 @@
".": { ".": {
"types": "./dist/oktaeder.d.ts", "types": "./dist/oktaeder.d.ts",
"import": "./dist/oktaeder.js" "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"
} }
} }
} }

View File

@ -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";
}

View File

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

View File

@ -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";
}

View File

@ -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";
}

View File

@ -4,7 +4,7 @@
* obtain one at http://mozilla.org/MPL/2.0/. * obtain one at http://mozilla.org/MPL/2.0/.
*/ */
import { Node } from "./Node"; import { Node } from ".";
export type Camera = CameraOrthographic | CameraPerspective; export type Camera = CameraOrthographic | CameraPerspective;
@ -43,8 +43,6 @@ export class CameraOrthographic {
nearPlane, nearPlane,
farPlane, farPlane,
}: CameraOrthographicProps) { }: CameraOrthographicProps) {
Object.defineProperty(this, "type", { value: "CameraOrthographic" });
this._name = name; this._name = name;
this._verticalSize = verticalSize; this._verticalSize = verticalSize;
@ -65,6 +63,8 @@ export class CameraOrthographic {
} }
} }
Object.defineProperty(CameraOrthographic.prototype, "type", { value: "CameraOrthographic" });
export class CameraPerspective { export class CameraPerspective {
readonly type!: "CameraPerspective"; readonly type!: "CameraPerspective";
@ -84,8 +84,6 @@ export class CameraPerspective {
nearPlane, nearPlane,
farPlane, farPlane,
}: CameraPerspectiveProps) { }: CameraPerspectiveProps) {
Object.defineProperty(this, "type", { value: "CameraPerspective" });
this._name = name; this._name = name;
this._verticalFovRad = verticalFovRad; 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 { export function isCameraOrthographic(value: unknown): value is CameraOrthographic {
return Boolean(value) && (value as CameraOrthographic).type === "CameraOrthographic"; return Boolean(value) && (value as CameraOrthographic).type === "CameraOrthographic";
} }

View File

@ -4,7 +4,7 @@
* obtain one at http://mozilla.org/MPL/2.0/. * obtain one at http://mozilla.org/MPL/2.0/.
*/ */
import { Vector3Object } from "./Vector3"; import { Vector3Object } from ".";
/* Named colors /* Named colors
* black #000000 (0, 0, 0, 1) * black #000000 (0, 0, 0, 1)
@ -63,8 +63,6 @@ export class Color {
b: number; b: number;
constructor(r: number, g: number, b: number) { constructor(r: number, g: number, b: number) {
Object.defineProperty(this, "type", { value: "Color" });
this.r = r; this.r = r;
this.g = g; this.g = g;
this.b = b; 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 { export function isColor(value: unknown): value is Color {
return Boolean(value) && (value as Color).type === "Color"; return Boolean(value) && (value as Color).type === "Color";
} }

View File

@ -4,8 +4,7 @@
* obtain one at http://mozilla.org/MPL/2.0/. * obtain one at http://mozilla.org/MPL/2.0/.
*/ */
import { QuaternionObject } from "./Quaternion"; import { QuaternionObject, Vector3Object } from ".";
import { Vector3Object } from "./Vector3";
export interface Matrix4x4Object { export interface Matrix4x4Object {
readonly ix: number; readonly ix: number;
@ -60,8 +59,6 @@ export class Matrix4x4 {
kx: number, ky: number, kz: number, kw: number, kx: number, ky: number, kz: number, kw: number,
tx: number, ty: number, tz: number, tw: number tx: number, ty: number, tz: number, tw: number
) { ) {
Object.defineProperty(this, "type", { value: "Matrix4x4" });
this.ix = ix; this.ix = ix;
this.iy = iy; this.iy = iy;
this.iz = iz; 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 { export function isMatrix4x4(value: unknown): value is Matrix4x4 {
return Boolean(value) && (value as Matrix4x4).type === "Matrix4x4"; return Boolean(value) && (value as Matrix4x4).type === "Matrix4x4";
} }

View File

@ -4,8 +4,7 @@
* obtain one at http://mozilla.org/MPL/2.0/. * obtain one at http://mozilla.org/MPL/2.0/.
*/ */
import { IndexBuffer } from "./IndexBuffer"; import { IndexBuffer, VertexBuffer } from "../resources";
import { VertexBuffer } from "./VertexBuffer";
export type Submesh = { export type Submesh = {
start: number, start: number,
@ -36,8 +35,6 @@ export class Mesh {
indexBuffer, indexBuffer,
submeshes, submeshes,
}: MeshProps) { }: MeshProps) {
Object.defineProperty(this, "type", { value: "Mesh" });
this._name = name; this._name = name;
this._vertexBuffer = vertexBuffer; 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 { export function isMesh(value: unknown): value is Mesh {
return Boolean(value) && (value as Mesh).type === "Mesh"; return Boolean(value) && (value as Mesh).type === "Mesh";
} }

View File

@ -4,11 +4,8 @@
* obtain one at http://mozilla.org/MPL/2.0/. * obtain one at http://mozilla.org/MPL/2.0/.
*/ */
import { Camera } from "./Camera"; import { Camera, Mesh, Quaternion, QuaternionObject, Vector3, Vector3Object } from ".";
import { Material } from "./Material"; import { Material } from "../resources";
import { Mesh } from "./Mesh";
import { Quaternion, QuaternionObject } from "./Quaternion";
import { Vector3, Vector3Object } from "./Vector3";
export interface NodeProps { export interface NodeProps {
readonly name?: string; readonly name?: string;
@ -57,8 +54,6 @@ export class Node {
materials = [], materials = [],
children = [], children = [],
}: NodeProps) { }: NodeProps) {
Object.defineProperty(this, "type", { value: "Node" });
this._name = name; this._name = name;
this._translation = translation !== undefined ? Vector3.fromObject(translation) : Vector3.zero(); 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 { export function isNode(value: unknown): value is Node {
return Boolean(value) && (value as Node).type === "Node"; return Boolean(value) && (value as Node).type === "Node";
} }

View File

@ -23,8 +23,6 @@ export class Quaternion {
w: number; w: number;
constructor(x: number, y: number, z: number, w: number) { constructor(x: number, y: number, z: number, w: number) {
Object.defineProperty(this, "type", { value: "Quaternion" });
this.x = x; this.x = x;
this.y = y; this.y = y;
this.z = z; 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 { export function isQuaternion(value: unknown): value is Quaternion {
return Boolean(value) && (value as Quaternion).type === "Quaternion"; return Boolean(value) && (value as Quaternion).type === "Quaternion";
} }

View File

@ -4,7 +4,7 @@
* obtain one at http://mozilla.org/MPL/2.0/. * obtain one at http://mozilla.org/MPL/2.0/.
*/ */
import { Node } from "./Node"; import { Node } from ".";
export interface SceneProps { export interface SceneProps {
readonly name?: string; readonly name?: string;
@ -24,14 +24,14 @@ export class Scene {
name = "", name = "",
nodes = [], nodes = [],
}: SceneProps) { }: SceneProps) {
Object.defineProperty(this, "type", { value: "Scene" });
this._name = name; this._name = name;
this._nodes = nodes; this._nodes = nodes;
} }
} }
Object.defineProperty(Scene.prototype, "type", { value: "Scene" });
export function isScene(value: unknown): value is Scene { export function isScene(value: unknown): value is Scene {
return Boolean(value) && (value as Scene).type === "Scene"; return Boolean(value) && (value as Scene).type === "Scene";
} }

87
src/data/Vector2.ts Normal file
View File

@ -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";
}

View File

@ -21,8 +21,6 @@ export class Vector3 {
z: number; z: number;
constructor(x: number, y: number, z: number) { constructor(x: number, y: number, z: number) {
Object.defineProperty(this, "type", { value: "Vector3" });
this.x = x; this.x = x;
this.y = y; this.y = y;
this.z = z; 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 { export function isVector3(value: unknown): value is Vector3 {
return Boolean(value) && (value as Vector3).type === "Vector3"; return Boolean(value) && (value as Vector3).type === "Vector3";
} }

113
src/data/Vector4.ts Normal file
View File

@ -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";
}

16
src/data/index.ts Normal file
View File

@ -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";

View File

@ -4,16 +4,108 @@
* obtain one at http://mozilla.org/MPL/2.0/. * obtain one at http://mozilla.org/MPL/2.0/.
*/ */
export * from "./Camera"; import { IndexBuffer, IndexBufferProps, Material, MaterialProps, Texture2D, Texture2DProps, VertexBuffer, VertexBufferProps } from "./resources";
export * from "./Color";
export * from "./IndexBuffer"; export class Renderer {
export * from "./Material";
export * from "./Matrix4x4"; _adapter: GPUAdapter;
export * from "./Mesh"; _device: GPUDevice;
export * from "./Node"; _context: GPUCanvasContext;
export * from "./Quaternion"; _format: GPUTextureFormat;
export * from "./Renderer";
export * from "./Scene"; /** 1×1 rgba8unorm texture of [255, 255, 255, 255] */
export * from "./Texture2D"; _textureWhite: Texture2D;
export * from "./Vector3"; /** 1×1 rgba8unorm texture of [0, 0, 0, 255] */
export * from "./VertexBuffer"; _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);
}
}

View File

@ -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;
}
}

View File

@ -4,9 +4,9 @@
* obtain one at http://mozilla.org/MPL/2.0/. * obtain one at http://mozilla.org/MPL/2.0/.
*/ */
import { Color, ColorObject } from "./Color"; import { Texture2D } from ".";
import { Renderer } from "./Renderer"; import { Color, ColorObject } from "../data";
import { Texture2D } from "./Texture2D"; import { Renderer } from "../oktaeder";
export const UNIFORM_BUFFER_SIZE = 64; export const UNIFORM_BUFFER_SIZE = 64;
@ -81,8 +81,6 @@ export class Material {
transparent = false, transparent = false,
doubleSided = false, doubleSided = false,
}: MaterialProps) { }: MaterialProps) {
Object.defineProperty(this, "type", { value: "Material" });
this._renderer = renderer; this._renderer = renderer;
this._name = name; this._name = name;
@ -108,11 +106,18 @@ export class Material {
this._doubleSided = doubleSided; this._doubleSided = doubleSided;
} }
/**
* Destroys owned GPU resources. The material should not be used after
* calling this method.
* @returns `this` for chaining
*/
dispose(): Material { dispose(): Material {
return this; return this;
} }
} }
Object.defineProperty(Material.prototype, "type", { value: "Material" });
export function isMaterial(value: unknown): value is Material { export function isMaterial(value: unknown): value is Material {
return Boolean(value) && (value as Material).type === "Material"; return Boolean(value) && (value as Material).type === "Material";
} }

160
src/resources/Texture2D.ts Normal file
View File

@ -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;
}
}

View File

@ -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";
}

10
src/resources/index.ts Normal file
View File

@ -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";