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", "^monaco-editor$": "<rootDir>/node_modules/monaco-editor/esm/vs/editor/editor.main.js",
"^worker-loader!": "<rootDir>/src/__mocks__/webworkers.js", "^worker-loader!": "<rootDir>/src/__mocks__/webworkers.js",
}, },
globals: {
"ts-jest": {
isolatedModules: true,
},
},
}; };

View File

@ -4,7 +4,6 @@
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@webgpu/glslang": "^0.0.12",
"camera-controls": "^1.16.2", "camera-controls": "^1.16.2",
"core-js": "^3.6.1", "core-js": "^3.6.1",
"golden-layout": "^1.5.9", "golden-layout": "^1.5.9",
@ -20,18 +19,20 @@
"test": "jest", "test": "jest",
"update_generic_data": "ts-node --project=tsconfig-scripts.json assets_generation/update_generic_data.ts", "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_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." "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": { "devDependencies": {
"@fortawesome/fontawesome-free": "^5.12.0", "@fortawesome/fontawesome-free": "^5.12.0",
"@types/cheerio": "^0.22.15", "@types/cheerio": "^0.22.15",
"@types/jest": "^24.0.24", "@types/jest": "^24.9.1",
"@types/lodash": "^4.14.149", "@types/lodash": "^4.14.149",
"@types/luxon": "^1.21.0", "@types/luxon": "^1.21.0",
"@types/node-fetch": "^2.5.4", "@types/node-fetch": "^2.5.4",
"@types/yaml": "^1.2.0", "@types/yaml": "^1.2.0",
"@typescript-eslint/eslint-plugin": "^2.12.0", "@typescript-eslint/eslint-plugin": "^2.12.0",
"@typescript-eslint/parser": "^2.12.0", "@typescript-eslint/parser": "^2.12.0",
"@webgpu/glslang": "^0.0.12",
"@webgpu/types": "^0.0.21", "@webgpu/types": "^0.0.21",
"cheerio": "^1.0.0-rc.3", "cheerio": "^1.0.0-rc.3",
"clean-webpack-plugin": "^3.0.0", "clean-webpack-plugin": "^3.0.0",
@ -44,15 +45,16 @@
"file-loader": "^5.0.2", "file-loader": "^5.0.2",
"fork-ts-checker-webpack-plugin": "^3.1.1", "fork-ts-checker-webpack-plugin": "^3.1.1",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"jest": "^24.9.0", "jest": "^25.1.0",
"jest-canvas-mock": "^2.2.0", "jest-canvas-mock": "^2.2.0",
"mini-css-extract-plugin": "^0.9.0", "mini-css-extract-plugin": "^0.9.0",
"monaco-editor-webpack-plugin": "^1.8.1", "monaco-editor-webpack-plugin": "^1.8.1",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.0",
"optimize-css-assets-webpack-plugin": "^5.0.3", "optimize-css-assets-webpack-plugin": "^5.0.3",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"raw-loader": "^4.0.0",
"terser-webpack-plugin": "^2.3.1", "terser-webpack-plugin": "^2.3.1",
"ts-jest": "^24.2.0", "ts-jest": "^25.0.0",
"ts-loader": "^6.2.1", "ts-loader": "^6.2.1",
"ts-node": "^8.5.4", "ts-node": "^8.5.4",
"typescript": "^3.7.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 { VertexFormat } from "./VertexFormat";
import { Texture } from "./Texture"; import { Texture } from "./Texture";
import { Gfx } from "./Gfx";
export interface Mesh { export class Mesh {
readonly format: VertexFormat; gfx_mesh: unknown;
readonly texture?: Texture;
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 { vertex_format_size, vertex_format_tex_offset, VertexFormat } from "./VertexFormat";
import { assert } from "../util"; import { assert } from "../util";
import { Mesh } from "./Mesh"; 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: { private readonly vertex_data: {
x: number; x: number;
y: number; y: number;
@ -15,10 +15,7 @@ export class MeshBuilder<MeshType extends Mesh> {
private readonly index_data: number[] = []; private readonly index_data: number[] = [];
private _texture?: Texture; private _texture?: Texture;
constructor( constructor(private readonly gfx: Gfx, private readonly format: VertexFormat) {}
private readonly renderer: GlRenderer<MeshType>,
private readonly format: VertexFormat,
) {}
vertex(x: number, y: number, z: number, u?: number, v?: number): this { vertex(x: number, y: number, z: number, u?: number, v?: number): this {
switch (this.format) { switch (this.format) {
@ -44,7 +41,7 @@ export class MeshBuilder<MeshType extends Mesh> {
return this; return this;
} }
build(): MeshType { build(): Mesh {
const v_size = vertex_format_size(this.format); const v_size = vertex_format_size(this.format);
const v_tex_offset = vertex_format_tex_offset(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_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)); const i_data = new Uint16Array(2 * Math.ceil(this.index_data.length / 2));
i_data.set(this.index_data); i_data.set(this.index_data);
return this.renderer.mesh( return new Mesh(
this.gfx,
this.format, this.format,
v_data, v_data,
i_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 { 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 { export class ShaderProgram {
private readonly gl: GL; private readonly gl: WebGL2RenderingContext;
private readonly program: WebGLProgram; private readonly program: WebGLProgram;
private readonly transform_loc: WebGLUniformLocation; private readonly transform_loc: WebGLUniformLocation;
private readonly tex_sampler_loc: WebGLUniformLocation | null; 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; this.gl = gl;
const program = gl.createProgram(); const program = gl.createProgram();
if (program == null) throw new Error("Failed to create program."); 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); const shader = gl.createShader(type);
if (shader == null) throw new Error(`Failed to create shader of type ${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 { export enum TextureFormat {
RGBA_S3TC_DXT1, RGBA_S3TC_DXT1,
@ -6,67 +6,28 @@ export enum TextureFormat {
} }
export class Texture { export class Texture {
private uploaded = false; gfx_texture: unknown;
private texture: WebGLTexture | null = null;
constructor( constructor(
private readonly gfx: Gfx,
private readonly format: TextureFormat,
private readonly width: number, private readonly width: number,
private readonly height: number, private readonly height: number,
private readonly format: TextureFormat,
private readonly data: ArrayBuffer, private readonly data: ArrayBuffer,
) {} ) {}
upload(gl: GL): void { upload(): void {
if (this.uploaded) return; if (this.gfx_texture == undefined) {
this.gfx_texture = this.gfx.create_texture(
const ext = gl.getExtension("WEBGL_compressed_texture_s3tc"); this.format,
if (!ext) {
throw new Error("Extension WEBGL_compressed_texture_s3tc not supported.");
}
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.width,
this.height, this.height,
0, this.data,
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 { destroy(): void {
gl.bindTexture(gl.TEXTURE_2D, this.texture); this.gfx.destroy_texture(this.gfx_texture);
}
unbind(gl: GL): void {
gl.bindTexture(gl.TEXTURE_2D, null);
}
delete(gl: GL): void {
gl.deleteTexture(this.texture);
} }
} }

View File

@ -1,5 +1,3 @@
export type GL = WebGL2RenderingContext;
export enum VertexFormat { export enum VertexFormat {
Pos, Pos,
PosTex, 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 { ShaderProgram } from "../ShaderProgram";
import { GL, VertexFormat } from "../VertexFormat"; import pos_vert_shader_source from "./pos.vert";
import { WebglScene } from "./WebglScene"; import pos_frag_shader_source from "./pos.frag";
import { import pos_tex_vert_shader_source from "./pos_tex.vert";
POS_FRAG_SHADER_SOURCE, import pos_tex_frag_shader_source from "./pos_tex.frag";
POS_TEX_FRAG_SHADER_SOURCE, import { GfxRenderer } from "../GfxRenderer";
POS_TEX_VERTEX_SHADER_SOURCE, import { WebglGfx, WebglMesh } from "./WebglGfx";
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";
export class WebglRenderer implements GlRenderer<WebglMesh> { export class WebglRenderer extends GfxRenderer {
private readonly gl: GL; private readonly gl: WebGL2RenderingContext;
private readonly shader_programs: ShaderProgram[]; private readonly shader_programs: ShaderProgram[];
private animation_frame?: number;
private projection_mat!: Mat4;
private pointer_pos?: Vec2;
protected readonly scene: WebglScene; readonly gfx: WebglGfx;
protected readonly camera = new Camera();
readonly canvas_element: HTMLCanvasElement;
constructor() { constructor() {
this.canvas_element = document.createElement("canvas"); super();
const gl = this.canvas_element.getContext("webgl2"); const gl = this.canvas_element.getContext("webgl2");
@ -36,22 +23,18 @@ export class WebglRenderer implements GlRenderer<WebglMesh> {
} }
this.gl = gl; this.gl = gl;
this.gfx = new WebglGfx(gl);
gl.enable(gl.DEPTH_TEST); gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE); gl.enable(gl.CULL_FACE);
gl.clearColor(0.1, 0.1, 0.1, 1); gl.clearColor(0.1, 0.1, 0.1, 1);
this.shader_programs = [ this.shader_programs = [
new ShaderProgram(gl, POS_VERTEX_SHADER_SOURCE, POS_FRAG_SHADER_SOURCE), new ShaderProgram(gl, pos_vert_shader_source, pos_frag_shader_source),
new ShaderProgram(gl, POS_TEX_VERTEX_SHADER_SOURCE, POS_TEX_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.set_size(800, 600);
this.canvas_element.addEventListener("mousedown", this.mousedown);
this.canvas_element.addEventListener("wheel", this.wheel, { passive: true });
} }
dispose(): void { dispose(): void {
@ -59,59 +42,18 @@ export class WebglRenderer implements GlRenderer<WebglMesh> {
program.delete(); 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 { set_size(width: number, height: number): void {
this.canvas_element.width = width; this.canvas_element.width = width;
this.canvas_element.height = height; this.canvas_element.height = height;
this.gl.viewport(0, 0, width, height); this.gl.viewport(0, 0, width, height);
// prettier-ignore super.set_size(width, height);
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();
} }
mesh_builder(vertex_format: VertexFormat): MeshBuilder<WebglMesh> { protected render(): void {
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; const gl = this.gl;
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 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); program.set_transform_uniform(mat);
if (node.mesh.texture) { if (node.mesh.texture?.gfx_texture) {
gl.activeTexture(gl.TEXTURE0); 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); 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(); program.unbind();
} }
return mat; return mat;
}, camera_project_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 { LogManager } from "../../Logger";
import { MeshBuilder } from "../MeshBuilder";
import { vertex_format_size, VertexFormat } from "../VertexFormat"; import { vertex_format_size, VertexFormat } from "../VertexFormat";
import { Texture } from "../Texture"; import { GfxRenderer } from "../GfxRenderer";
import { GlRenderer } from "../GlRenderer"; import { mat4_product } from "../../math";
import { WebgpuMesh } from "./WebgpuMesh"; import { WebgpuGfx, WebgpuMesh } from "./WebgpuGfx";
import { WebgpuScene } from "./WebgpuScene"; import { ShaderLoader } from "./ShaderLoader";
import { Camera } from "../Camera"; import { HttpClient } from "../../HttpClient";
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 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. * Uses the experimental WebGPU API for rendering.
*/ */
export class WebgpuRenderer implements GlRenderer<WebgpuMesh> { export class WebgpuRenderer extends GfxRenderer {
private disposed: boolean = false; private disposed: boolean = false;
/**
* Is defined when an animation frame is scheduled.
*/
private animation_frame?: number;
/** /**
* Is defined when the renderer is fully initialized. * 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 width = 800;
private height = 600; private height = 600;
private pointer_pos?: Vec2; private shader_loader: ShaderLoader;
protected scene?: WebgpuScene; get gfx(): WebgpuGfx {
protected readonly camera = new Camera(); return this.gpu!.gfx;
}
readonly canvas_element: HTMLCanvasElement = document.createElement("canvas"); constructor(http_client: HttpClient) {
super();
constructor() { this.shader_loader = new ShaderLoader(http_client);
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(); this.initialize();
} }
@ -82,146 +54,13 @@ export class WebgpuRenderer implements GlRenderer<WebgpuMesh> {
const adapter = await window.navigator.gpu.requestAdapter(); const adapter = await window.navigator.gpu.requestAdapter();
const device = await adapter.requestDevice(); const device = await adapter.requestDevice();
const glslang_module = await import( const vertex_shader_source = await this.shader_loader.load("vertex_shader.vert");
// @ts-ignore const fragment_shader_source = await this.shader_loader.load("fragment_shader.frag");
/* webpackIgnore: true */ "https://unpkg.com/@webgpu/glslang@0.0.7/web/glslang.js"
);
const glslang = await glslang_module.default();
if (!this.disposed) { 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"; const swap_chain_format = "bgra8unorm";
this.swap_chain = context.configureSwapChain({ const swap_chain = context.configureSwapChain({
device: device, device: device,
format: swap_chain_format, format: swap_chain_format,
}); });
@ -233,20 +72,30 @@ class InitializedRenderer implements Disposable {
visibility: GPUShaderStage.VERTEX, // eslint-disable-line no-undef visibility: GPUShaderStage.VERTEX, // eslint-disable-line no-undef
type: "uniform-buffer", 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",
},
], ],
}); });
this.pipeline = device.createRenderPipeline({ const pipeline = device.createRenderPipeline({
layout: device.createPipelineLayout({ bindGroupLayouts: [bind_group_layout] }), layout: device.createPipelineLayout({ bindGroupLayouts: [bind_group_layout] }),
vertexStage: { vertexStage: {
module: device.createShaderModule({ module: device.createShaderModule({
code: glslang.compileGLSL(VERTEX_SHADER_SOURCE, "vertex", true), code: vertex_shader_source,
}), }),
entryPoint: "main", entryPoint: "main",
}, },
fragmentStage: { fragmentStage: {
module: device.createShaderModule({ module: device.createShaderModule({
code: glslang.compileGLSL(FRAG_SHADER_SOURCE, "fragment", true), code: fragment_shader_source,
}), }),
entryPoint: "main", entryPoint: "main",
}, },
@ -256,7 +105,7 @@ class InitializedRenderer implements Disposable {
indexFormat: "uint16", indexFormat: "uint16",
vertexBuffers: [ vertexBuffers: [
{ {
arrayStride: vertex_format_size(VertexFormat.Pos), arrayStride: vertex_format_size(VertexFormat.PosTex),
stepMode: "vertex", stepMode: "vertex",
attributes: [ attributes: [
{ {
@ -264,31 +113,56 @@ class InitializedRenderer implements Disposable {
offset: 0, offset: 0,
shaderLocation: 0, shaderLocation: 0,
}, },
{
format: "ushort2norm",
offset: 12,
shaderLocation: 1,
},
], ],
}, },
], ],
}, },
}); });
this.scene = new WebgpuScene(device, bind_group_layout); this.gpu = {
gfx: new WebgpuGfx(device, bind_group_layout),
device,
swap_chain,
pipeline,
};
this.set_size(this.width, this.height);
}
} catch (e) {
logger.error("Failed to initialize WebGPU renderer.", e);
}
}
dispose(): void {
this.disposed = true;
super.dispose();
} }
set_size(width: number, height: number): void { set_size(width: number, height: number): void {
this.width = width;
this.height = height;
// 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.width = width;
this.canvas_element.height = height; 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 { super.set_size(width, height);
const command_encoder = this.device.createCommandEncoder(); }
const texture_view = this.swap_chain.getCurrentTexture().createView();
protected render(): void {
if (this.gpu) {
const { device, swap_chain, pipeline } = this.gpu;
const command_encoder = device.createCommandEncoder();
const texture_view = swap_chain.getCurrentTexture().createView();
const pass_encoder = command_encoder.beginRenderPass({ const pass_encoder = command_encoder.beginRenderPass({
colorAttachments: [ colorAttachments: [
@ -299,15 +173,23 @@ class InitializedRenderer implements Disposable {
], ],
}); });
pass_encoder.setPipeline(this.pipeline); pass_encoder.setPipeline(pipeline);
const camera_project_mat = mat4_product(this.projection_mat, this.camera.transform.mat4); const camera_project_mat = mat4_product(
this.projection_mat,
this.camera.transform.mat4,
);
this.scene.traverse((node, parent_mat) => { this.scene.traverse((node, parent_mat) => {
const mat = mat4_product(parent_mat, node.transform.mat4); const mat = mat4_product(parent_mat, node.transform.mat4);
if (node.mesh) { if (node.mesh) {
node.mesh.render(pass_encoder, mat); 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);
} }
return mat; return mat;
@ -315,10 +197,7 @@ class InitializedRenderer implements Disposable {
pass_encoder.endPass(); pass_encoder.endPass();
this.device.defaultQueue.submit([command_encoder.finish()]); device.defaultQueue.submit([command_encoder.finish()]);
} }
dispose(): void {
this.scene.destroy();
} }
} }

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 { with_disposer } from "../../../test/src/core/observables/disposable_helpers";
import { TextureController } from "../controllers/TextureController"; import { TextureController } from "../controllers/TextureController";
import { TextureRenderer } from "../rendering/TextureRenderer"; 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.", () => test("Renders correctly without textures.", () =>
with_disposer(disposer => { with_disposer(disposer => {
const ctrl = disposer.add(new TextureController()); const ctrl = disposer.add(new TextureController());
const view = disposer.add( 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."); 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" class="core_RendererWidget"
> >
<canvas <canvas
style="outline: none;" height="600"
tabindex="0" width="800"
/> />
</div> </div>
</div> </div>

View File

@ -54,20 +54,18 @@ export function initialize_viewer(
async () => { async () => {
const { TextureController } = await import("./controllers/TextureController"); const { TextureController } = await import("./controllers/TextureController");
const { TextureView } = await import("./gui/TextureView"); const { TextureView } = await import("./gui/TextureView");
const { TextureRenderer } = await import("./rendering/TextureRenderer");
const controller = disposer.add(new TextureController()); const controller = disposer.add(new TextureController());
let renderer: Renderer; let renderer: Renderer;
if (gui_store.feature_active("webgpu")) { if (gui_store.feature_active("webgpu")) {
const { TextureWebgpuRenderer } = await import("./rendering/TextureWebgpuRenderer"); const { WebgpuRenderer } = await import("../core/rendering/webgpu/WebgpuRenderer");
renderer = new TextureWebgpuRenderer(controller); renderer = new TextureRenderer(controller, new WebgpuRenderer(http_client));
} else if (gui_store.feature_active("webgl")) {
const { TextureWebglRenderer } = await import("./rendering/TextureWebglRenderer");
renderer = new TextureWebglRenderer(controller);
} else { } else {
const { TextureRenderer } = await import("./rendering/TextureRenderer"); const { WebglRenderer } = await import("../core/rendering/webgl/WebglRenderer");
renderer = new TextureRenderer(controller, create_three_renderer()); renderer = new TextureRenderer(controller, new WebglRenderer());
} }
return new TextureView(controller, renderer); 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 { 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 { LogManager } from "../../core/Logger";
import { TextureController } from "../controllers/TextureController"; 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"); 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 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) { constructor(ctrl: TextureController, private readonly renderer: GfxRenderer) {
super(three_renderer); this.canvas_element = renderer.canvas_element;
this.disposer.add_all( this.disposer.add_all(
ctrl.textures.observe(({ value: textures }) => { ctrl.textures.observe(({ value: textures }) => {
this.scene.remove(...this.quad_meshes); renderer.scene.destroy();
renderer.camera.reset();
this.render_textures(textures); this.create_quads(textures);
renderer.schedule_render();
this.reset_camera(new Vector3(0, 0, 5), new Vector3());
this.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 { dispose(): void {
super.dispose(); this.renderer.dispose();
this.disposer.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_width = 10 * (textures.length - 1); // 10px spacing between textures.
let total_height = 0; let total_height = 0;
@ -69,47 +59,61 @@ export class TextureRenderer extends ThreeRenderer implements Disposable {
const y = -Math.floor(total_height / 2); const y = -Math.floor(total_height / 2);
for (const tex of textures) { for (const tex of textures) {
let texture: Texture | undefined = undefined;
try { try {
texture = xvr_texture_to_texture(tex); const quad_mesh = this.create_quad(tex);
} catch (e) {
logger.error("Couldn't convert XVR texture.", e);
}
const quad_mesh = new Mesh( this.renderer.scene.root_node.add_child(
this.create_quad( quad_mesh,
x, new TranslateTransform(x, y + (total_height - tex.height) / 2, 0),
y + Math.floor((total_height - tex.height) / 2),
tex.width,
tex.height,
),
texture
? new MeshBasicMaterial({
map: texture,
transparent: true,
})
: new MeshBasicMaterial({
color: 0xff00ff,
}),
); );
} catch (e) {
this.quad_meshes.push(quad_mesh); logger.error("Couldn't create quad for texture.", e);
this.scene.add(quad_mesh); }
x += 10 + tex.width; x += 10 + tex.width;
} }
} }
private create_quad(x: number, y: number, width: number, height: number): PlaneGeometry { private create_quad(tex: XvrTexture): Mesh {
const quad = new PlaneGeometry(width, height, 1, 1); return this.renderer
quad.faceVertexUvs = [ .mesh_builder(VertexFormat.PosTex)
[ .vertex(0, 0, 0, 0, 1)
[new Vector2(0, 0), new Vector2(0, 1), new Vector2(1, 0)], .vertex(tex.width, 0, 0, 1, 1)
[new Vector2(0, 1), new Vector2(1, 1), new Vector2(1, 0)], .vertex(tex.width, tex.height, 0, 1, 0)
], .vertex(0, tex.height, 0, 0, 0)
];
quad.translate(x + width / 2, y + height / 2, -5); .triangle(0, 1, 2)
return quad; .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: { module: {
rules: [ rules: [
{
test: /^worker-loader!/,
loader: "worker-loader",
options: { name: "worker.[hash].js" },
},
{ {
test: /\.(gif|jpg|png|svg|ttf)$/, test: /\.(gif|jpg|png|svg|ttf)$/,
loader: "file-loader", loader: "file-loader",
}, },
{
test: /\.(vert|frag)$/,
loader: "raw-loader",
},
], ],
}, },
plugins: [ plugins: [

1801
yarn.lock

File diff suppressed because it is too large Load Diff