Started work on WebGPU renderer.

This commit is contained in:
Daan Vanden Bosch 2020-01-23 01:16:52 +01:00
parent 9960d745c2
commit baffab3234
23 changed files with 921 additions and 253 deletions

View File

@ -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 }],

View File

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

View File

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

View File

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

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

View File

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

View 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,
);
}
}

View File

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

View File

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

View File

@ -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 {

View File

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

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

View File

@ -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<WebglMesh> {
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<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 => {
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);

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

View 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();
}
}

View 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();
}
}

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

View File

@ -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 {

View 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();
}
}

View File

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

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

View File

@ -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,

View File

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