WebGPU renderer can now render textures. WebGL and WebGPU renderers now reuse more code.

This commit is contained in:
Daan Vanden Bosch 2020-01-25 22:38:07 +01:00
parent baffab3234
commit a19a3a4837
41 changed files with 2041 additions and 1828 deletions

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,15 @@
#version 450
precision mediump float;
precision mediump sampler;
layout(set = 0, binding = 1) uniform sampler tex_sampler;
layout(set = 0, binding = 2) uniform texture2D tex;
layout(location = 0) in vec2 frag_tex_coords;
layout(location = 0) out vec4 out_color;
void main() {
out_color = texture(sampler2D(tex, tex_sampler), frag_tex_coords);
}

View File

@ -0,0 +1,15 @@
#version 450
layout(set = 0, binding = 0) uniform Uniforms {
mat4 mvp_mat;
} uniforms;
layout(location = 0) in vec3 pos;
layout(location = 1) in vec2 tex_coords;
layout(location = 0) out vec2 frag_tex_coords;
void main() {
gl_Position = uniforms.mvp_mat * vec4(pos, 1.0);
frag_tex_coords = tex_coords;
}

View File

@ -0,0 +1,33 @@
import glsl_module, { ShaderStage } from "@webgpu/glslang";
import * as fs from "fs";
import { RESOURCE_DIR, ASSETS_DIR } from "./index";
const glsl = glsl_module();
const SHADER_RESOURCES_DIR = `${RESOURCE_DIR}/shaders`;
const SHADER_ASSETS_DIR = `${ASSETS_DIR}/shaders`;
function compile_shader(source_file: string, shader_stage: ShaderStage): void {
const source = fs.readFileSync(`${SHADER_RESOURCES_DIR}/${source_file}`, "utf8");
const spir_v = glsl.compileGLSL(source, shader_stage, true);
fs.writeFileSync(
`${SHADER_ASSETS_DIR}/${source_file}.spv`,
new Uint8Array(spir_v.buffer, spir_v.byteOffset, spir_v.byteLength),
);
}
for (const file of fs.readdirSync(SHADER_RESOURCES_DIR)) {
let shader_stage: ShaderStage;
switch (file.slice(-4)) {
case "vert":
shader_stage = "vertex";
break;
case "frag":
shader_stage = "fragment";
break;
default:
throw new Error(`Unsupported shader type: ${file.slice(-4)}`);
}
compile_shader(file, shader_stage);
}

View File

@ -8,4 +8,9 @@ module.exports = {
"^monaco-editor$": "<rootDir>/node_modules/monaco-editor/esm/vs/editor/editor.main.js",
"^worker-loader!": "<rootDir>/src/__mocks__/webworkers.js",
},
globals: {
"ts-jest": {
isolatedModules: true,
},
},
};

View File

@ -4,7 +4,6 @@
"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",
@ -20,18 +19,20 @@
"test": "jest",
"update_generic_data": "ts-node --project=tsconfig-scripts.json assets_generation/update_generic_data.ts",
"update_ephinea_data": "ts-node --project=tsconfig-scripts.json assets_generation/update_ephinea_data.ts",
"update_shaders": "ts-node --project=tsconfig-scripts.json assets_generation/update_shaders.ts",
"lint": "prettier --check \"{src,assets_generation,test}/**/*.{ts,tsx}\" && echo Linting... && eslint \"{src,assets_generation,test}/**/*.{ts,tsx}\" && echo All code passes the prettier and eslint checks."
},
"devDependencies": {
"@fortawesome/fontawesome-free": "^5.12.0",
"@types/cheerio": "^0.22.15",
"@types/jest": "^24.0.24",
"@types/jest": "^24.9.1",
"@types/lodash": "^4.14.149",
"@types/luxon": "^1.21.0",
"@types/node-fetch": "^2.5.4",
"@types/yaml": "^1.2.0",
"@typescript-eslint/eslint-plugin": "^2.12.0",
"@typescript-eslint/parser": "^2.12.0",
"@webgpu/glslang": "^0.0.12",
"@webgpu/types": "^0.0.21",
"cheerio": "^1.0.0-rc.3",
"clean-webpack-plugin": "^3.0.0",
@ -44,15 +45,16 @@
"file-loader": "^5.0.2",
"fork-ts-checker-webpack-plugin": "^3.1.1",
"html-webpack-plugin": "^3.2.0",
"jest": "^24.9.0",
"jest": "^25.1.0",
"jest-canvas-mock": "^2.2.0",
"mini-css-extract-plugin": "^0.9.0",
"monaco-editor-webpack-plugin": "^1.8.1",
"node-fetch": "^2.6.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"prettier": "^1.19.1",
"raw-loader": "^4.0.0",
"terser-webpack-plugin": "^2.3.1",
"ts-jest": "^24.2.0",
"ts-jest": "^25.0.0",
"ts-loader": "^6.2.1",
"ts-node": "^8.5.4",
"typescript": "^3.7.4",

22
src/core/rendering/Gfx.ts Normal file
View File

@ -0,0 +1,22 @@
import { Texture, TextureFormat } from "./Texture";
import { VertexFormat } from "./VertexFormat";
export interface Gfx<GfxMesh = unknown, GfxTexture = unknown> {
create_gfx_mesh(
format: VertexFormat,
vertex_data: ArrayBuffer,
index_data: ArrayBuffer,
texture?: Texture,
): GfxMesh;
destroy_gfx_mesh(gfx_mesh?: GfxMesh): void;
create_texture(
format: TextureFormat,
width: number,
height: number,
data: ArrayBuffer,
): GfxTexture;
destroy_texture(texture?: GfxTexture): void;
}

View File

@ -0,0 +1,110 @@
import { Renderer } from "./Renderer";
import { VertexFormat } from "./VertexFormat";
import { MeshBuilder } from "./MeshBuilder";
import { Scene } from "./Scene";
import { Camera } from "./Camera";
import { Gfx } from "./Gfx";
import { Mat4, Vec2, vec2_diff } from "../math";
export abstract class GfxRenderer implements Renderer {
private pointer_pos?: Vec2;
/**
* Is defined when an animation frame is scheduled.
*/
private animation_frame?: number;
protected projection_mat: Mat4 = Mat4.identity();
abstract readonly gfx: Gfx;
readonly scene = new Scene();
readonly camera = new Camera();
readonly canvas_element: HTMLCanvasElement = document.createElement("canvas");
protected constructor() {
this.canvas_element.width = 800;
this.canvas_element.height = 600;
this.canvas_element.addEventListener("mousedown", this.mousedown);
this.canvas_element.addEventListener("wheel", this.wheel, { passive: true });
}
dispose(): void {
this.scene.destroy();
}
set_size(width: number, height: number): void {
// 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,
);
this.schedule_render();
}
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.call_render);
}
};
private call_render = (): void => {
this.animation_frame = undefined;
this.render();
};
protected abstract render(): void;
mesh_builder(vertex_format: VertexFormat): MeshBuilder {
return new MeshBuilder(this.gfx, vertex_format);
}
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();
};
}

View File

@ -1,17 +0,0 @@
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,7 +1,30 @@
import { VertexFormat } from "./VertexFormat";
import { Texture } from "./Texture";
import { Gfx } from "./Gfx";
export interface Mesh {
readonly format: VertexFormat;
readonly texture?: Texture;
export class Mesh {
gfx_mesh: unknown;
constructor(
private readonly gfx: Gfx,
readonly format: VertexFormat,
readonly vertex_data: ArrayBuffer,
readonly index_data: ArrayBuffer,
readonly index_count: number,
readonly texture?: Texture,
) {}
upload(): void {
this.texture?.upload();
this.gfx_mesh = this.gfx.create_gfx_mesh(
this.format,
this.vertex_data,
this.index_data,
this.texture,
);
}
destroy(): void {
this.gfx.destroy_gfx_mesh(this.gfx_mesh);
}
}

View File

@ -2,9 +2,9 @@ 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";
import { Gfx } from "./Gfx";
export class MeshBuilder<MeshType extends Mesh> {
export class MeshBuilder {
private readonly vertex_data: {
x: number;
y: number;
@ -15,10 +15,7 @@ export class MeshBuilder<MeshType extends Mesh> {
private readonly index_data: number[] = [];
private _texture?: Texture;
constructor(
private readonly renderer: GlRenderer<MeshType>,
private readonly format: VertexFormat,
) {}
constructor(private readonly gfx: Gfx, private readonly format: VertexFormat) {}
vertex(x: number, y: number, z: number, u?: number, v?: number): this {
switch (this.format) {
@ -44,7 +41,7 @@ export class MeshBuilder<MeshType extends Mesh> {
return this;
}
build(): MeshType {
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);
@ -67,7 +64,8 @@ export class MeshBuilder<MeshType extends Mesh> {
const i_data = new Uint16Array(2 * Math.ceil(this.index_data.length / 2));
i_data.set(this.index_data);
return this.renderer.mesh(
return new Mesh(
this.gfx,
this.format,
v_data,
i_data,

View File

@ -0,0 +1,55 @@
import { IdentityTransform, Transform } from "./Transform";
import { Mesh } from "./Mesh";
export class Scene {
readonly root_node = new Node(this, undefined, new IdentityTransform());
/**
* Destroys all GPU objects related to this scene and resets the scene.
*/
destroy(): void {
this.traverse(node => {
node.mesh?.destroy();
node.mesh?.texture?.destroy();
node.mesh = undefined;
}, undefined);
this.root_node.clear_children();
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 {
private readonly _children: Node[] = [];
get children(): readonly Node[] {
return this._children;
}
constructor(
private readonly scene: Scene,
public mesh: Mesh | undefined,
public transform: Transform,
) {}
add_child(mesh: Mesh | undefined, transform: Transform): void {
this._children.push(new Node(this.scene, mesh, transform));
mesh?.upload();
}
clear_children(): void {
this._children.splice(0);
}
}

View File

@ -1,13 +1,13 @@
import { Mat4 } from "../math";
import { GL, VERTEX_POS_LOC, VERTEX_TEX_LOC } from "./VertexFormat";
import { VERTEX_POS_LOC, VERTEX_TEX_LOC } from "./VertexFormat";
export class ShaderProgram {
private readonly gl: GL;
private readonly gl: WebGL2RenderingContext;
private readonly program: WebGLProgram;
private readonly transform_loc: WebGLUniformLocation;
private readonly tex_sampler_loc: WebGLUniformLocation | null;
constructor(gl: GL, vertex_source: string, frag_source: string) {
constructor(gl: WebGL2RenderingContext, vertex_source: string, frag_source: string) {
this.gl = gl;
const program = gl.createProgram();
if (program == null) throw new Error("Failed to create program.");
@ -71,7 +71,7 @@ export class ShaderProgram {
}
}
function create_shader(gl: GL, type: GLenum, source: string): WebGLShader {
function create_shader(gl: WebGL2RenderingContext, type: GLenum, source: string): WebGLShader {
const shader = gl.createShader(type);
if (shader == null) throw new Error(`Failed to create shader of type ${type}.`);

View File

@ -1,4 +1,4 @@
import { GL } from "./VertexFormat";
import { Gfx } from "./Gfx";
export enum TextureFormat {
RGBA_S3TC_DXT1,
@ -6,67 +6,28 @@ export enum TextureFormat {
}
export class Texture {
private uploaded = false;
private texture: WebGLTexture | null = null;
gfx_texture: unknown;
constructor(
private readonly gfx: Gfx,
private readonly format: TextureFormat,
private readonly width: number,
private readonly height: number,
private readonly format: TextureFormat,
private readonly data: ArrayBuffer,
) {}
upload(gl: GL): void {
if (this.uploaded) return;
const ext = gl.getExtension("WEBGL_compressed_texture_s3tc");
if (!ext) {
throw new Error("Extension WEBGL_compressed_texture_s3tc not supported.");
upload(): void {
if (this.gfx_texture == undefined) {
this.gfx_texture = this.gfx.create_texture(
this.format,
this.width,
this.height,
this.data,
);
}
const texture = gl.createTexture();
if (texture == null) throw new Error("Failed to create texture.");
this.texture = texture;
let gl_format: GLenum;
switch (this.format) {
case TextureFormat.RGBA_S3TC_DXT1:
gl_format = ext.COMPRESSED_RGBA_S3TC_DXT1_EXT;
break;
case TextureFormat.RGBA_S3TC_DXT3:
gl_format = ext.COMPRESSED_RGBA_S3TC_DXT3_EXT;
break;
}
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.compressedTexImage2D(
gl.TEXTURE_2D,
0,
gl_format,
this.width,
this.height,
0,
new Uint8Array(this.data),
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.bindTexture(gl.TEXTURE_2D, null);
this.uploaded = true;
}
bind(gl: GL): void {
gl.bindTexture(gl.TEXTURE_2D, this.texture);
}
unbind(gl: GL): void {
gl.bindTexture(gl.TEXTURE_2D, null);
}
delete(gl: GL): void {
gl.deleteTexture(this.texture);
destroy(): void {
this.gfx.destroy_texture(this.gfx_texture);
}
}

View File

@ -1,5 +1,3 @@
export type GL = WebGL2RenderingContext;
export enum VertexFormat {
Pos,
PosTex,

View File

@ -1,55 +0,0 @@
export const POS_VERTEX_SHADER_SOURCE = `#version 300 es
precision mediump float;
uniform mat4 transform;
in vec4 pos;
void main() {
gl_Position = transform * pos;
}
`;
export const POS_FRAG_SHADER_SOURCE = `#version 300 es
precision mediump float;
out vec4 frag_color;
void main() {
frag_color = vec4(0, 1, 1, 1);
}
`;
export const POS_TEX_VERTEX_SHADER_SOURCE = `#version 300 es
precision mediump float;
uniform mat4 transform;
in vec4 pos;
in vec2 tex;
out vec2 f_tex;
void main() {
gl_Position = transform * pos;
f_tex = tex;
}
`;
export const POS_TEX_FRAG_SHADER_SOURCE = `#version 300 es
precision mediump float;
uniform sampler2D tex_sampler;
in vec2 f_tex;
out vec4 frag_color;
void main() {
frag_color = texture(tex_sampler, f_tex);
}
`;

View File

@ -0,0 +1,149 @@
import { Gfx } from "../Gfx";
import { Texture, TextureFormat } from "../Texture";
import {
vertex_format_size,
vertex_format_tex_offset,
VERTEX_POS_LOC,
VERTEX_TEX_LOC,
VertexFormat,
} from "../VertexFormat";
export type WebglMesh = {
readonly vao: WebGLVertexArrayObject;
readonly vertex_buffer: WebGLBuffer;
readonly index_buffer: WebGLBuffer;
};
export class WebglGfx implements Gfx<WebglMesh, WebGLTexture> {
constructor(private readonly gl: WebGL2RenderingContext) {}
create_gfx_mesh(
format: VertexFormat,
vertex_data: ArrayBuffer,
index_data: ArrayBuffer,
texture?: Texture,
): WebglMesh {
const gl = this.gl;
let vao: WebGLVertexArrayObject | null = null;
let vertex_buffer: WebGLBuffer | null = null;
let index_buffer: WebGLBuffer | null = null;
try {
vao = gl.createVertexArray();
if (vao == null) throw new Error("Failed to create VAO.");
vertex_buffer = gl.createBuffer();
if (vertex_buffer == null) throw new Error("Failed to create vertex buffer.");
index_buffer = gl.createBuffer();
if (index_buffer == null) throw new Error("Failed to create index buffer.");
gl.bindVertexArray(vao);
// Vertex data.
gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertex_data, gl.STATIC_DRAW);
const vertex_size = vertex_format_size(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(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, index_buffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index_data, gl.STATIC_DRAW);
gl.bindVertexArray(null);
texture?.upload();
return {
vao,
vertex_buffer,
index_buffer,
};
} catch (e) {
gl.deleteVertexArray(vao);
gl.deleteBuffer(vertex_buffer);
gl.deleteBuffer(index_buffer);
throw e;
}
}
destroy_gfx_mesh(gfx_mesh?: WebglMesh): void {
if (gfx_mesh) {
const gl = this.gl;
gl.deleteVertexArray(gfx_mesh.vao);
gl.deleteBuffer(gfx_mesh.vertex_buffer);
gl.deleteBuffer(gfx_mesh.index_buffer);
}
}
create_texture(
format: TextureFormat,
width: number,
height: number,
data: ArrayBuffer,
): WebGLTexture {
const gl = this.gl;
const ext = gl.getExtension("WEBGL_compressed_texture_s3tc");
if (!ext) {
throw new Error("Extension WEBGL_compressed_texture_s3tc not supported.");
}
const gl_texture = gl.createTexture();
if (gl_texture == null) throw new Error("Failed to create texture.");
let gl_format: GLenum;
switch (format) {
case TextureFormat.RGBA_S3TC_DXT1:
gl_format = ext.COMPRESSED_RGBA_S3TC_DXT1_EXT;
break;
case TextureFormat.RGBA_S3TC_DXT3:
gl_format = ext.COMPRESSED_RGBA_S3TC_DXT3_EXT;
break;
}
gl.bindTexture(gl.TEXTURE_2D, gl_texture);
gl.compressedTexImage2D(
gl.TEXTURE_2D,
0,
gl_format,
width,
height,
0,
new Uint8Array(data),
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.bindTexture(gl.TEXTURE_2D, null);
return gl_texture;
}
destroy_texture(texture?: WebGLTexture): void {
if (texture != undefined) {
this.gl.deleteTexture(texture);
}
}
}

View File

@ -1,99 +0,0 @@
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,33 +1,20 @@
import { Mat4, mat4_product, Vec2, vec2_diff } from "../../math";
import { mat4_product } 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";
import { MeshBuilder } from "../MeshBuilder";
import { Texture } from "../Texture";
import { GlRenderer } from "../GlRenderer";
import { WebglMesh } from "./WebglMesh";
import pos_vert_shader_source from "./pos.vert";
import pos_frag_shader_source from "./pos.frag";
import pos_tex_vert_shader_source from "./pos_tex.vert";
import pos_tex_frag_shader_source from "./pos_tex.frag";
import { GfxRenderer } from "../GfxRenderer";
import { WebglGfx, WebglMesh } from "./WebglGfx";
export class WebglRenderer implements GlRenderer<WebglMesh> {
private readonly gl: GL;
export class WebglRenderer extends GfxRenderer {
private readonly gl: WebGL2RenderingContext;
private readonly shader_programs: ShaderProgram[];
private animation_frame?: number;
private projection_mat!: Mat4;
private pointer_pos?: Vec2;
protected readonly scene: WebglScene;
protected readonly camera = new Camera();
readonly canvas_element: HTMLCanvasElement;
readonly gfx: WebglGfx;
constructor() {
this.canvas_element = document.createElement("canvas");
super();
const gl = this.canvas_element.getContext("webgl2");
@ -36,22 +23,18 @@ export class WebglRenderer implements GlRenderer<WebglMesh> {
}
this.gl = gl;
this.gfx = new WebglGfx(gl);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
gl.clearColor(0.1, 0.1, 0.1, 1);
this.shader_programs = [
new ShaderProgram(gl, POS_VERTEX_SHADER_SOURCE, POS_FRAG_SHADER_SOURCE),
new ShaderProgram(gl, POS_TEX_VERTEX_SHADER_SOURCE, POS_TEX_FRAG_SHADER_SOURCE),
new ShaderProgram(gl, pos_vert_shader_source, pos_frag_shader_source),
new ShaderProgram(gl, pos_tex_vert_shader_source, pos_tex_frag_shader_source),
];
this.scene = new WebglScene(gl);
this.set_size(800, 600);
this.canvas_element.addEventListener("mousedown", this.mousedown);
this.canvas_element.addEventListener("wheel", this.wheel, { passive: true });
}
dispose(): void {
@ -59,59 +42,18 @@ export class WebglRenderer implements GlRenderer<WebglMesh> {
program.delete();
}
this.scene.delete();
super.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.canvas_element.width = width;
this.canvas_element.height = height;
this.gl.viewport(0, 0, width, 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,
);
this.schedule_render();
super.set_size(width, height);
}
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;
protected render(): void {
const gl = this.gl;
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
@ -127,55 +69,23 @@ export class WebglRenderer implements GlRenderer<WebglMesh> {
program.set_transform_uniform(mat);
if (node.mesh.texture) {
if (node.mesh.texture?.gfx_texture) {
gl.activeTexture(gl.TEXTURE0);
node.mesh.texture.bind(gl);
gl.bindTexture(gl.TEXTURE_2D, node.mesh.texture.gfx_texture as WebGLTexture);
program.set_texture_uniform(gl.TEXTURE0);
}
node.mesh.render(gl);
const gfx_mesh = node.mesh.gfx_mesh as WebglMesh;
gl.bindVertexArray(gfx_mesh.vao);
gl.drawElements(gl.TRIANGLES, node.mesh.index_count, gl.UNSIGNED_SHORT, 0);
gl.bindVertexArray(null);
gl.bindTexture(gl.TEXTURE_2D, null);
node.mesh.texture?.unbind(gl);
program.unbind();
}
return mat;
}, camera_project_mat);
};
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();
};
}
}

View File

@ -1,65 +0,0 @@
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,9 @@
#version 300 es
precision mediump float;
out vec4 frag_color;
void main() {
frag_color = vec4(0, 1, 1, 1);
}

View File

@ -0,0 +1,11 @@
#version 300 es
precision mediump float;
uniform mat4 transform;
in vec4 pos;
void main() {
gl_Position = transform * pos;
}

View File

@ -0,0 +1,13 @@
#version 300 es
precision mediump float;
uniform sampler2D tex_sampler;
in vec2 f_tex;
out vec4 frag_color;
void main() {
frag_color = texture(tex_sampler, f_tex);
}

View File

@ -0,0 +1,15 @@
#version 300 es
precision mediump float;
uniform mat4 transform;
in vec4 pos;
in vec2 tex;
out vec2 f_tex;
void main() {
gl_Position = transform * pos;
f_tex = tex;
}

View File

@ -0,0 +1,8 @@
export const POS_VERTEX_SHADER_SOURCE = `
`;
export const POS_FRAG_SHADER_SOURCE = ``;
export const POS_TEX_VERTEX_SHADER_SOURCE = ``;
export const POS_TEX_FRAG_SHADER_SOURCE = ``;

View File

@ -0,0 +1,9 @@
import { HttpClient } from "../../HttpClient";
export class ShaderLoader {
constructor(private readonly http_client: HttpClient) {}
async load(name: string): Promise<Uint32Array> {
return new Uint32Array(await this.http_client.get(`/shaders/${name}.spv`).array_buffer());
}
}

View File

@ -0,0 +1,173 @@
import { Gfx } from "../Gfx";
import { Texture, TextureFormat } from "../Texture";
import { VertexFormat } from "../VertexFormat";
export type WebgpuMesh = {
readonly uniform_buffer: GPUBuffer;
readonly bind_group: GPUBindGroup;
readonly vertex_buffer: GPUBuffer;
readonly index_buffer: GPUBuffer;
};
export class WebgpuGfx implements Gfx<WebgpuMesh, GPUTexture> {
constructor(
private readonly device: GPUDevice,
private readonly bind_group_layout: GPUBindGroupLayout,
) {}
create_gfx_mesh(
format: VertexFormat,
vertex_data: ArrayBuffer,
index_data: ArrayBuffer,
texture?: Texture,
): WebgpuMesh {
const uniform_buffer = this.device.createBuffer({
size: 4 * 16,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, // eslint-disable-line no-undef
});
const bind_group = this.device.createBindGroup({
layout: this.bind_group_layout,
bindings: [
{
binding: 0,
resource: {
buffer: uniform_buffer,
},
},
{
binding: 1,
resource: this.device.createSampler({
magFilter: "linear",
minFilter: "linear",
}),
},
{
binding: 2,
resource: (texture!.gfx_texture as GPUTexture).createView(),
},
],
});
const vertex_buffer = this.device.createBuffer({
size: vertex_data.byteLength,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, // eslint-disable-line no-undef
});
vertex_buffer.setSubData(0, new Uint8Array(vertex_data));
const index_buffer = this.device.createBuffer({
size: index_data.byteLength,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.INDEX, // eslint-disable-line no-undef
});
index_buffer.setSubData(0, new Uint16Array(index_data));
return {
uniform_buffer,
bind_group,
vertex_buffer,
index_buffer,
};
}
destroy_gfx_mesh(gfx_mesh?: WebgpuMesh): void {
if (gfx_mesh) {
gfx_mesh.uniform_buffer.destroy();
gfx_mesh.vertex_buffer.destroy();
gfx_mesh.index_buffer.destroy();
}
}
create_texture(
format: TextureFormat,
width: number,
height: number,
data: ArrayBuffer,
): GPUTexture {
if (format === TextureFormat.RGBA_S3TC_DXT1 || format === TextureFormat.RGBA_S3TC_DXT3) {
// Chrome's WebGPU implementation doesn't support compressed textures yet. Use a dummy
// texture instead.
const ab = new ArrayBuffer(16);
const ba = new Uint32Array(ab);
ba[0] = 0xffff0000;
ba[1] = 0xff00ff00;
ba[2] = 0xff0000ff;
ba[3] = 0xff00ffff;
width = 2;
height = 2;
data = ab;
}
const texture = this.device.createTexture({
size: {
width,
height,
depth: 1,
},
format: "rgba8unorm",
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.SAMPLED, // eslint-disable-line no-undef
});
const row_pitch = Math.ceil((4 * width) / 256) * 256;
const data_size = row_pitch * height;
const buffer = this.device.createBuffer({
size: data_size,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, // eslint-disable-line no-undef
});
let buffer_data: Uint8Array;
if (data_size === data.byteLength) {
buffer_data = new Uint8Array(data);
} else {
buffer_data = new Uint8Array(data_size);
const orig_data = new Uint8Array(data);
let orig_idx = 0;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = 4 * x + row_pitch * y;
buffer_data[idx] = orig_data[orig_idx];
buffer_data[idx + 1] = orig_data[orig_idx + 1];
buffer_data[idx + 2] = orig_data[orig_idx + 2];
buffer_data[idx + 3] = orig_data[orig_idx + 3];
orig_idx += 4;
}
}
}
buffer.setSubData(0, buffer_data);
const command_encoder = this.device.createCommandEncoder();
command_encoder.copyBufferToTexture(
{
buffer,
rowPitch: row_pitch,
imageHeight: 0,
},
{
texture,
},
{
width,
height,
depth: 1,
},
);
this.device.defaultQueue.submit([command_encoder.finish()]);
buffer.destroy();
return texture;
}
destroy_texture(texture?: GPUTexture): void {
texture?.destroy();
}
}

View File

@ -1,72 +0,0 @@
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

@ -1,67 +1,39 @@
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";
import { GfxRenderer } from "../GfxRenderer";
import { mat4_product } from "../../math";
import { WebgpuGfx, WebgpuMesh } from "./WebgpuGfx";
import { ShaderLoader } from "./ShaderLoader";
import { HttpClient } from "../../HttpClient";
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> {
export class WebgpuRenderer extends GfxRenderer {
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 gpu?: {
gfx: WebgpuGfx;
device: GPUDevice;
swap_chain: GPUSwapChain;
pipeline: GPURenderPipeline;
};
private width = 800;
private height = 600;
private pointer_pos?: Vec2;
private shader_loader: ShaderLoader;
protected scene?: WebgpuScene;
protected readonly camera = new Camera();
get gfx(): WebgpuGfx {
return this.gpu!.gfx;
}
readonly canvas_element: HTMLCanvasElement = document.createElement("canvas");
constructor(http_client: HttpClient) {
super();
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.shader_loader = new ShaderLoader(http_client);
this.initialize();
}
@ -82,37 +54,84 @@ export class WebgpuRenderer implements GlRenderer<WebgpuMesh> {
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();
const vertex_shader_source = await this.shader_loader.load("vertex_shader.vert");
const fragment_shader_source = await this.shader_loader.load("fragment_shader.frag");
if (!this.disposed) {
this.renderer = new InitializedRenderer(
this.canvas_element,
context,
const swap_chain_format = "bgra8unorm";
const 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",
},
{
binding: 1,
visibility: GPUShaderStage.FRAGMENT, // eslint-disable-line no-undef
type: "sampler",
},
{
binding: 2,
visibility: GPUShaderStage.FRAGMENT, // eslint-disable-line no-undef
type: "sampled-texture",
},
],
});
const pipeline = device.createRenderPipeline({
layout: device.createPipelineLayout({ bindGroupLayouts: [bind_group_layout] }),
vertexStage: {
module: device.createShaderModule({
code: vertex_shader_source,
}),
entryPoint: "main",
},
fragmentStage: {
module: device.createShaderModule({
code: fragment_shader_source,
}),
entryPoint: "main",
},
primitiveTopology: "triangle-list",
colorStates: [{ format: swap_chain_format }],
vertexState: {
indexFormat: "uint16",
vertexBuffers: [
{
arrayStride: vertex_format_size(VertexFormat.PosTex),
stepMode: "vertex",
attributes: [
{
format: "float3",
offset: 0,
shaderLocation: 0,
},
{
format: "ushort2norm",
offset: 12,
shaderLocation: 1,
},
],
},
],
},
});
this.gpu = {
gfx: new WebgpuGfx(device, bind_group_layout),
device,
glslang,
this.camera,
);
this.renderer.set_size(this.width, this.height);
swap_chain,
pipeline,
};
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();
this.set_size(this.width, this.height);
}
} catch (e) {
logger.error("Failed to initialize WebGPU renderer.", e);
@ -121,204 +140,64 @@ export class WebgpuRenderer implements GlRenderer<WebgpuMesh> {
dispose(): void {
this.disposed = true;
this.renderer?.dispose();
super.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);
// There seems to be a bug in chrome's WebGPU implementation that requires you to set a
// canvas element's width and height after it's added to the DOM.
if (this.gpu) {
this.canvas_element.width = width;
this.canvas_element.height = height;
}
this.schedule_render();
};
}
super.set_size(width, height);
}
class InitializedRenderer implements Disposable {
private readonly swap_chain: GPUSwapChain;
private readonly pipeline: GPURenderPipeline;
private projection_mat: Mat4 = Mat4.identity();
protected render(): void {
if (this.gpu) {
const { device, swap_chain, pipeline } = this.gpu;
readonly scene: WebgpuScene;
const command_encoder = device.createCommandEncoder();
const texture_view = swap_chain.getCurrentTexture().createView();
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: [
const pass_encoder = command_encoder.beginRenderPass({
colorAttachments: [
{
arrayStride: vertex_format_size(VertexFormat.Pos),
stepMode: "vertex",
attributes: [
{
format: "float3",
offset: 0,
shaderLocation: 0,
},
],
attachment: texture_view,
loadValue: { r: 0.1, g: 0.1, b: 0.1, a: 1 },
},
],
},
});
});
this.scene = new WebgpuScene(device, bind_group_layout);
}
pass_encoder.setPipeline(pipeline);
set_size(width: number, height: number): void {
this.canvas_element.width = width;
this.canvas_element.height = height;
const camera_project_mat = mat4_product(
this.projection_mat,
this.camera.transform.mat4,
);
// 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,
);
}
this.scene.traverse((node, parent_mat) => {
const mat = mat4_product(parent_mat, node.transform.mat4);
render(): void {
const command_encoder = this.device.createCommandEncoder();
const texture_view = this.swap_chain.getCurrentTexture().createView();
if (node.mesh) {
const gfx_mesh = node.mesh.gfx_mesh as WebgpuMesh;
gfx_mesh.uniform_buffer.setSubData(0, mat.data);
pass_encoder.setBindGroup(0, gfx_mesh.bind_group);
pass_encoder.setVertexBuffer(0, gfx_mesh.vertex_buffer);
pass_encoder.setIndexBuffer(gfx_mesh.index_buffer);
pass_encoder.drawIndexed(node.mesh.index_count, 1, 0, 0, 0);
}
const pass_encoder = command_encoder.beginRenderPass({
colorAttachments: [
{
attachment: texture_view,
loadValue: { r: 0.1, g: 0.1, b: 0.1, a: 1 },
},
],
});
return mat;
}, camera_project_mat);
pass_encoder.setPipeline(this.pipeline);
pass_encoder.endPass();
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();
device.defaultQueue.submit([command_encoder.finish()]);
}
}
}

View File

@ -1,67 +0,0 @@
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

@ -2,13 +2,13 @@ import { TextureView } from "./TextureView";
import { with_disposer } from "../../../test/src/core/observables/disposable_helpers";
import { TextureController } from "../controllers/TextureController";
import { TextureRenderer } from "../rendering/TextureRenderer";
import { StubThreeRenderer } from "../../../test/src/core/rendering/StubThreeRenderer";
import { StubGfxRenderer } from "../../../test/src/core/rendering/StubGfxRenderer";
test("Renders correctly without textures.", () =>
with_disposer(disposer => {
const ctrl = disposer.add(new TextureController());
const view = disposer.add(
new TextureView(ctrl, new TextureRenderer(ctrl, new StubThreeRenderer())),
new TextureView(ctrl, new TextureRenderer(ctrl, new StubGfxRenderer())),
);
expect(view.element).toMatchSnapshot("Should render a toolbar and a renderer widget.");

View File

@ -35,8 +35,8 @@ exports[`Renders correctly without textures.: Should render a toolbar and a rend
class="core_RendererWidget"
>
<canvas
style="outline: none;"
tabindex="0"
height="600"
width="800"
/>
</div>
</div>

View File

@ -54,20 +54,18 @@ export function initialize_viewer(
async () => {
const { TextureController } = await import("./controllers/TextureController");
const { TextureView } = await import("./gui/TextureView");
const { TextureRenderer } = await import("./rendering/TextureRenderer");
const controller = disposer.add(new TextureController());
let renderer: 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);
const { WebgpuRenderer } = await import("../core/rendering/webgpu/WebgpuRenderer");
renderer = new TextureRenderer(controller, new WebgpuRenderer(http_client));
} else {
const { TextureRenderer } = await import("./rendering/TextureRenderer");
renderer = new TextureRenderer(controller, create_three_renderer());
const { WebglRenderer } = await import("../core/rendering/webgl/WebglRenderer");
renderer = new TextureRenderer(controller, new WebglRenderer());
}
return new TextureView(controller, renderer);

View File

@ -1,62 +1,52 @@
import {
Mesh,
MeshBasicMaterial,
OrthographicCamera,
PlaneGeometry,
Texture,
Vector2,
Vector3,
} from "three";
import { Disposable } from "../../core/observable/Disposable";
import { DisposableThreeRenderer, ThreeRenderer } from "../../core/rendering/ThreeRenderer";
import { Disposer } from "../../core/observable/Disposer";
import { XvrTexture } from "../../core/data_formats/parsing/ninja/texture";
import { xvr_texture_to_texture } from "../../core/rendering/conversion/ninja_textures";
import { LogManager } from "../../core/Logger";
import { TextureController } from "../controllers/TextureController";
import { XvrTexture } from "../../core/data_formats/parsing/ninja/texture";
import { TranslateTransform } from "../../core/rendering/Transform";
import { VertexFormat } from "../../core/rendering/VertexFormat";
import { Texture, TextureFormat } from "../../core/rendering/Texture";
import { Mesh } from "../../core/rendering/Mesh";
import { GfxRenderer } from "../../core/rendering/GfxRenderer";
import { Renderer } from "../../core/rendering/Renderer";
const logger = LogManager.get("viewer/rendering/TextureRenderer");
export class TextureRenderer extends ThreeRenderer implements Disposable {
export class TextureRenderer implements Renderer {
private readonly disposer = new Disposer();
private readonly quad_meshes: Mesh[] = [];
readonly camera = new OrthographicCamera(-400, 400, 300, -300, 1, 10);
readonly canvas_element: HTMLCanvasElement;
constructor(ctrl: TextureController, three_renderer: DisposableThreeRenderer) {
super(three_renderer);
constructor(ctrl: TextureController, private readonly renderer: GfxRenderer) {
this.canvas_element = renderer.canvas_element;
this.disposer.add_all(
ctrl.textures.observe(({ value: textures }) => {
this.scene.remove(...this.quad_meshes);
this.render_textures(textures);
this.reset_camera(new Vector3(0, 0, 5), new Vector3());
this.schedule_render();
renderer.scene.destroy();
renderer.camera.reset();
this.create_quads(textures);
renderer.schedule_render();
}),
);
this.init_camera_controls();
this.controls.azimuthRotateSpeed = 0;
this.controls.polarRotateSpeed = 0;
}
set_size(width: number, height: number): void {
this.camera.left = -Math.floor(width / 2);
this.camera.right = Math.ceil(width / 2);
this.camera.top = Math.floor(height / 2);
this.camera.bottom = -Math.ceil(height / 2);
this.camera.updateProjectionMatrix();
super.set_size(width, height);
}
dispose(): void {
super.dispose();
this.renderer.dispose();
this.disposer.dispose();
}
private render_textures(textures: readonly XvrTexture[]): void {
start_rendering(): void {
this.renderer.start_rendering();
}
stop_rendering(): void {
this.renderer.stop_rendering();
}
set_size(width: number, height: number): void {
this.renderer.set_size(width, height);
}
private create_quads(textures: readonly XvrTexture[]): void {
let total_width = 10 * (textures.length - 1); // 10px spacing between textures.
let total_height = 0;
@ -69,47 +59,61 @@ export class TextureRenderer extends ThreeRenderer implements Disposable {
const y = -Math.floor(total_height / 2);
for (const tex of textures) {
let texture: Texture | undefined = undefined;
try {
texture = xvr_texture_to_texture(tex);
const quad_mesh = this.create_quad(tex);
this.renderer.scene.root_node.add_child(
quad_mesh,
new TranslateTransform(x, y + (total_height - tex.height) / 2, 0),
);
} catch (e) {
logger.error("Couldn't convert XVR texture.", e);
logger.error("Couldn't create quad for texture.", e);
}
const quad_mesh = new Mesh(
this.create_quad(
x,
y + Math.floor((total_height - tex.height) / 2),
tex.width,
tex.height,
),
texture
? new MeshBasicMaterial({
map: texture,
transparent: true,
})
: new MeshBasicMaterial({
color: 0xff00ff,
}),
);
this.quad_meshes.push(quad_mesh);
this.scene.add(quad_mesh);
x += 10 + tex.width;
}
}
private create_quad(x: number, y: number, width: number, height: number): PlaneGeometry {
const quad = new PlaneGeometry(width, height, 1, 1);
quad.faceVertexUvs = [
[
[new Vector2(0, 0), new Vector2(0, 1), new Vector2(1, 0)],
[new Vector2(0, 1), new Vector2(1, 1), new Vector2(1, 0)],
],
];
quad.translate(x + width / 2, y + height / 2, -5);
return quad;
private create_quad(tex: XvrTexture): Mesh {
return this.renderer
.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)
.vertex(0, tex.height, 0, 0, 0)
.triangle(0, 1, 2)
.triangle(2, 3, 0)
.texture(this.xvr_texture_to_texture(tex))
.build();
}
private 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(
this.renderer.gfx!,
format,
tex.width,
tex.height,
tex.data.slice(0, data_size),
);
}
}

View File

@ -1,97 +0,0 @@
import { Disposer } from "../../core/observable/Disposer";
import { LogManager } from "../../core/Logger";
import { TextureController } from "../controllers/TextureController";
import { WebglRenderer } from "../../core/rendering/webgl/WebglRenderer";
import { XvrTexture } from "../../core/data_formats/parsing/ninja/texture";
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/TextureWebglRenderer");
export class TextureWebglRenderer extends WebglRenderer {
private readonly disposer = new Disposer();
constructor(ctrl: TextureController) {
super();
this.disposer.add_all(
ctrl.textures.observe(({ value: textures }) => {
this.scene.delete();
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): 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)
.vertex(0, tex.height, 0, 0, 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

@ -1,97 +0,0 @@
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

@ -0,0 +1,14 @@
import { GfxRenderer } from "../../../../src/core/rendering/GfxRenderer";
import { Gfx } from "../../../../src/core/rendering/Gfx";
export class StubGfxRenderer extends GfxRenderer {
get gfx(): Gfx {
throw new Error("gfx is not implemented.");
}
constructor() {
super();
}
protected render(): void {} // eslint-disable-line
}

9
typedefs/shaders.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
const shader_source: string;
declare module "*.vert" {
export default shader_source;
}
declare module "*.frag" {
export default shader_source;
}

View File

@ -13,15 +13,14 @@ module.exports = {
},
module: {
rules: [
{
test: /^worker-loader!/,
loader: "worker-loader",
options: { name: "worker.[hash].js" },
},
{
test: /\.(gif|jpg|png|svg|ttf)$/,
loader: "file-loader",
},
{
test: /\.(vert|frag)$/,
loader: "raw-loader",
},
],
},
plugins: [

1801
yarn.lock

File diff suppressed because it is too large Load Diff