mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-05 07:18:29 +08:00
Started work on WebGPU renderer.
This commit is contained in:
parent
9960d745c2
commit
baffab3234
@ -17,6 +17,7 @@
|
|||||||
"ignorePatterns": ["webpack.*.js"],
|
"ignorePatterns": ["webpack.*.js"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/array-type": ["warn", { "default": "array", "readonly": "array" }],
|
"@typescript-eslint/array-type": ["warn", { "default": "array", "readonly": "array" }],
|
||||||
|
"@typescript-eslint/ban-ts-ignore": "off",
|
||||||
"@typescript-eslint/camelcase": "off",
|
"@typescript-eslint/camelcase": "off",
|
||||||
"@typescript-eslint/class-name-casing": "warn",
|
"@typescript-eslint/class-name-casing": "warn",
|
||||||
"@typescript-eslint/explicit-function-return-type": ["warn", { "allowExpressions": true }],
|
"@typescript-eslint/explicit-function-return-type": ["warn", { "allowExpressions": true }],
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@webgpu/glslang": "^0.0.12",
|
||||||
"camera-controls": "^1.16.2",
|
"camera-controls": "^1.16.2",
|
||||||
"core-js": "^3.6.1",
|
"core-js": "^3.6.1",
|
||||||
"golden-layout": "^1.5.9",
|
"golden-layout": "^1.5.9",
|
||||||
@ -31,6 +32,7 @@
|
|||||||
"@types/yaml": "^1.2.0",
|
"@types/yaml": "^1.2.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.12.0",
|
"@typescript-eslint/eslint-plugin": "^2.12.0",
|
||||||
"@typescript-eslint/parser": "^2.12.0",
|
"@typescript-eslint/parser": "^2.12.0",
|
||||||
|
"@webgpu/types": "^0.0.21",
|
||||||
"cheerio": "^1.0.0-rc.3",
|
"cheerio": "^1.0.0-rc.3",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
"copy-webpack-plugin": "^5.1.1",
|
"copy-webpack-plugin": "^5.1.1",
|
||||||
|
@ -37,9 +37,23 @@ export class Vec3 {
|
|||||||
constructor(public x: number, public y: number, public z: number) {}
|
constructor(public x: number, public y: number, public z: number) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores data in column-major order.
|
||||||
|
*/
|
||||||
export class Mat4 {
|
export class Mat4 {
|
||||||
static of(...values: readonly number[]): Mat4 {
|
// prettier-ignore
|
||||||
return new Mat4(new Float32Array(values));
|
static of(
|
||||||
|
m00: number, m01: number, m02: number, m03: number,
|
||||||
|
m10: number, m11: number, m12: number, m13: number,
|
||||||
|
m20: number, m21: number, m22: number, m23: number,
|
||||||
|
m30: number, m31: number, m32: number, m33: number,
|
||||||
|
): Mat4 {
|
||||||
|
return new Mat4(new Float32Array([
|
||||||
|
m00, m10, m20, m30,
|
||||||
|
m01, m11, m21, m31,
|
||||||
|
m02, m12, m22, m32,
|
||||||
|
m03, m13, m23, m33,
|
||||||
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
static identity(): Mat4 {
|
static identity(): Mat4 {
|
||||||
@ -73,7 +87,7 @@ function mat4_product_into_array(array: Float32Array, a: Mat4, b: Mat4): void {
|
|||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
for (let j = 0; j < 4; j++) {
|
for (let j = 0; j < 4; j++) {
|
||||||
for (let k = 0; k < 4; k++) {
|
for (let k = 0; k < 4; k++) {
|
||||||
array[i * 4 + j] += a.data[i * 4 + k] * b.data[k * 4 + j];
|
array[i + j * 4] += a.data[i + k * 4] * b.data[k + j * 4];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,9 +46,9 @@ export class Camera {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private update_transform(): void {
|
private update_transform(): void {
|
||||||
this._transform.data[3] = -this.look_at.x;
|
this._transform.data[12] = -this.look_at.x;
|
||||||
this._transform.data[7] = -this.look_at.y;
|
this._transform.data[13] = -this.look_at.y;
|
||||||
this._transform.data[11] = -this.look_at.z;
|
this._transform.data[14] = -this.look_at.z;
|
||||||
this._transform.data[0] = this._zoom;
|
this._transform.data[0] = this._zoom;
|
||||||
this._transform.data[5] = this._zoom;
|
this._transform.data[5] = this._zoom;
|
||||||
this._transform.data[10] = this._zoom;
|
this._transform.data[10] = this._zoom;
|
||||||
|
17
src/core/rendering/GlRenderer.ts
Normal file
17
src/core/rendering/GlRenderer.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Renderer } from "./Renderer";
|
||||||
|
import { VertexFormat } from "./VertexFormat";
|
||||||
|
import { MeshBuilder } from "./MeshBuilder";
|
||||||
|
import { Texture } from "./Texture";
|
||||||
|
import { Mesh } from "./Mesh";
|
||||||
|
|
||||||
|
export interface GlRenderer<MeshType extends Mesh> extends Renderer {
|
||||||
|
mesh_builder(vertex_format: VertexFormat): MeshBuilder<MeshType>;
|
||||||
|
|
||||||
|
mesh(
|
||||||
|
vertex_format: VertexFormat,
|
||||||
|
vertex_data: ArrayBuffer,
|
||||||
|
index_data: ArrayBuffer,
|
||||||
|
index_count: number,
|
||||||
|
texture?: Texture,
|
||||||
|
): MeshType;
|
||||||
|
}
|
@ -1,162 +1,7 @@
|
|||||||
import {
|
import { VertexFormat } from "./VertexFormat";
|
||||||
GL,
|
|
||||||
vertex_format_size,
|
|
||||||
vertex_format_tex_offset,
|
|
||||||
VERTEX_POS_LOC,
|
|
||||||
VERTEX_TEX_LOC,
|
|
||||||
VertexFormat,
|
|
||||||
} from "./VertexFormat";
|
|
||||||
import { assert } from "../util";
|
|
||||||
import { Texture } from "./Texture";
|
import { Texture } from "./Texture";
|
||||||
|
|
||||||
export class Mesh {
|
export interface Mesh {
|
||||||
private readonly index_count: number;
|
readonly format: VertexFormat;
|
||||||
private vao: WebGLVertexArrayObject | null = null;
|
readonly texture?: Texture;
|
||||||
private vertex_buffer: WebGLBuffer | null = null;
|
|
||||||
private index_buffer: WebGLBuffer | null = null;
|
|
||||||
private uploaded = false;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly format: VertexFormat,
|
|
||||||
private readonly vertex_data: ArrayBuffer,
|
|
||||||
private readonly index_data: ArrayBuffer,
|
|
||||||
readonly texture?: Texture,
|
|
||||||
) {
|
|
||||||
this.index_count = index_data.byteLength / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
upload(gl: GL): void {
|
|
||||||
if (this.uploaded) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.vao = gl.createVertexArray();
|
|
||||||
if (this.vao == null) throw new Error("Failed to create VAO.");
|
|
||||||
|
|
||||||
this.vertex_buffer = gl.createBuffer();
|
|
||||||
if (this.vertex_buffer == null) throw new Error("Failed to create vertex buffer.");
|
|
||||||
|
|
||||||
this.index_buffer = gl.createBuffer();
|
|
||||||
if (this.index_buffer == null) throw new Error("Failed to create index buffer.");
|
|
||||||
|
|
||||||
gl.bindVertexArray(this.vao);
|
|
||||||
|
|
||||||
// Vertex data.
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertex_buffer);
|
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, this.vertex_data, gl.STATIC_DRAW);
|
|
||||||
|
|
||||||
const vertex_size = vertex_format_size(this.format);
|
|
||||||
|
|
||||||
gl.vertexAttribPointer(VERTEX_POS_LOC, 3, gl.FLOAT, true, vertex_size, 0);
|
|
||||||
gl.enableVertexAttribArray(VERTEX_POS_LOC);
|
|
||||||
|
|
||||||
const tex_offset = vertex_format_tex_offset(this.format);
|
|
||||||
|
|
||||||
if (tex_offset !== -1) {
|
|
||||||
gl.vertexAttribPointer(
|
|
||||||
VERTEX_TEX_LOC,
|
|
||||||
2,
|
|
||||||
gl.UNSIGNED_SHORT,
|
|
||||||
true,
|
|
||||||
vertex_size,
|
|
||||||
tex_offset,
|
|
||||||
);
|
|
||||||
gl.enableVertexAttribArray(VERTEX_TEX_LOC);
|
|
||||||
}
|
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
||||||
|
|
||||||
// Index data.
|
|
||||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.index_buffer);
|
|
||||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.index_data, gl.STATIC_DRAW);
|
|
||||||
|
|
||||||
gl.bindVertexArray(null);
|
|
||||||
|
|
||||||
this.texture?.upload(gl);
|
|
||||||
|
|
||||||
this.uploaded = true;
|
|
||||||
} catch (e) {
|
|
||||||
gl.deleteVertexArray(this.vao);
|
|
||||||
this.vao = null;
|
|
||||||
gl.deleteBuffer(this.vertex_buffer);
|
|
||||||
this.vertex_buffer = null;
|
|
||||||
gl.deleteBuffer(this.index_buffer);
|
|
||||||
this.index_buffer = null;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render(gl: GL): void {
|
|
||||||
gl.bindVertexArray(this.vao);
|
|
||||||
|
|
||||||
gl.drawElements(gl.TRIANGLES, this.index_count, gl.UNSIGNED_SHORT, 0);
|
|
||||||
|
|
||||||
gl.bindVertexArray(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(gl: GL): void {
|
|
||||||
gl.deleteVertexArray(this.vao);
|
|
||||||
gl.deleteBuffer(this.vertex_buffer);
|
|
||||||
gl.deleteBuffer(this.index_buffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MeshBuilder {
|
|
||||||
private readonly vertex_data: {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
z: number;
|
|
||||||
u?: number;
|
|
||||||
v?: number;
|
|
||||||
}[] = [];
|
|
||||||
private readonly index_data: number[] = [];
|
|
||||||
private _texture?: Texture;
|
|
||||||
|
|
||||||
constructor(private readonly format: VertexFormat) {}
|
|
||||||
|
|
||||||
vertex(x: number, y: number, z: number, u?: number, v?: number): this {
|
|
||||||
switch (this.format) {
|
|
||||||
case VertexFormat.PosTex:
|
|
||||||
assert(
|
|
||||||
u != undefined && v != undefined,
|
|
||||||
`Vertex format ${VertexFormat[this.format]} requires texture coordinates.`,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.vertex_data.push({ x, y, z, u, v });
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
triangle(v1: number, v2: number, v3: number): this {
|
|
||||||
this.index_data.push(v1, v2, v3);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
texture(tex: Texture): this {
|
|
||||||
this._texture = tex;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
build(): Mesh {
|
|
||||||
const v_size = vertex_format_size(this.format);
|
|
||||||
const v_tex_offset = vertex_format_tex_offset(this.format);
|
|
||||||
const v_data = new ArrayBuffer(this.vertex_data.length * v_size);
|
|
||||||
const v_view = new DataView(v_data);
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
for (const { x, y, z, u, v } of this.vertex_data) {
|
|
||||||
v_view.setFloat32(i, x, true);
|
|
||||||
v_view.setFloat32(i + 4, y, true);
|
|
||||||
v_view.setFloat32(i + 8, z, true);
|
|
||||||
|
|
||||||
if (v_tex_offset !== -1) {
|
|
||||||
v_view.setUint16(i + v_tex_offset, u! * 0xffff, true);
|
|
||||||
v_view.setUint16(i + v_tex_offset + 2, v! * 0xffff, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
i += v_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Mesh(this.format, v_data, new Uint16Array(this.index_data), this._texture);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
78
src/core/rendering/MeshBuilder.ts
Normal file
78
src/core/rendering/MeshBuilder.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { Texture } from "./Texture";
|
||||||
|
import { vertex_format_size, vertex_format_tex_offset, VertexFormat } from "./VertexFormat";
|
||||||
|
import { assert } from "../util";
|
||||||
|
import { Mesh } from "./Mesh";
|
||||||
|
import { GlRenderer } from "./GlRenderer";
|
||||||
|
|
||||||
|
export class MeshBuilder<MeshType extends Mesh> {
|
||||||
|
private readonly vertex_data: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
u?: number;
|
||||||
|
v?: number;
|
||||||
|
}[] = [];
|
||||||
|
private readonly index_data: number[] = [];
|
||||||
|
private _texture?: Texture;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly renderer: GlRenderer<MeshType>,
|
||||||
|
private readonly format: VertexFormat,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
vertex(x: number, y: number, z: number, u?: number, v?: number): this {
|
||||||
|
switch (this.format) {
|
||||||
|
case VertexFormat.PosTex:
|
||||||
|
assert(
|
||||||
|
u != undefined && v != undefined,
|
||||||
|
`Vertex format ${VertexFormat[this.format]} requires texture coordinates.`,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.vertex_data.push({ x, y, z, u, v });
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
triangle(v1: number, v2: number, v3: number): this {
|
||||||
|
this.index_data.push(v1, v2, v3);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
texture(tex: Texture): this {
|
||||||
|
this._texture = tex;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
build(): MeshType {
|
||||||
|
const v_size = vertex_format_size(this.format);
|
||||||
|
const v_tex_offset = vertex_format_tex_offset(this.format);
|
||||||
|
const v_data = new ArrayBuffer(this.vertex_data.length * v_size);
|
||||||
|
const v_view = new DataView(v_data);
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
for (const { x, y, z, u, v } of this.vertex_data) {
|
||||||
|
v_view.setFloat32(i, x, true);
|
||||||
|
v_view.setFloat32(i + 4, y, true);
|
||||||
|
v_view.setFloat32(i + 8, z, true);
|
||||||
|
|
||||||
|
if (v_tex_offset !== -1) {
|
||||||
|
v_view.setUint16(i + v_tex_offset, u! * 0xffff, true);
|
||||||
|
v_view.setUint16(i + v_tex_offset + 2, v! * 0xffff, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
i += v_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
const i_data = new Uint16Array(2 * Math.ceil(this.index_data.length / 2));
|
||||||
|
i_data.set(this.index_data);
|
||||||
|
|
||||||
|
return this.renderer.mesh(
|
||||||
|
this.format,
|
||||||
|
v_data,
|
||||||
|
i_data,
|
||||||
|
this.index_data.length,
|
||||||
|
this._texture,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,11 @@
|
|||||||
import { Disposable } from "../observable/Disposable";
|
import { Disposable } from "../observable/Disposable";
|
||||||
|
|
||||||
export abstract class Renderer implements Disposable {
|
export interface Renderer extends Disposable {
|
||||||
abstract readonly canvas_element: HTMLCanvasElement;
|
readonly canvas_element: HTMLCanvasElement;
|
||||||
|
|
||||||
abstract dispose(): void;
|
start_rendering(): void;
|
||||||
|
|
||||||
abstract start_rendering(): void;
|
stop_rendering(): void;
|
||||||
|
|
||||||
abstract stop_rendering(): void;
|
set_size(width: number, height: number): void;
|
||||||
|
|
||||||
abstract set_size(width: number, height: number): void;
|
|
||||||
}
|
}
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
import { Mesh } from "./Mesh";
|
|
||||||
import { IdentityTransform, Transform } from "./Transform";
|
|
||||||
import { GL } from "./VertexFormat";
|
|
||||||
|
|
||||||
export class Scene {
|
|
||||||
readonly root_node = new Node(undefined, new IdentityTransform());
|
|
||||||
|
|
||||||
constructor(private readonly gl: GL) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new node with `node` as parent. Takes ownership of `mesh`.
|
|
||||||
*
|
|
||||||
* @param node - The parent node.
|
|
||||||
* @param mesh - The new node's mesh.
|
|
||||||
* @param transform - The new node's transform.
|
|
||||||
*/
|
|
||||||
add_child(node: Node, mesh: Mesh, transform: Transform): this {
|
|
||||||
node.children.push(new Node(mesh, transform));
|
|
||||||
mesh.upload(this.gl);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes all GL objects related to this scene and resets the scene.
|
|
||||||
*/
|
|
||||||
delete(): void {
|
|
||||||
this.traverse(node => {
|
|
||||||
node.mesh?.texture?.delete(this.gl);
|
|
||||||
node.mesh?.delete(this.gl);
|
|
||||||
node.mesh = undefined;
|
|
||||||
}, undefined);
|
|
||||||
|
|
||||||
this.root_node.children.splice(0);
|
|
||||||
this.root_node.transform = new IdentityTransform();
|
|
||||||
}
|
|
||||||
|
|
||||||
traverse<T>(f: (node: Node, data: T) => T, data: T): void {
|
|
||||||
this.traverse_node(this.root_node, f, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private traverse_node<T>(node: Node, f: (node: Node, data: T) => T, data: T): void {
|
|
||||||
const child_data = f(node, data);
|
|
||||||
|
|
||||||
for (const child of node.children) {
|
|
||||||
this.traverse_node(child, f, child_data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Node {
|
|
||||||
readonly children: Node[] = [];
|
|
||||||
|
|
||||||
constructor(public mesh: Mesh | undefined, public transform: Transform) {}
|
|
||||||
}
|
|
@ -51,7 +51,7 @@ export class ShaderProgram {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set_transform_uniform(matrix: Mat4): void {
|
set_transform_uniform(matrix: Mat4): void {
|
||||||
this.gl.uniformMatrix4fv(this.transform_loc, true, matrix.data);
|
this.gl.uniformMatrix4fv(this.transform_loc, false, matrix.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
set_texture_uniform(unit: GLenum): void {
|
set_texture_uniform(unit: GLenum): void {
|
||||||
|
@ -27,7 +27,7 @@ export interface DisposableThreeRenderer extends THREE.Renderer, Disposable {}
|
|||||||
/**
|
/**
|
||||||
* Uses THREE.js for rendering.
|
* Uses THREE.js for rendering.
|
||||||
*/
|
*/
|
||||||
export abstract class ThreeRenderer extends Renderer {
|
export abstract class ThreeRenderer implements Renderer {
|
||||||
private _debug = false;
|
private _debug = false;
|
||||||
|
|
||||||
get debug(): boolean {
|
get debug(): boolean {
|
||||||
@ -51,7 +51,6 @@ export abstract class ThreeRenderer extends Renderer {
|
|||||||
private readonly size = new Vector2(0, 0);
|
private readonly size = new Vector2(0, 0);
|
||||||
|
|
||||||
protected constructor(three_renderer: DisposableThreeRenderer) {
|
protected constructor(three_renderer: DisposableThreeRenderer) {
|
||||||
super();
|
|
||||||
this.renderer = three_renderer;
|
this.renderer = three_renderer;
|
||||||
this.renderer.domElement.tabIndex = 0;
|
this.renderer.domElement.tabIndex = 0;
|
||||||
this.renderer.domElement.addEventListener("mousedown", this.on_mouse_down);
|
this.renderer.domElement.addEventListener("mousedown", this.on_mouse_down);
|
||||||
|
99
src/core/rendering/webgl/WebglMesh.ts
Normal file
99
src/core/rendering/webgl/WebglMesh.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { Mesh } from "../Mesh";
|
||||||
|
import {
|
||||||
|
GL,
|
||||||
|
vertex_format_size,
|
||||||
|
vertex_format_tex_offset,
|
||||||
|
VERTEX_POS_LOC,
|
||||||
|
VERTEX_TEX_LOC,
|
||||||
|
VertexFormat,
|
||||||
|
} from "../VertexFormat";
|
||||||
|
import { Texture } from "../Texture";
|
||||||
|
|
||||||
|
export class WebglMesh implements Mesh {
|
||||||
|
private vao: WebGLVertexArrayObject | null = null;
|
||||||
|
private vertex_buffer: WebGLBuffer | null = null;
|
||||||
|
private index_buffer: WebGLBuffer | null = null;
|
||||||
|
private uploaded = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly format: VertexFormat,
|
||||||
|
private readonly vertex_data: ArrayBuffer,
|
||||||
|
private readonly index_data: ArrayBuffer,
|
||||||
|
private readonly index_count: number,
|
||||||
|
readonly texture?: Texture,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
upload(gl: GL): void {
|
||||||
|
if (this.uploaded) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.vao = gl.createVertexArray();
|
||||||
|
if (this.vao == null) throw new Error("Failed to create VAO.");
|
||||||
|
|
||||||
|
this.vertex_buffer = gl.createBuffer();
|
||||||
|
if (this.vertex_buffer == null) throw new Error("Failed to create vertex buffer.");
|
||||||
|
|
||||||
|
this.index_buffer = gl.createBuffer();
|
||||||
|
if (this.index_buffer == null) throw new Error("Failed to create index buffer.");
|
||||||
|
|
||||||
|
gl.bindVertexArray(this.vao);
|
||||||
|
|
||||||
|
// Vertex data.
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertex_buffer);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, this.vertex_data, gl.STATIC_DRAW);
|
||||||
|
|
||||||
|
const vertex_size = vertex_format_size(this.format);
|
||||||
|
|
||||||
|
gl.vertexAttribPointer(VERTEX_POS_LOC, 3, gl.FLOAT, true, vertex_size, 0);
|
||||||
|
gl.enableVertexAttribArray(VERTEX_POS_LOC);
|
||||||
|
|
||||||
|
const tex_offset = vertex_format_tex_offset(this.format);
|
||||||
|
|
||||||
|
if (tex_offset !== -1) {
|
||||||
|
gl.vertexAttribPointer(
|
||||||
|
VERTEX_TEX_LOC,
|
||||||
|
2,
|
||||||
|
gl.UNSIGNED_SHORT,
|
||||||
|
true,
|
||||||
|
vertex_size,
|
||||||
|
tex_offset,
|
||||||
|
);
|
||||||
|
gl.enableVertexAttribArray(VERTEX_TEX_LOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
||||||
|
|
||||||
|
// Index data.
|
||||||
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.index_buffer);
|
||||||
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.index_data, gl.STATIC_DRAW);
|
||||||
|
|
||||||
|
gl.bindVertexArray(null);
|
||||||
|
|
||||||
|
this.texture?.upload(gl);
|
||||||
|
|
||||||
|
this.uploaded = true;
|
||||||
|
} catch (e) {
|
||||||
|
gl.deleteVertexArray(this.vao);
|
||||||
|
this.vao = null;
|
||||||
|
gl.deleteBuffer(this.vertex_buffer);
|
||||||
|
this.vertex_buffer = null;
|
||||||
|
gl.deleteBuffer(this.index_buffer);
|
||||||
|
this.index_buffer = null;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(gl: GL): void {
|
||||||
|
gl.bindVertexArray(this.vao);
|
||||||
|
|
||||||
|
gl.drawElements(gl.TRIANGLES, this.index_count, gl.UNSIGNED_SHORT, 0);
|
||||||
|
|
||||||
|
gl.bindVertexArray(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(gl: GL): void {
|
||||||
|
gl.deleteVertexArray(this.vao);
|
||||||
|
gl.deleteBuffer(this.vertex_buffer);
|
||||||
|
gl.deleteBuffer(this.index_buffer);
|
||||||
|
}
|
||||||
|
}
|
@ -1,31 +1,32 @@
|
|||||||
import { Renderer } from "./Renderer";
|
import { Mat4, mat4_product, Vec2, vec2_diff } from "../../math";
|
||||||
import { Mat4, mat4_product, Vec2, vec2_diff } from "../math";
|
import { ShaderProgram } from "../ShaderProgram";
|
||||||
import { ShaderProgram } from "./ShaderProgram";
|
import { GL, VertexFormat } from "../VertexFormat";
|
||||||
import { GL } from "./VertexFormat";
|
import { WebglScene } from "./WebglScene";
|
||||||
import { Scene } from "./Scene";
|
|
||||||
import {
|
import {
|
||||||
POS_FRAG_SHADER_SOURCE,
|
POS_FRAG_SHADER_SOURCE,
|
||||||
POS_TEX_FRAG_SHADER_SOURCE,
|
POS_TEX_FRAG_SHADER_SOURCE,
|
||||||
POS_TEX_VERTEX_SHADER_SOURCE,
|
POS_TEX_VERTEX_SHADER_SOURCE,
|
||||||
POS_VERTEX_SHADER_SOURCE,
|
POS_VERTEX_SHADER_SOURCE,
|
||||||
} from "./shader_sources";
|
} from "../shader_sources";
|
||||||
import { Camera } from "./Camera";
|
import { Camera } from "../Camera";
|
||||||
|
import { MeshBuilder } from "../MeshBuilder";
|
||||||
|
import { Texture } from "../Texture";
|
||||||
|
import { GlRenderer } from "../GlRenderer";
|
||||||
|
import { WebglMesh } from "./WebglMesh";
|
||||||
|
|
||||||
export class WebglRenderer extends Renderer {
|
export class WebglRenderer implements GlRenderer<WebglMesh> {
|
||||||
private readonly gl: GL;
|
private readonly gl: GL;
|
||||||
private readonly shader_programs: ShaderProgram[];
|
private readonly shader_programs: ShaderProgram[];
|
||||||
private animation_frame?: number;
|
private animation_frame?: number;
|
||||||
private projection!: Mat4;
|
private projection_mat!: Mat4;
|
||||||
private pointer_pos?: Vec2;
|
private pointer_pos?: Vec2;
|
||||||
|
|
||||||
protected readonly scene: Scene;
|
protected readonly scene: WebglScene;
|
||||||
protected readonly camera = new Camera();
|
protected readonly camera = new Camera();
|
||||||
|
|
||||||
readonly canvas_element: HTMLCanvasElement;
|
readonly canvas_element: HTMLCanvasElement;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
|
||||||
|
|
||||||
this.canvas_element = document.createElement("canvas");
|
this.canvas_element = document.createElement("canvas");
|
||||||
|
|
||||||
const gl = this.canvas_element.getContext("webgl2");
|
const gl = this.canvas_element.getContext("webgl2");
|
||||||
@ -45,7 +46,7 @@ export class WebglRenderer extends Renderer {
|
|||||||
new ShaderProgram(gl, POS_TEX_VERTEX_SHADER_SOURCE, POS_TEX_FRAG_SHADER_SOURCE),
|
new ShaderProgram(gl, POS_TEX_VERTEX_SHADER_SOURCE, POS_TEX_FRAG_SHADER_SOURCE),
|
||||||
];
|
];
|
||||||
|
|
||||||
this.scene = new Scene(gl);
|
this.scene = new WebglScene(gl);
|
||||||
|
|
||||||
this.set_size(800, 600);
|
this.set_size(800, 600);
|
||||||
|
|
||||||
@ -85,7 +86,7 @@ export class WebglRenderer extends Renderer {
|
|||||||
this.gl.viewport(0, 0, width, height);
|
this.gl.viewport(0, 0, width, height);
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
this.projection = Mat4.of(
|
this.projection_mat = Mat4.of(
|
||||||
2/width, 0, 0, 0,
|
2/width, 0, 0, 0,
|
||||||
0, 2/height, 0, 0,
|
0, 2/height, 0, 0,
|
||||||
0, 0, 2/10, 0,
|
0, 0, 2/10, 0,
|
||||||
@ -95,13 +96,27 @@ export class WebglRenderer extends Renderer {
|
|||||||
this.schedule_render();
|
this.schedule_render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mesh_builder(vertex_format: VertexFormat): MeshBuilder<WebglMesh> {
|
||||||
|
return new MeshBuilder(this, vertex_format);
|
||||||
|
}
|
||||||
|
|
||||||
|
mesh(
|
||||||
|
vertex_format: VertexFormat,
|
||||||
|
vertex_data: ArrayBuffer,
|
||||||
|
index_data: ArrayBuffer,
|
||||||
|
index_count: number,
|
||||||
|
texture: Texture,
|
||||||
|
): WebglMesh {
|
||||||
|
return new WebglMesh(vertex_format, vertex_data, index_data, index_count, texture);
|
||||||
|
}
|
||||||
|
|
||||||
private render = (): void => {
|
private render = (): void => {
|
||||||
this.animation_frame = undefined;
|
this.animation_frame = undefined;
|
||||||
const gl = this.gl;
|
const gl = this.gl;
|
||||||
|
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
||||||
|
|
||||||
const camera_project_mat = mat4_product(this.projection, this.camera.transform.mat4);
|
const camera_project_mat = mat4_product(this.projection_mat, this.camera.transform.mat4);
|
||||||
|
|
||||||
this.scene.traverse((node, parent_mat) => {
|
this.scene.traverse((node, parent_mat) => {
|
||||||
const mat = mat4_product(parent_mat, node.transform.mat4);
|
const mat = mat4_product(parent_mat, node.transform.mat4);
|
65
src/core/rendering/webgl/WebglScene.ts
Normal file
65
src/core/rendering/webgl/WebglScene.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { IdentityTransform, Transform } from "../Transform";
|
||||||
|
import { GL } from "../VertexFormat";
|
||||||
|
import { WebglMesh } from "./WebglMesh";
|
||||||
|
|
||||||
|
export class WebglScene {
|
||||||
|
readonly root_node = new WebglNode(this, undefined, new IdentityTransform());
|
||||||
|
|
||||||
|
constructor(private readonly gl: GL) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all GL objects related to this scene and resets the scene.
|
||||||
|
*/
|
||||||
|
delete(): void {
|
||||||
|
this.traverse(node => {
|
||||||
|
node.mesh?.texture?.delete(this.gl);
|
||||||
|
node.mesh?.delete(this.gl);
|
||||||
|
node.mesh = undefined;
|
||||||
|
}, undefined);
|
||||||
|
|
||||||
|
this.root_node.clear_children();
|
||||||
|
this.root_node.transform = new IdentityTransform();
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse<T>(f: (node: WebglNode, data: T) => T, data: T): void {
|
||||||
|
this.traverse_node(this.root_node, f, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
upload(mesh: WebglMesh): void {
|
||||||
|
mesh.upload(this.gl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private traverse_node<T>(node: WebglNode, f: (node: WebglNode, data: T) => T, data: T): void {
|
||||||
|
const child_data = f(node, data);
|
||||||
|
|
||||||
|
for (const child of node.children) {
|
||||||
|
this.traverse_node(child, f, child_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebglNode {
|
||||||
|
private readonly _children: WebglNode[] = [];
|
||||||
|
|
||||||
|
get children(): readonly WebglNode[] {
|
||||||
|
return this._children;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly scene: WebglScene,
|
||||||
|
public mesh: WebglMesh | undefined,
|
||||||
|
public transform: Transform,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
add_child(mesh: WebglMesh | undefined, transform: Transform): void {
|
||||||
|
this._children.push(new WebglNode(this.scene, mesh, transform));
|
||||||
|
|
||||||
|
if (mesh) {
|
||||||
|
this.scene.upload(mesh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_children(): void {
|
||||||
|
this._children.splice(0);
|
||||||
|
}
|
||||||
|
}
|
72
src/core/rendering/webgpu/WebgpuMesh.ts
Normal file
72
src/core/rendering/webgpu/WebgpuMesh.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { Mesh } from "../Mesh";
|
||||||
|
import { Texture } from "../Texture";
|
||||||
|
import { VertexFormat } from "../VertexFormat";
|
||||||
|
import { defined } from "../../util";
|
||||||
|
import { Mat4 } from "../../math";
|
||||||
|
|
||||||
|
export class WebgpuMesh implements Mesh {
|
||||||
|
private uniform_buffer?: GPUBuffer;
|
||||||
|
private bind_group?: GPUBindGroup;
|
||||||
|
private vertex_buffer?: GPUBuffer;
|
||||||
|
private index_buffer?: GPUBuffer;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly format: VertexFormat,
|
||||||
|
private readonly vertex_data: ArrayBuffer,
|
||||||
|
private readonly index_data: ArrayBuffer,
|
||||||
|
private readonly index_count: number,
|
||||||
|
readonly texture?: Texture,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
upload(device: GPUDevice, bind_group_layout: GPUBindGroupLayout): void {
|
||||||
|
this.uniform_buffer = device.createBuffer({
|
||||||
|
size: 4 * 16,
|
||||||
|
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, // eslint-disable-line no-undef
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bind_group = device.createBindGroup({
|
||||||
|
layout: bind_group_layout,
|
||||||
|
bindings: [
|
||||||
|
{
|
||||||
|
binding: 0,
|
||||||
|
resource: {
|
||||||
|
buffer: this.uniform_buffer,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.vertex_buffer = device.createBuffer({
|
||||||
|
size: this.vertex_data.byteLength,
|
||||||
|
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, // eslint-disable-line no-undef
|
||||||
|
});
|
||||||
|
|
||||||
|
this.vertex_buffer.setSubData(0, new Uint8Array(this.vertex_data));
|
||||||
|
|
||||||
|
this.index_buffer = device.createBuffer({
|
||||||
|
size: this.index_data.byteLength,
|
||||||
|
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.INDEX, // eslint-disable-line no-undef
|
||||||
|
});
|
||||||
|
|
||||||
|
this.index_buffer.setSubData(0, new Uint16Array(this.index_data));
|
||||||
|
}
|
||||||
|
|
||||||
|
render(pass_encoder: GPURenderPassEncoder, mat: Mat4): void {
|
||||||
|
defined(this.uniform_buffer, "uniform_buffer");
|
||||||
|
defined(this.bind_group, "bind_group");
|
||||||
|
defined(this.vertex_buffer, "vertex_buffer");
|
||||||
|
defined(this.index_buffer, "index_buffer");
|
||||||
|
|
||||||
|
this.uniform_buffer.setSubData(0, mat.data);
|
||||||
|
pass_encoder.setBindGroup(0, this.bind_group);
|
||||||
|
pass_encoder.setVertexBuffer(0, this.vertex_buffer);
|
||||||
|
pass_encoder.setIndexBuffer(this.index_buffer);
|
||||||
|
pass_encoder.drawIndexed(this.index_count, 1, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.uniform_buffer?.destroy();
|
||||||
|
this.vertex_buffer?.destroy();
|
||||||
|
this.index_buffer?.destroy();
|
||||||
|
}
|
||||||
|
}
|
324
src/core/rendering/webgpu/WebgpuRenderer.ts
Normal file
324
src/core/rendering/webgpu/WebgpuRenderer.ts
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
import { LogManager } from "../../Logger";
|
||||||
|
import { MeshBuilder } from "../MeshBuilder";
|
||||||
|
import { vertex_format_size, VertexFormat } from "../VertexFormat";
|
||||||
|
import { Texture } from "../Texture";
|
||||||
|
import { GlRenderer } from "../GlRenderer";
|
||||||
|
import { WebgpuMesh } from "./WebgpuMesh";
|
||||||
|
import { WebgpuScene } from "./WebgpuScene";
|
||||||
|
import { Camera } from "../Camera";
|
||||||
|
import { Disposable } from "../../observable/Disposable";
|
||||||
|
import { Mat4, mat4_product, Vec2, vec2_diff } from "../../math";
|
||||||
|
import { IdentityTransform } from "../Transform";
|
||||||
|
|
||||||
|
const logger = LogManager.get("core/rendering/webgpu/WebgpuRenderer");
|
||||||
|
|
||||||
|
const VERTEX_SHADER_SOURCE = `#version 450
|
||||||
|
|
||||||
|
layout(set = 0, binding = 0) uniform Uniforms {
|
||||||
|
mat4 mvp_mat;
|
||||||
|
} uniforms;
|
||||||
|
|
||||||
|
layout(location = 0) in vec3 pos;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_Position = uniforms.mvp_mat * vec4(pos, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FRAG_SHADER_SOURCE = `#version 450
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 out_color;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
out_color = vec4(0.0, 0.4, 0.8, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses the experimental WebGPU API for rendering.
|
||||||
|
*/
|
||||||
|
export class WebgpuRenderer implements GlRenderer<WebgpuMesh> {
|
||||||
|
private disposed: boolean = false;
|
||||||
|
/**
|
||||||
|
* Is defined when an animation frame is scheduled.
|
||||||
|
*/
|
||||||
|
private animation_frame?: number;
|
||||||
|
/**
|
||||||
|
* Is defined when the renderer is fully initialized.
|
||||||
|
*/
|
||||||
|
private renderer?: InitializedRenderer;
|
||||||
|
private width = 800;
|
||||||
|
private height = 600;
|
||||||
|
private pointer_pos?: Vec2;
|
||||||
|
|
||||||
|
protected scene?: WebgpuScene;
|
||||||
|
protected readonly camera = new Camera();
|
||||||
|
|
||||||
|
readonly canvas_element: HTMLCanvasElement = document.createElement("canvas");
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.canvas_element.width = this.width;
|
||||||
|
this.canvas_element.height = this.height;
|
||||||
|
|
||||||
|
this.canvas_element.addEventListener("mousedown", this.mousedown);
|
||||||
|
this.canvas_element.addEventListener("wheel", this.wheel, { passive: true });
|
||||||
|
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initialize(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (window.navigator.gpu == undefined) {
|
||||||
|
logger.error("WebGPU not supported on this device.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = this.canvas_element.getContext("gpupresent") as GPUCanvasContext | null;
|
||||||
|
|
||||||
|
if (context == null) {
|
||||||
|
logger.error("Failed to initialize gpupresent context.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const adapter = await window.navigator.gpu.requestAdapter();
|
||||||
|
const device = await adapter.requestDevice();
|
||||||
|
const glslang_module = await import(
|
||||||
|
// @ts-ignore
|
||||||
|
/* webpackIgnore: true */ "https://unpkg.com/@webgpu/glslang@0.0.7/web/glslang.js"
|
||||||
|
);
|
||||||
|
const glslang = await glslang_module.default();
|
||||||
|
|
||||||
|
if (!this.disposed) {
|
||||||
|
this.renderer = new InitializedRenderer(
|
||||||
|
this.canvas_element,
|
||||||
|
context,
|
||||||
|
device,
|
||||||
|
glslang,
|
||||||
|
this.camera,
|
||||||
|
);
|
||||||
|
this.renderer.set_size(this.width, this.height);
|
||||||
|
|
||||||
|
this.scene = this.renderer.scene;
|
||||||
|
|
||||||
|
this.scene.root_node.add_child(
|
||||||
|
this.mesh_builder(VertexFormat.Pos)
|
||||||
|
.vertex(1, 1, 0.5)
|
||||||
|
.vertex(-1, 1, 0.5)
|
||||||
|
.vertex(-1, -1, 0.5)
|
||||||
|
.vertex(1, -1, 0.5)
|
||||||
|
.triangle(0, 1, 2)
|
||||||
|
.triangle(0, 2, 3)
|
||||||
|
.build(),
|
||||||
|
new IdentityTransform(),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.schedule_render();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Failed to initialize WebGPU renderer.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this.disposed = true;
|
||||||
|
this.renderer?.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
start_rendering(): void {
|
||||||
|
this.schedule_render();
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_rendering(): void {
|
||||||
|
if (this.animation_frame != undefined) {
|
||||||
|
cancelAnimationFrame(this.animation_frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animation_frame = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule_render = (): void => {
|
||||||
|
if (this.animation_frame == undefined) {
|
||||||
|
this.animation_frame = requestAnimationFrame(this.render);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
set_size(width: number, height: number): void {
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.renderer?.set_size(width, height);
|
||||||
|
this.schedule_render();
|
||||||
|
}
|
||||||
|
|
||||||
|
mesh_builder(vertex_format: VertexFormat): MeshBuilder<WebgpuMesh> {
|
||||||
|
return new MeshBuilder(this, vertex_format);
|
||||||
|
}
|
||||||
|
|
||||||
|
mesh(
|
||||||
|
vertex_format: VertexFormat,
|
||||||
|
vertex_data: ArrayBuffer,
|
||||||
|
index_data: ArrayBuffer,
|
||||||
|
index_count: number,
|
||||||
|
texture?: Texture,
|
||||||
|
): WebgpuMesh {
|
||||||
|
return new WebgpuMesh(vertex_format, vertex_data, index_data, index_count, texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private render = (): void => {
|
||||||
|
this.animation_frame = undefined;
|
||||||
|
this.renderer?.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
private mousedown = (evt: MouseEvent): void => {
|
||||||
|
if (evt.buttons === 1) {
|
||||||
|
this.pointer_pos = new Vec2(evt.clientX, evt.clientY);
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", this.mousemove);
|
||||||
|
window.addEventListener("mouseup", this.mouseup);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private mousemove = (evt: MouseEvent): void => {
|
||||||
|
if (evt.buttons === 1) {
|
||||||
|
const new_pos = new Vec2(evt.clientX, evt.clientY);
|
||||||
|
const diff = vec2_diff(new_pos, this.pointer_pos!);
|
||||||
|
this.camera.pan(-diff.x, diff.y, 0);
|
||||||
|
this.pointer_pos = new_pos;
|
||||||
|
this.schedule_render();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private mouseup = (): void => {
|
||||||
|
this.pointer_pos = undefined;
|
||||||
|
|
||||||
|
window.removeEventListener("mousemove", this.mousemove);
|
||||||
|
window.removeEventListener("mouseup", this.mouseup);
|
||||||
|
};
|
||||||
|
|
||||||
|
private wheel = (evt: WheelEvent): void => {
|
||||||
|
if (evt.deltaY < 0) {
|
||||||
|
this.camera.zoom(1.1);
|
||||||
|
} else {
|
||||||
|
this.camera.zoom(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.schedule_render();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class InitializedRenderer implements Disposable {
|
||||||
|
private readonly swap_chain: GPUSwapChain;
|
||||||
|
private readonly pipeline: GPURenderPipeline;
|
||||||
|
private projection_mat: Mat4 = Mat4.identity();
|
||||||
|
|
||||||
|
readonly scene: WebgpuScene;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly canvas_element: HTMLCanvasElement,
|
||||||
|
private readonly context: GPUCanvasContext,
|
||||||
|
private readonly device: GPUDevice,
|
||||||
|
private readonly glslang: any,
|
||||||
|
private readonly camera: Camera,
|
||||||
|
) {
|
||||||
|
const swap_chain_format = "bgra8unorm";
|
||||||
|
|
||||||
|
this.swap_chain = context.configureSwapChain({
|
||||||
|
device: device,
|
||||||
|
format: swap_chain_format,
|
||||||
|
});
|
||||||
|
|
||||||
|
const bind_group_layout = device.createBindGroupLayout({
|
||||||
|
bindings: [
|
||||||
|
{
|
||||||
|
binding: 0,
|
||||||
|
visibility: GPUShaderStage.VERTEX, // eslint-disable-line no-undef
|
||||||
|
type: "uniform-buffer",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pipeline = device.createRenderPipeline({
|
||||||
|
layout: device.createPipelineLayout({ bindGroupLayouts: [bind_group_layout] }),
|
||||||
|
vertexStage: {
|
||||||
|
module: device.createShaderModule({
|
||||||
|
code: glslang.compileGLSL(VERTEX_SHADER_SOURCE, "vertex", true),
|
||||||
|
}),
|
||||||
|
entryPoint: "main",
|
||||||
|
},
|
||||||
|
fragmentStage: {
|
||||||
|
module: device.createShaderModule({
|
||||||
|
code: glslang.compileGLSL(FRAG_SHADER_SOURCE, "fragment", true),
|
||||||
|
}),
|
||||||
|
entryPoint: "main",
|
||||||
|
},
|
||||||
|
primitiveTopology: "triangle-list",
|
||||||
|
colorStates: [{ format: swap_chain_format }],
|
||||||
|
vertexState: {
|
||||||
|
indexFormat: "uint16",
|
||||||
|
vertexBuffers: [
|
||||||
|
{
|
||||||
|
arrayStride: vertex_format_size(VertexFormat.Pos),
|
||||||
|
stepMode: "vertex",
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
format: "float3",
|
||||||
|
offset: 0,
|
||||||
|
shaderLocation: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.scene = new WebgpuScene(device, bind_group_layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
set_size(width: number, height: number): void {
|
||||||
|
this.canvas_element.width = width;
|
||||||
|
this.canvas_element.height = height;
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
this.projection_mat = Mat4.of(
|
||||||
|
2/width, 0, 0, 0,
|
||||||
|
0, 2/height, 0, 0,
|
||||||
|
0, 0, 2/10, 0,
|
||||||
|
0, 0, 0, 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): void {
|
||||||
|
const command_encoder = this.device.createCommandEncoder();
|
||||||
|
const texture_view = this.swap_chain.getCurrentTexture().createView();
|
||||||
|
|
||||||
|
const pass_encoder = command_encoder.beginRenderPass({
|
||||||
|
colorAttachments: [
|
||||||
|
{
|
||||||
|
attachment: texture_view,
|
||||||
|
loadValue: { r: 0.1, g: 0.1, b: 0.1, a: 1 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
pass_encoder.setPipeline(this.pipeline);
|
||||||
|
|
||||||
|
const camera_project_mat = mat4_product(this.projection_mat, this.camera.transform.mat4);
|
||||||
|
|
||||||
|
this.scene.traverse((node, parent_mat) => {
|
||||||
|
const mat = mat4_product(parent_mat, node.transform.mat4);
|
||||||
|
|
||||||
|
if (node.mesh) {
|
||||||
|
node.mesh.render(pass_encoder, mat);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mat;
|
||||||
|
}, camera_project_mat);
|
||||||
|
|
||||||
|
pass_encoder.endPass();
|
||||||
|
|
||||||
|
this.device.defaultQueue.submit([command_encoder.finish()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this.scene.destroy();
|
||||||
|
}
|
||||||
|
}
|
67
src/core/rendering/webgpu/WebgpuScene.ts
Normal file
67
src/core/rendering/webgpu/WebgpuScene.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { IdentityTransform, Transform } from "../Transform";
|
||||||
|
import { WebgpuMesh } from "./WebgpuMesh";
|
||||||
|
|
||||||
|
export class WebgpuScene {
|
||||||
|
readonly root_node = new WebgpuNode(this, undefined, new IdentityTransform());
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly device: GPUDevice,
|
||||||
|
private readonly bind_group_layout: GPUBindGroupLayout,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroys all WebGPU objects related to this scene and resets the scene.
|
||||||
|
*/
|
||||||
|
destroy(): void {
|
||||||
|
this.traverse(node => {
|
||||||
|
// node.mesh?.texture?.delete(this.gl);
|
||||||
|
node.mesh?.destroy();
|
||||||
|
node.mesh = undefined;
|
||||||
|
}, undefined);
|
||||||
|
|
||||||
|
this.root_node.clear_children();
|
||||||
|
this.root_node.transform = new IdentityTransform();
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse<T>(f: (node: WebgpuNode, data: T) => T, data: T): void {
|
||||||
|
this.traverse_node(this.root_node, f, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
upload(mesh: WebgpuMesh): void {
|
||||||
|
mesh.upload(this.device, this.bind_group_layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private traverse_node<T>(node: WebgpuNode, f: (node: WebgpuNode, data: T) => T, data: T): void {
|
||||||
|
const child_data = f(node, data);
|
||||||
|
|
||||||
|
for (const child of node.children) {
|
||||||
|
this.traverse_node(child, f, child_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WebgpuNode {
|
||||||
|
private readonly _children: WebgpuNode[] = [];
|
||||||
|
|
||||||
|
get children(): readonly WebgpuNode[] {
|
||||||
|
return this._children;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly scene: WebgpuScene,
|
||||||
|
public mesh: WebgpuMesh | undefined,
|
||||||
|
public transform: Transform,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
add_child(mesh: WebgpuMesh | undefined, transform: Transform): void {
|
||||||
|
this._children.push(new WebgpuNode(this.scene, mesh, transform));
|
||||||
|
|
||||||
|
if (mesh) {
|
||||||
|
this.scene.upload(mesh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_children(): void {
|
||||||
|
this._children.splice(0);
|
||||||
|
}
|
||||||
|
}
|
@ -59,7 +59,10 @@ export function initialize_viewer(
|
|||||||
|
|
||||||
let renderer: Renderer;
|
let renderer: Renderer;
|
||||||
|
|
||||||
if (gui_store.feature_active("renderer")) {
|
if (gui_store.feature_active("webgpu")) {
|
||||||
|
const { TextureWebgpuRenderer } = await import("./rendering/TextureWebgpuRenderer");
|
||||||
|
renderer = new TextureWebgpuRenderer(controller);
|
||||||
|
} else if (gui_store.feature_active("webgl")) {
|
||||||
const { TextureWebglRenderer } = await import("./rendering/TextureWebglRenderer");
|
const { TextureWebglRenderer } = await import("./rendering/TextureWebglRenderer");
|
||||||
renderer = new TextureWebglRenderer(controller);
|
renderer = new TextureWebglRenderer(controller);
|
||||||
} else {
|
} else {
|
||||||
|
16
src/viewer/rendering/ModelWebglRenderer.ts
Normal file
16
src/viewer/rendering/ModelWebglRenderer.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { WebglRenderer } from "../../core/rendering/webgl/WebglRenderer";
|
||||||
|
import { ModelStore } from "../stores/ModelStore";
|
||||||
|
import { Disposer } from "../../core/observable/Disposer";
|
||||||
|
|
||||||
|
export class ModelWebglRenderer extends WebglRenderer {
|
||||||
|
private readonly disposer = new Disposer();
|
||||||
|
|
||||||
|
constructor(private readonly store: ModelStore) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
super.dispose();
|
||||||
|
this.disposer.dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,14 @@
|
|||||||
import { Disposer } from "../../core/observable/Disposer";
|
import { Disposer } from "../../core/observable/Disposer";
|
||||||
import { LogManager } from "../../core/Logger";
|
import { LogManager } from "../../core/Logger";
|
||||||
import { TextureController } from "../controllers/TextureController";
|
import { TextureController } from "../controllers/TextureController";
|
||||||
import { WebglRenderer } from "../../core/rendering/WebglRenderer";
|
import { WebglRenderer } from "../../core/rendering/webgl/WebglRenderer";
|
||||||
import { XvrTexture } from "../../core/data_formats/parsing/ninja/texture";
|
import { XvrTexture } from "../../core/data_formats/parsing/ninja/texture";
|
||||||
import { Mesh, MeshBuilder } from "../../core/rendering/Mesh";
|
|
||||||
import { TranslateTransform } from "../../core/rendering/Transform";
|
import { TranslateTransform } from "../../core/rendering/Transform";
|
||||||
import { VertexFormat } from "../../core/rendering/VertexFormat";
|
import { VertexFormat } from "../../core/rendering/VertexFormat";
|
||||||
import { Texture, TextureFormat } from "../../core/rendering/Texture";
|
import { Texture, TextureFormat } from "../../core/rendering/Texture";
|
||||||
|
import { WebglMesh } from "../../core/rendering/webgl/WebglMesh";
|
||||||
|
|
||||||
const logger = LogManager.get("viewer/rendering/WebglTextureRenderer");
|
const logger = LogManager.get("viewer/rendering/TextureWebglRenderer");
|
||||||
|
|
||||||
export class TextureWebglRenderer extends WebglRenderer {
|
export class TextureWebglRenderer extends WebglRenderer {
|
||||||
private readonly disposer = new Disposer();
|
private readonly disposer = new Disposer();
|
||||||
@ -47,8 +47,7 @@ export class TextureWebglRenderer extends WebglRenderer {
|
|||||||
try {
|
try {
|
||||||
const quad_mesh = this.create_quad(tex);
|
const quad_mesh = this.create_quad(tex);
|
||||||
|
|
||||||
this.scene.add_child(
|
this.scene.root_node.add_child(
|
||||||
this.scene.root_node,
|
|
||||||
quad_mesh,
|
quad_mesh,
|
||||||
new TranslateTransform(x, y + (total_height - tex.height) / 2, 0),
|
new TranslateTransform(x, y + (total_height - tex.height) / 2, 0),
|
||||||
);
|
);
|
||||||
@ -60,8 +59,8 @@ export class TextureWebglRenderer extends WebglRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private create_quad(tex: XvrTexture): Mesh {
|
private create_quad(tex: XvrTexture): WebglMesh {
|
||||||
return new MeshBuilder(VertexFormat.PosTex)
|
return this.mesh_builder(VertexFormat.PosTex)
|
||||||
.vertex(0, 0, 0, 0, 1)
|
.vertex(0, 0, 0, 0, 1)
|
||||||
.vertex(tex.width, 0, 0, 1, 1)
|
.vertex(tex.width, 0, 0, 1, 1)
|
||||||
.vertex(tex.width, tex.height, 0, 1, 0)
|
.vertex(tex.width, tex.height, 0, 1, 0)
|
||||||
@ -76,7 +75,7 @@ export class TextureWebglRenderer extends WebglRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function xvr_texture_to_texture(tex: XvrTexture): Texture {
|
function xvr_texture_to_texture(tex: XvrTexture): Texture {
|
||||||
let format: TextureFormat;
|
let format: TextureFormat;
|
||||||
let data_size: number;
|
let data_size: number;
|
||||||
|
|
||||||
|
97
src/viewer/rendering/TextureWebgpuRenderer.ts
Normal file
97
src/viewer/rendering/TextureWebgpuRenderer.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { Disposer } from "../../core/observable/Disposer";
|
||||||
|
import { LogManager } from "../../core/Logger";
|
||||||
|
import { TextureController } from "../controllers/TextureController";
|
||||||
|
import { XvrTexture } from "../../core/data_formats/parsing/ninja/texture";
|
||||||
|
import { VertexFormat } from "../../core/rendering/VertexFormat";
|
||||||
|
import { Texture, TextureFormat } from "../../core/rendering/Texture";
|
||||||
|
import { WebgpuRenderer } from "../../core/rendering/webgpu/WebgpuRenderer";
|
||||||
|
import { WebgpuMesh } from "../../core/rendering/webgpu/WebgpuMesh";
|
||||||
|
import { TranslateTransform } from "../../core/rendering/Transform";
|
||||||
|
|
||||||
|
const logger = LogManager.get("viewer/rendering/webgpu/TextureWebgpuRenderer");
|
||||||
|
|
||||||
|
export class TextureWebgpuRenderer extends WebgpuRenderer {
|
||||||
|
private readonly disposer = new Disposer();
|
||||||
|
|
||||||
|
constructor(ctrl: TextureController) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.disposer.add_all(
|
||||||
|
ctrl.textures.observe(({ value: textures }) => {
|
||||||
|
this.scene?.destroy();
|
||||||
|
this.camera.reset();
|
||||||
|
this.create_quads(textures);
|
||||||
|
this.schedule_render();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
super.dispose();
|
||||||
|
this.disposer.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private create_quads(textures: readonly XvrTexture[]): void {
|
||||||
|
let total_width = 10 * (textures.length - 1); // 10px spacing between textures.
|
||||||
|
let total_height = 0;
|
||||||
|
|
||||||
|
for (const tex of textures) {
|
||||||
|
total_width += tex.width;
|
||||||
|
total_height = Math.max(total_height, tex.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = -Math.floor(total_width / 2);
|
||||||
|
const y = -Math.floor(total_height / 2);
|
||||||
|
|
||||||
|
for (const tex of textures) {
|
||||||
|
try {
|
||||||
|
const quad_mesh = this.create_quad(tex);
|
||||||
|
|
||||||
|
this.scene?.root_node.add_child(
|
||||||
|
quad_mesh,
|
||||||
|
new TranslateTransform(x, y + (total_height - tex.height) / 2, 0),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Couldn't create quad for texture.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
x += 10 + tex.width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private create_quad(tex: XvrTexture): WebgpuMesh {
|
||||||
|
return this.mesh_builder(VertexFormat.Pos)
|
||||||
|
.vertex(0, 0, 0)
|
||||||
|
.vertex(tex.width, 0, 0)
|
||||||
|
.vertex(tex.width, tex.height, 0)
|
||||||
|
.vertex(0, tex.height, 0)
|
||||||
|
|
||||||
|
.triangle(0, 1, 2)
|
||||||
|
.triangle(2, 3, 0)
|
||||||
|
|
||||||
|
.texture(xvr_texture_to_texture(tex))
|
||||||
|
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function xvr_texture_to_texture(tex: XvrTexture): Texture {
|
||||||
|
let format: TextureFormat;
|
||||||
|
let data_size: number;
|
||||||
|
|
||||||
|
// Ignore mipmaps.
|
||||||
|
switch (tex.format[1]) {
|
||||||
|
case 6:
|
||||||
|
format = TextureFormat.RGBA_S3TC_DXT1;
|
||||||
|
data_size = (tex.width * tex.height) / 2;
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
format = TextureFormat.RGBA_S3TC_DXT3;
|
||||||
|
data_size = tex.width * tex.height;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Format ${tex.format.join(", ")} not supported.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Texture(tex.width, tex.height, format, tex.data.slice(0, data_size));
|
||||||
|
}
|
@ -5,6 +5,7 @@
|
|||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"lib": ["esnext", "dom", "dom.iterable"],
|
"lib": ["esnext", "dom", "dom.iterable"],
|
||||||
|
"typeRoots": ["node_modules/@types", "node_modules/@webgpu"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -675,6 +675,16 @@
|
|||||||
"@webassemblyjs/wast-parser" "1.8.5"
|
"@webassemblyjs/wast-parser" "1.8.5"
|
||||||
"@xtuc/long" "4.2.2"
|
"@xtuc/long" "4.2.2"
|
||||||
|
|
||||||
|
"@webgpu/glslang@^0.0.12":
|
||||||
|
version "0.0.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/@webgpu/glslang/-/glslang-0.0.12.tgz#ee40e8d38c31436508147feaf0f646fde9bb4da6"
|
||||||
|
integrity sha512-GfEVo1GUxNfXjO4Z7I06XXYJA45N6sHoKqI5Ptf3vafnPowq2C1woCqVe7frsClMfBB2yLn1vJss2oWl5EdTig==
|
||||||
|
|
||||||
|
"@webgpu/types@^0.0.21":
|
||||||
|
version "0.0.21"
|
||||||
|
resolved "https://registry.yarnpkg.com/@webgpu/types/-/types-0.0.21.tgz#ada3f2a984a10ffb8579564aef079928005f44ee"
|
||||||
|
integrity sha512-fqIYQ9PybboEFUFV3iup7TRWkuPBZXzBCWbTbowyMfZb8Pt6zlg4T58tm4/WQgtN3KwuQoAuM64M7SUfW8+3ng==
|
||||||
|
|
||||||
"@xtuc/ieee754@^1.2.0":
|
"@xtuc/ieee754@^1.2.0":
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
|
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
|
||||||
|
Loading…
Reference in New Issue
Block a user