From baffab3234ef9365f40bbaead5bf2505dcaacccb Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Thu, 23 Jan 2020 01:16:52 +0100 Subject: [PATCH] Started work on WebGPU renderer. --- .eslintrc.json | 1 + package.json | 2 + src/core/math/index.ts | 20 +- src/core/rendering/Camera.ts | 6 +- src/core/rendering/GlRenderer.ts | 17 + src/core/rendering/Mesh.ts | 163 +-------- src/core/rendering/MeshBuilder.ts | 78 +++++ src/core/rendering/Renderer.ts | 12 +- src/core/rendering/Scene.ts | 54 --- src/core/rendering/ShaderProgram.ts | 2 +- src/core/rendering/ThreeRenderer.ts | 3 +- src/core/rendering/webgl/WebglMesh.ts | 99 ++++++ .../rendering/{ => webgl}/WebglRenderer.ts | 45 ++- src/core/rendering/webgl/WebglScene.ts | 65 ++++ src/core/rendering/webgpu/WebgpuMesh.ts | 72 ++++ src/core/rendering/webgpu/WebgpuRenderer.ts | 324 ++++++++++++++++++ src/core/rendering/webgpu/WebgpuScene.ts | 67 ++++ src/viewer/index.ts | 5 +- src/viewer/rendering/ModelWebglRenderer.ts | 16 + src/viewer/rendering/TextureWebglRenderer.ts | 15 +- src/viewer/rendering/TextureWebgpuRenderer.ts | 97 ++++++ tsconfig.json | 1 + yarn.lock | 10 + 23 files changed, 921 insertions(+), 253 deletions(-) create mode 100644 src/core/rendering/GlRenderer.ts create mode 100644 src/core/rendering/MeshBuilder.ts delete mode 100644 src/core/rendering/Scene.ts create mode 100644 src/core/rendering/webgl/WebglMesh.ts rename src/core/rendering/{ => webgl}/WebglRenderer.ts (78%) create mode 100644 src/core/rendering/webgl/WebglScene.ts create mode 100644 src/core/rendering/webgpu/WebgpuMesh.ts create mode 100644 src/core/rendering/webgpu/WebgpuRenderer.ts create mode 100644 src/core/rendering/webgpu/WebgpuScene.ts create mode 100644 src/viewer/rendering/ModelWebglRenderer.ts create mode 100644 src/viewer/rendering/TextureWebgpuRenderer.ts diff --git a/.eslintrc.json b/.eslintrc.json index dd25e09c..a9cd7ec4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -17,6 +17,7 @@ "ignorePatterns": ["webpack.*.js"], "rules": { "@typescript-eslint/array-type": ["warn", { "default": "array", "readonly": "array" }], + "@typescript-eslint/ban-ts-ignore": "off", "@typescript-eslint/camelcase": "off", "@typescript-eslint/class-name-casing": "warn", "@typescript-eslint/explicit-function-return-type": ["warn", { "allowExpressions": true }], diff --git a/package.json b/package.json index 1346411f..1fc6c55f 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "license": "MIT", "dependencies": { + "@webgpu/glslang": "^0.0.12", "camera-controls": "^1.16.2", "core-js": "^3.6.1", "golden-layout": "^1.5.9", @@ -31,6 +32,7 @@ "@types/yaml": "^1.2.0", "@typescript-eslint/eslint-plugin": "^2.12.0", "@typescript-eslint/parser": "^2.12.0", + "@webgpu/types": "^0.0.21", "cheerio": "^1.0.0-rc.3", "clean-webpack-plugin": "^3.0.0", "copy-webpack-plugin": "^5.1.1", diff --git a/src/core/math/index.ts b/src/core/math/index.ts index 32cfc163..8d12b245 100644 --- a/src/core/math/index.ts +++ b/src/core/math/index.ts @@ -37,9 +37,23 @@ export class Vec3 { constructor(public x: number, public y: number, public z: number) {} } +/** + * Stores data in column-major order. + */ export class Mat4 { - static of(...values: readonly number[]): Mat4 { - return new Mat4(new Float32Array(values)); + // prettier-ignore + 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 { @@ -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 j = 0; j < 4; j++) { 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]; } } } diff --git a/src/core/rendering/Camera.ts b/src/core/rendering/Camera.ts index 63ca5969..35b48c01 100644 --- a/src/core/rendering/Camera.ts +++ b/src/core/rendering/Camera.ts @@ -46,9 +46,9 @@ export class Camera { } private update_transform(): void { - this._transform.data[3] = -this.look_at.x; - this._transform.data[7] = -this.look_at.y; - this._transform.data[11] = -this.look_at.z; + this._transform.data[12] = -this.look_at.x; + this._transform.data[13] = -this.look_at.y; + this._transform.data[14] = -this.look_at.z; this._transform.data[0] = this._zoom; this._transform.data[5] = this._zoom; this._transform.data[10] = this._zoom; diff --git a/src/core/rendering/GlRenderer.ts b/src/core/rendering/GlRenderer.ts new file mode 100644 index 00000000..367fb5e7 --- /dev/null +++ b/src/core/rendering/GlRenderer.ts @@ -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 extends Renderer { + mesh_builder(vertex_format: VertexFormat): MeshBuilder; + + mesh( + vertex_format: VertexFormat, + vertex_data: ArrayBuffer, + index_data: ArrayBuffer, + index_count: number, + texture?: Texture, + ): MeshType; +} diff --git a/src/core/rendering/Mesh.ts b/src/core/rendering/Mesh.ts index 755eb008..d57d7f9f 100644 --- a/src/core/rendering/Mesh.ts +++ b/src/core/rendering/Mesh.ts @@ -1,162 +1,7 @@ -import { - GL, - vertex_format_size, - vertex_format_tex_offset, - VERTEX_POS_LOC, - VERTEX_TEX_LOC, - VertexFormat, -} from "./VertexFormat"; -import { assert } from "../util"; +import { VertexFormat } from "./VertexFormat"; import { Texture } from "./Texture"; -export class Mesh { - private readonly index_count: number; - 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, - 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); - } +export interface Mesh { + readonly format: VertexFormat; + readonly texture?: Texture; } diff --git a/src/core/rendering/MeshBuilder.ts b/src/core/rendering/MeshBuilder.ts new file mode 100644 index 00000000..7216bffd --- /dev/null +++ b/src/core/rendering/MeshBuilder.ts @@ -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 { + 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, + 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, + ); + } +} diff --git a/src/core/rendering/Renderer.ts b/src/core/rendering/Renderer.ts index 42c3eb8d..76ed3ae4 100644 --- a/src/core/rendering/Renderer.ts +++ b/src/core/rendering/Renderer.ts @@ -1,13 +1,11 @@ import { Disposable } from "../observable/Disposable"; -export abstract class Renderer implements Disposable { - abstract readonly canvas_element: HTMLCanvasElement; +export interface Renderer extends Disposable { + readonly canvas_element: HTMLCanvasElement; - abstract dispose(): void; + start_rendering(): void; - abstract start_rendering(): void; + stop_rendering(): void; - abstract stop_rendering(): void; - - abstract set_size(width: number, height: number): void; + set_size(width: number, height: number): void; } diff --git a/src/core/rendering/Scene.ts b/src/core/rendering/Scene.ts deleted file mode 100644 index 70300727..00000000 --- a/src/core/rendering/Scene.ts +++ /dev/null @@ -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(f: (node: Node, data: T) => T, data: T): void { - this.traverse_node(this.root_node, f, data); - } - - private traverse_node(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) {} -} diff --git a/src/core/rendering/ShaderProgram.ts b/src/core/rendering/ShaderProgram.ts index b12ddee2..017652fa 100644 --- a/src/core/rendering/ShaderProgram.ts +++ b/src/core/rendering/ShaderProgram.ts @@ -51,7 +51,7 @@ export class ShaderProgram { } 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 { diff --git a/src/core/rendering/ThreeRenderer.ts b/src/core/rendering/ThreeRenderer.ts index 64358b7c..8fba6fe2 100644 --- a/src/core/rendering/ThreeRenderer.ts +++ b/src/core/rendering/ThreeRenderer.ts @@ -27,7 +27,7 @@ export interface DisposableThreeRenderer extends THREE.Renderer, Disposable {} /** * Uses THREE.js for rendering. */ -export abstract class ThreeRenderer extends Renderer { +export abstract class ThreeRenderer implements Renderer { private _debug = false; get debug(): boolean { @@ -51,7 +51,6 @@ export abstract class ThreeRenderer extends Renderer { private readonly size = new Vector2(0, 0); protected constructor(three_renderer: DisposableThreeRenderer) { - super(); this.renderer = three_renderer; this.renderer.domElement.tabIndex = 0; this.renderer.domElement.addEventListener("mousedown", this.on_mouse_down); diff --git a/src/core/rendering/webgl/WebglMesh.ts b/src/core/rendering/webgl/WebglMesh.ts new file mode 100644 index 00000000..c66af82f --- /dev/null +++ b/src/core/rendering/webgl/WebglMesh.ts @@ -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); + } +} diff --git a/src/core/rendering/WebglRenderer.ts b/src/core/rendering/webgl/WebglRenderer.ts similarity index 78% rename from src/core/rendering/WebglRenderer.ts rename to src/core/rendering/webgl/WebglRenderer.ts index 0d38b4b5..95db7e48 100644 --- a/src/core/rendering/WebglRenderer.ts +++ b/src/core/rendering/webgl/WebglRenderer.ts @@ -1,31 +1,32 @@ -import { Renderer } from "./Renderer"; -import { Mat4, mat4_product, Vec2, vec2_diff } from "../math"; -import { ShaderProgram } from "./ShaderProgram"; -import { GL } from "./VertexFormat"; -import { Scene } from "./Scene"; +import { Mat4, mat4_product, Vec2, vec2_diff } from "../../math"; +import { ShaderProgram } from "../ShaderProgram"; +import { GL, VertexFormat } from "../VertexFormat"; +import { WebglScene } from "./WebglScene"; import { POS_FRAG_SHADER_SOURCE, POS_TEX_FRAG_SHADER_SOURCE, POS_TEX_VERTEX_SHADER_SOURCE, POS_VERTEX_SHADER_SOURCE, -} from "./shader_sources"; -import { Camera } from "./Camera"; +} from "../shader_sources"; +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 { private readonly gl: GL; private readonly shader_programs: ShaderProgram[]; private animation_frame?: number; - private projection!: Mat4; + private projection_mat!: Mat4; private pointer_pos?: Vec2; - protected readonly scene: Scene; + protected readonly scene: WebglScene; protected readonly camera = new Camera(); readonly canvas_element: HTMLCanvasElement; constructor() { - super(); - this.canvas_element = document.createElement("canvas"); 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), ]; - this.scene = new Scene(gl); + this.scene = new WebglScene(gl); this.set_size(800, 600); @@ -85,7 +86,7 @@ export class WebglRenderer extends Renderer { this.gl.viewport(0, 0, width, height); // prettier-ignore - this.projection = Mat4.of( + this.projection_mat = Mat4.of( 2/width, 0, 0, 0, 0, 2/height, 0, 0, 0, 0, 2/10, 0, @@ -95,13 +96,27 @@ export class WebglRenderer extends Renderer { this.schedule_render(); } + mesh_builder(vertex_format: VertexFormat): MeshBuilder { + 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 => { this.animation_frame = undefined; const gl = this.gl; 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) => { const mat = mat4_product(parent_mat, node.transform.mat4); diff --git a/src/core/rendering/webgl/WebglScene.ts b/src/core/rendering/webgl/WebglScene.ts new file mode 100644 index 00000000..44913352 --- /dev/null +++ b/src/core/rendering/webgl/WebglScene.ts @@ -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(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(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); + } +} diff --git a/src/core/rendering/webgpu/WebgpuMesh.ts b/src/core/rendering/webgpu/WebgpuMesh.ts new file mode 100644 index 00000000..029d9c56 --- /dev/null +++ b/src/core/rendering/webgpu/WebgpuMesh.ts @@ -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(); + } +} diff --git a/src/core/rendering/webgpu/WebgpuRenderer.ts b/src/core/rendering/webgpu/WebgpuRenderer.ts new file mode 100644 index 00000000..b03749c6 --- /dev/null +++ b/src/core/rendering/webgpu/WebgpuRenderer.ts @@ -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 { + 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 { + 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 { + 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(); + } +} diff --git a/src/core/rendering/webgpu/WebgpuScene.ts b/src/core/rendering/webgpu/WebgpuScene.ts new file mode 100644 index 00000000..9f5f6925 --- /dev/null +++ b/src/core/rendering/webgpu/WebgpuScene.ts @@ -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(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(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); + } +} diff --git a/src/viewer/index.ts b/src/viewer/index.ts index 6cfe606c..fba33b58 100644 --- a/src/viewer/index.ts +++ b/src/viewer/index.ts @@ -59,7 +59,10 @@ export function initialize_viewer( 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"); renderer = new TextureWebglRenderer(controller); } else { diff --git a/src/viewer/rendering/ModelWebglRenderer.ts b/src/viewer/rendering/ModelWebglRenderer.ts new file mode 100644 index 00000000..45d502f1 --- /dev/null +++ b/src/viewer/rendering/ModelWebglRenderer.ts @@ -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(); + } +} diff --git a/src/viewer/rendering/TextureWebglRenderer.ts b/src/viewer/rendering/TextureWebglRenderer.ts index 8de28bce..c77ec113 100644 --- a/src/viewer/rendering/TextureWebglRenderer.ts +++ b/src/viewer/rendering/TextureWebglRenderer.ts @@ -1,14 +1,14 @@ import { Disposer } from "../../core/observable/Disposer"; import { LogManager } from "../../core/Logger"; 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 { Mesh, MeshBuilder } from "../../core/rendering/Mesh"; import { TranslateTransform } from "../../core/rendering/Transform"; import { VertexFormat } from "../../core/rendering/VertexFormat"; 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 { private readonly disposer = new Disposer(); @@ -47,8 +47,7 @@ export class TextureWebglRenderer extends WebglRenderer { try { const quad_mesh = this.create_quad(tex); - this.scene.add_child( - this.scene.root_node, + this.scene.root_node.add_child( quad_mesh, 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 { - return new MeshBuilder(VertexFormat.PosTex) + private create_quad(tex: XvrTexture): WebglMesh { + return this.mesh_builder(VertexFormat.PosTex) .vertex(0, 0, 0, 0, 1) .vertex(tex.width, 0, 0, 1, 1) .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 data_size: number; diff --git a/src/viewer/rendering/TextureWebgpuRenderer.ts b/src/viewer/rendering/TextureWebgpuRenderer.ts new file mode 100644 index 00000000..ea6c2d90 --- /dev/null +++ b/src/viewer/rendering/TextureWebgpuRenderer.ts @@ -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)); +} diff --git a/tsconfig.json b/tsconfig.json index dcc6ecea..4ef9db3f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "module": "esnext", "target": "es6", "lib": ["esnext", "dom", "dom.iterable"], + "typeRoots": ["node_modules/@types", "node_modules/@webgpu"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, diff --git a/yarn.lock b/yarn.lock index c4445d37..c1080bc1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -675,6 +675,16 @@ "@webassemblyjs/wast-parser" "1.8.5" "@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": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"