mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Started work on WebGPU renderer.
This commit is contained in:
parent
9960d745c2
commit
baffab3234
@ -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 }],
|
||||
|
@ -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",
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
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 {
|
||||
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;
|
||||
}
|
||||
|
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";
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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 {
|
||||
this.gl.uniformMatrix4fv(this.transform_loc, true, matrix.data);
|
||||
this.gl.uniformMatrix4fv(this.transform_loc, false, matrix.data);
|
||||
}
|
||||
|
||||
set_texture_uniform(unit: GLenum): void {
|
||||
|
@ -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);
|
||||
|
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 { 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);
|
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;
|
||||
|
||||
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 {
|
||||
|
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 { 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;
|
||||
|
||||
|
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",
|
||||
"target": "es6",
|
||||
"lib": ["esnext", "dom", "dom.iterable"],
|
||||
"typeRoots": ["node_modules/@types", "node_modules/@webgpu"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
|
10
yarn.lock
10
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"
|
||||
|
Loading…
Reference in New Issue
Block a user