mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
WebGPU renderer can now render textures. WebGL and WebGPU renderers now reuse more code.
This commit is contained in:
parent
baffab3234
commit
a19a3a4837
BIN
assets/shaders/fragment_shader.frag.spv
Normal file
BIN
assets/shaders/fragment_shader.frag.spv
Normal file
Binary file not shown.
BIN
assets/shaders/vertex_shader.vert.spv
Normal file
BIN
assets/shaders/vertex_shader.vert.spv
Normal file
Binary file not shown.
15
assets_generation/resources/shaders/fragment_shader.frag
Normal file
15
assets_generation/resources/shaders/fragment_shader.frag
Normal 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);
|
||||
}
|
15
assets_generation/resources/shaders/vertex_shader.vert
Normal file
15
assets_generation/resources/shaders/vertex_shader.vert
Normal 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;
|
||||
}
|
33
assets_generation/update_shaders.ts
Normal file
33
assets_generation/update_shaders.ts
Normal 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);
|
||||
}
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
10
package.json
10
package.json
@ -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
22
src/core/rendering/Gfx.ts
Normal 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;
|
||||
}
|
110
src/core/rendering/GfxRenderer.ts
Normal file
110
src/core/rendering/GfxRenderer.ts
Normal 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();
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
55
src/core/rendering/Scene.ts
Normal file
55
src/core/rendering/Scene.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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}.`);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
export type GL = WebGL2RenderingContext;
|
||||
|
||||
export enum VertexFormat {
|
||||
Pos,
|
||||
PosTex,
|
||||
|
@ -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);
|
||||
}
|
||||
`;
|
149
src/core/rendering/webgl/WebglGfx.ts
Normal file
149
src/core/rendering/webgl/WebglGfx.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
9
src/core/rendering/webgl/pos.frag
Normal file
9
src/core/rendering/webgl/pos.frag
Normal file
@ -0,0 +1,9 @@
|
||||
#version 300 es
|
||||
|
||||
precision mediump float;
|
||||
|
||||
out vec4 frag_color;
|
||||
|
||||
void main() {
|
||||
frag_color = vec4(0, 1, 1, 1);
|
||||
}
|
11
src/core/rendering/webgl/pos.vert
Normal file
11
src/core/rendering/webgl/pos.vert
Normal file
@ -0,0 +1,11 @@
|
||||
#version 300 es
|
||||
|
||||
precision mediump float;
|
||||
|
||||
uniform mat4 transform;
|
||||
|
||||
in vec4 pos;
|
||||
|
||||
void main() {
|
||||
gl_Position = transform * pos;
|
||||
}
|
13
src/core/rendering/webgl/pos_tex.frag
Normal file
13
src/core/rendering/webgl/pos_tex.frag
Normal 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);
|
||||
}
|
15
src/core/rendering/webgl/pos_tex.vert
Normal file
15
src/core/rendering/webgl/pos_tex.vert
Normal 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;
|
||||
}
|
8
src/core/rendering/webgl/shader_sources.ts
Normal file
8
src/core/rendering/webgl/shader_sources.ts
Normal 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 = ``;
|
9
src/core/rendering/webgpu/ShaderLoader.ts
Normal file
9
src/core/rendering/webgpu/ShaderLoader.ts
Normal 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());
|
||||
}
|
||||
}
|
173
src/core/rendering/webgpu/WebgpuGfx.ts
Normal file
173
src/core/rendering/webgpu/WebgpuGfx.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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.");
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
@ -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));
|
||||
}
|
14
test/src/core/rendering/StubGfxRenderer.ts
Normal file
14
test/src/core/rendering/StubGfxRenderer.ts
Normal 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
9
typedefs/shaders.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
const shader_source: string;
|
||||
|
||||
declare module "*.vert" {
|
||||
export default shader_source;
|
||||
}
|
||||
|
||||
declare module "*.frag" {
|
||||
export default shader_source;
|
||||
}
|
@ -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: [
|
||||
|
Loading…
Reference in New Issue
Block a user