Improved WebGPU renderer:

- The renderer now uses buffer memory mapping instead of the deprecated setSubData
- It can now render models without texture
- It can now use S3TC textures
This commit is contained in:
Daan Vanden Bosch 2020-04-26 22:19:26 +02:00
parent 7a7957f3d3
commit c9891410d9
27 changed files with 490 additions and 306 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,23 @@
#version 450
precision mediump float;
const vec3 light_pos = normalize(vec3(-1, 1, 1));
const vec4 sky_color = vec4(1, 1, 1, 1);
const vec4 ground_color = vec4(0.1, 0.1, 0.1, 1);
layout(location = 0) in vec3 frag_normal;
layout(location = 0) out vec4 out_color;
void main() {
float cos0 = dot(frag_normal, light_pos);
float a = 0.5 + 0.5 * cos0;
float a_back = 1.0 - a;
if (gl_FrontFacing) {
out_color = mix(ground_color, sky_color, a);
} else {
out_color = mix(ground_color, sky_color, a_back);
}
}

View File

@ -0,0 +1,16 @@
#version 450
layout(set = 0, binding = 0) uniform Uniforms {
mat4 mvp_mat;
mat3 normal_mat;
} uniforms;
layout(location = 0) in vec3 pos;
layout(location = 1) in vec3 normal;
layout(location = 0) out vec3 frag_normal;
void main() {
gl_Position = uniforms.mvp_mat * vec4(pos, 1.0);
frag_normal = normalize(uniforms.normal_mat * normal);
}

View File

@ -5,7 +5,7 @@ layout(set = 0, binding = 0) uniform Uniforms {
} uniforms; } uniforms;
layout(location = 0) in vec3 pos; layout(location = 0) in vec3 pos;
layout(location = 1) in vec2 tex_coords; layout(location = 2) in vec2 tex_coords;
layout(location = 0) out vec2 frag_tex_coords; layout(location = 0) out vec2 frag_tex_coords;

View File

@ -1,7 +1,8 @@
import glsl_module, { ShaderStage } from "@webgpu/glslang"; /* eslint-disable no-console */
import glsl_module, { Glslang, ShaderStage } from "@webgpu/glslang";
import * as fs from "fs"; import * as fs from "fs";
import { RESOURCE_DIR, ASSETS_DIR } from "./index"; import { RESOURCE_DIR, ASSETS_DIR } from "./index";
const glsl = glsl_module(); const glsl = (glsl_module() as any) as Glslang;
const SHADER_RESOURCES_DIR = `${RESOURCE_DIR}/shaders`; const SHADER_RESOURCES_DIR = `${RESOURCE_DIR}/shaders`;
const SHADER_ASSETS_DIR = `${ASSETS_DIR}/shaders`; const SHADER_ASSETS_DIR = `${ASSETS_DIR}/shaders`;
@ -16,6 +17,8 @@ function compile_shader(source_file: string, shader_stage: ShaderStage): void {
} }
for (const file of fs.readdirSync(SHADER_RESOURCES_DIR)) { for (const file of fs.readdirSync(SHADER_RESOURCES_DIR)) {
console.info(`Compiling ${file}.`);
let shader_stage: ShaderStage; let shader_stage: ShaderStage;
switch (file.slice(-4)) { switch (file.slice(-4)) {

View File

@ -11,7 +11,7 @@ export enum Severity {
Off, Off,
} }
export const Severities = enum_values<Severity>(Severity); export const Severities: readonly Severity[] = enum_values(Severity);
export function severity_from_string(str: string): Severity { export function severity_from_string(str: string): Severity {
const severity = (Severity as any)[str.slice(0, 1).toUpperCase() + str.slice(1).toLowerCase()]; const severity = (Severity as any)[str.slice(0, 1).toUpperCase() + str.slice(1).toLowerCase()];

View File

@ -7,7 +7,7 @@ export enum Server {
Ephinea = "Ephinea", Ephinea = "Ephinea",
} }
export const Servers: Server[] = enum_values(Server); export const Servers: readonly Server[] = enum_values(Server);
export enum SectionId { export enum SectionId {
Viridia, Viridia,
@ -22,7 +22,7 @@ export enum SectionId {
Whitill, Whitill,
} }
export const SectionIds: SectionId[] = enum_values(SectionId); export const SectionIds: readonly SectionId[] = enum_values(SectionId);
export enum Difficulty { export enum Difficulty {
Normal, Normal,
@ -31,4 +31,4 @@ export enum Difficulty {
Ultimate, Ultimate,
} }
export const Difficulties: Difficulty[] = enum_values(Difficulty); export const Difficulties: readonly Difficulty[] = enum_values(Difficulty);

View File

@ -1,9 +1,9 @@
import { Texture, TextureFormat } from "./Texture"; import { Texture, TextureFormat } from "./Texture";
import { VertexFormat } from "./VertexFormat"; import { VertexFormatType } from "./VertexFormat";
export interface Gfx<GfxMesh = unknown, GfxTexture = unknown> { export interface Gfx<GfxMesh = unknown, GfxTexture = unknown> {
create_gfx_mesh( create_gfx_mesh(
format: VertexFormat, format: VertexFormatType,
vertex_data: ArrayBuffer, vertex_data: ArrayBuffer,
index_data: ArrayBuffer, index_data: ArrayBuffer,
texture?: Texture, texture?: Texture,

View File

@ -17,9 +17,10 @@ export abstract class GfxRenderer implements Renderer {
abstract readonly gfx: Gfx; abstract readonly gfx: Gfx;
readonly scene = new Scene(); readonly scene = new Scene();
readonly camera: Camera; readonly camera: Camera;
readonly canvas_element: HTMLCanvasElement = document.createElement("canvas"); readonly canvas_element: HTMLCanvasElement;
protected constructor(projection: Projection) { protected constructor(canvas_element: HTMLCanvasElement, projection: Projection) {
this.canvas_element = canvas_element;
this.canvas_element.width = this.width; this.canvas_element.width = this.width;
this.canvas_element.height = this.height; this.canvas_element.height = this.height;
this.canvas_element.addEventListener("mousedown", this.mousedown); this.canvas_element.addEventListener("mousedown", this.mousedown);

View File

@ -1,4 +1,4 @@
import { VertexFormat } from "./VertexFormat"; import { VertexFormatType } from "./VertexFormat";
import { Texture } from "./Texture"; import { Texture } from "./Texture";
import { Gfx } from "./Gfx"; import { Gfx } from "./Gfx";
import { import {
@ -10,16 +10,16 @@ import {
export class Mesh { export class Mesh {
/* eslint-disable no-dupe-class-members */ /* eslint-disable no-dupe-class-members */
static builder(format: VertexFormat.PosNorm): PosNormMeshBuilder; static builder(format: VertexFormatType.PosNorm): PosNormMeshBuilder;
static builder(format: VertexFormat.PosTex): PosTexMeshBuilder; static builder(format: VertexFormatType.PosTex): PosTexMeshBuilder;
static builder(format: VertexFormat.PosNormTex): PosNormTexMeshBuilder; static builder(format: VertexFormatType.PosNormTex): PosNormTexMeshBuilder;
static builder(format: VertexFormat): MeshBuilder { static builder(format: VertexFormatType): MeshBuilder {
switch (format) { switch (format) {
case VertexFormat.PosNorm: case VertexFormatType.PosNorm:
return new PosNormMeshBuilder(); return new PosNormMeshBuilder();
case VertexFormat.PosTex: case VertexFormatType.PosTex:
return new PosTexMeshBuilder(); return new PosTexMeshBuilder();
case VertexFormat.PosNormTex: case VertexFormatType.PosNormTex:
return new PosNormTexMeshBuilder(); return new PosNormTexMeshBuilder();
} }
} }
@ -28,7 +28,7 @@ export class Mesh {
gfx_mesh: unknown; gfx_mesh: unknown;
constructor( constructor(
readonly format: VertexFormat, readonly format: VertexFormatType,
readonly vertex_data: ArrayBuffer, readonly vertex_data: ArrayBuffer,
readonly index_data: ArrayBuffer, readonly index_data: ArrayBuffer,
readonly index_count: number, readonly index_count: number,

View File

@ -1,14 +1,11 @@
import { Texture } from "./Texture"; import { Texture } from "./Texture";
import { import { VERTEX_FORMATS, VertexFormat, VertexFormatType } from "./VertexFormat";
vertex_format_normal_offset,
vertex_format_size,
vertex_format_tex_offset,
VertexFormat,
} from "./VertexFormat";
import { Mesh } from "./Mesh"; import { Mesh } from "./Mesh";
import { Vec2, Vec3 } from "../math/linear_algebra"; import { Vec2, Vec3 } from "../math/linear_algebra";
export abstract class MeshBuilder { export abstract class MeshBuilder {
private readonly format: VertexFormat;
protected readonly vertex_data: { protected readonly vertex_data: {
pos: Vec3; pos: Vec3;
normal?: Vec3; normal?: Vec3;
@ -21,7 +18,9 @@ export abstract class MeshBuilder {
return this.vertex_data.length; return this.vertex_data.length;
} }
protected constructor(private readonly format: VertexFormat) {} protected constructor(format_type: VertexFormatType) {
this.format = VERTEX_FORMATS[format_type];
}
triangle(v1: number, v2: number, v3: number): this { triangle(v1: number, v2: number, v3: number): this {
this.index_data.push(v1, v2, v3); this.index_data.push(v1, v2, v3);
@ -29,9 +28,9 @@ export abstract class MeshBuilder {
} }
build(): Mesh { build(): Mesh {
const v_size = vertex_format_size(this.format); const v_size = this.format.size;
const v_normal_offset = vertex_format_normal_offset(this.format); const v_normal_offset = this.format.normal_offset;
const v_tex_offset = vertex_format_tex_offset(this.format); const v_tex_offset = this.format.tex_offset;
const v_data = new ArrayBuffer(this.vertex_data.length * v_size); const v_data = new ArrayBuffer(this.vertex_data.length * v_size);
const v_view = new DataView(v_data); const v_view = new DataView(v_data);
let i = 0; let i = 0;
@ -41,13 +40,13 @@ export abstract class MeshBuilder {
v_view.setFloat32(i + 4, pos.y, true); v_view.setFloat32(i + 4, pos.y, true);
v_view.setFloat32(i + 8, pos.z, true); v_view.setFloat32(i + 8, pos.z, true);
if (v_normal_offset !== -1) { if (v_normal_offset != undefined) {
v_view.setFloat32(i + v_normal_offset, normal!.x, true); v_view.setFloat32(i + v_normal_offset, normal!.x, true);
v_view.setFloat32(i + v_normal_offset + 4, normal!.y, true); v_view.setFloat32(i + v_normal_offset + 4, normal!.y, true);
v_view.setFloat32(i + v_normal_offset + 8, normal!.z, true); v_view.setFloat32(i + v_normal_offset + 8, normal!.z, true);
} }
if (v_tex_offset !== -1) { if (v_tex_offset != undefined) {
v_view.setUint16(i + v_tex_offset, tex!.x * 0xffff, true); v_view.setUint16(i + v_tex_offset, tex!.x * 0xffff, true);
v_view.setUint16(i + v_tex_offset + 2, tex!.y * 0xffff, true); v_view.setUint16(i + v_tex_offset + 2, tex!.y * 0xffff, true);
} }
@ -56,16 +55,16 @@ export abstract class MeshBuilder {
} }
// Make index data divisible by 4 for WebGPU. // Make index data divisible by 4 for WebGPU.
const i_data = new Uint16Array(2 * Math.ceil(this.index_data.length / 2)); const i_data = new ArrayBuffer(4 * Math.ceil(this.index_data.length / 2));
i_data.set(this.index_data); new Uint16Array(i_data).set(this.index_data);
return new Mesh(this.format, v_data, i_data, this.index_data.length, this._texture); return new Mesh(this.format.type, v_data, i_data, this.index_data.length, this._texture);
} }
} }
export class PosNormMeshBuilder extends MeshBuilder { export class PosNormMeshBuilder extends MeshBuilder {
constructor() { constructor() {
super(VertexFormat.PosNorm); super(VertexFormatType.PosNorm);
} }
vertex(pos: Vec3, normal: Vec3): this { vertex(pos: Vec3, normal: Vec3): this {
@ -76,7 +75,7 @@ export class PosNormMeshBuilder extends MeshBuilder {
export class PosTexMeshBuilder extends MeshBuilder { export class PosTexMeshBuilder extends MeshBuilder {
constructor() { constructor() {
super(VertexFormat.PosTex); super(VertexFormatType.PosTex);
} }
vertex(pos: Vec3, tex: Vec2): this { vertex(pos: Vec3, tex: Vec2): this {
@ -92,7 +91,7 @@ export class PosTexMeshBuilder extends MeshBuilder {
export class PosNormTexMeshBuilder extends MeshBuilder { export class PosNormTexMeshBuilder extends MeshBuilder {
constructor() { constructor() {
super(VertexFormat.PosNormTex); super(VertexFormatType.PosNormTex);
} }
vertex(pos: Vec3, normal: Vec3, tex: Vec2): this { vertex(pos: Vec3, normal: Vec3, tex: Vec2): this {

View File

@ -1,41 +1,41 @@
export enum VertexFormat { export enum VertexFormatType {
PosNorm, PosNorm,
PosTex, PosTex,
PosNormTex, PosNormTex,
} }
export type VertexFormat = {
readonly type: VertexFormatType;
readonly size: number;
readonly normal_offset?: number;
readonly tex_offset?: number;
readonly uniform_buffer_size: number;
};
export const VERTEX_FORMATS: readonly VertexFormat[] = [
{
type: VertexFormatType.PosNorm,
size: 24,
normal_offset: 12,
tex_offset: undefined,
uniform_buffer_size: 4 * (16 + 9),
},
{
type: VertexFormatType.PosTex,
size: 16,
normal_offset: undefined,
tex_offset: 12,
uniform_buffer_size: 4 * 16,
},
// TODO: add VertexFormat for PosNormTex.
// {
// type: VertexFormatType.PosNormTex,
// size: 28,
// normal_offset: 12,
// tex_offset: 24,
// },
];
export const VERTEX_POS_LOC = 0; export const VERTEX_POS_LOC = 0;
export const VERTEX_NORMAL_LOC = 1; export const VERTEX_NORMAL_LOC = 1;
export const VERTEX_TEX_LOC = 2; export const VERTEX_TEX_LOC = 2;
export function vertex_format_size(format: VertexFormat): number {
switch (format) {
case VertexFormat.PosNorm:
return 24;
case VertexFormat.PosTex:
return 16;
case VertexFormat.PosNormTex:
return 28;
}
}
export function vertex_format_normal_offset(format: VertexFormat): number {
switch (format) {
case VertexFormat.PosTex:
return -1;
case VertexFormat.PosNorm:
case VertexFormat.PosNormTex:
return 12;
}
}
export function vertex_format_tex_offset(format: VertexFormat): number {
switch (format) {
case VertexFormat.PosNorm:
return -1;
case VertexFormat.PosTex:
return 12;
case VertexFormat.PosNormTex:
return 24;
}
}

View File

@ -3,7 +3,7 @@ import { NjcmModel } from "../../data_formats/parsing/ninja/njcm";
import { XjModel } from "../../data_formats/parsing/ninja/xj"; import { XjModel } from "../../data_formats/parsing/ninja/xj";
import { vec3_to_math } from "./index"; import { vec3_to_math } from "./index";
import { Mesh } from "../Mesh"; import { Mesh } from "../Mesh";
import { VertexFormat } from "../VertexFormat"; import { VertexFormatType } from "../VertexFormat";
import { EulerOrder, Quat } from "../../math/quaternions"; import { EulerOrder, Quat } from "../../math/quaternions";
import { import {
mat3_vec3_multiply_into, mat3_vec3_multiply_into,
@ -54,7 +54,7 @@ class VerticesHolder {
class MeshCreator { class MeshCreator {
private readonly vertices = new VerticesHolder(); private readonly vertices = new VerticesHolder();
private readonly builder = Mesh.builder(VertexFormat.PosNorm); private readonly builder = Mesh.builder(VertexFormatType.PosNorm);
to_mesh(object: NjObject): Mesh { to_mesh(object: NjObject): Mesh {
this.object_to_mesh(object, Mat4.identity()); this.object_to_mesh(object, Mat4.identity());

View File

@ -1,10 +1,10 @@
import { Mesh } from "./Mesh"; import { Mesh } from "./Mesh";
import { VertexFormat } from "./VertexFormat"; import { VertexFormatType } from "./VertexFormat";
import { Vec3 } from "../math/linear_algebra"; import { Vec3 } from "../math/linear_algebra";
export function cube_mesh(): Mesh { export function cube_mesh(): Mesh {
return ( return (
Mesh.builder(VertexFormat.PosNorm) Mesh.builder(VertexFormatType.PosNorm)
// Front // Front
.vertex(new Vec3(1, 1, -1), new Vec3(0, 0, -1)) .vertex(new Vec3(1, 1, -1), new Vec3(0, 0, -1))
.vertex(new Vec3(-1, 1, -1), new Vec3(0, 0, -1)) .vertex(new Vec3(-1, 1, -1), new Vec3(0, 0, -1))

View File

@ -1,13 +1,11 @@
import { Gfx } from "../Gfx"; import { Gfx } from "../Gfx";
import { Texture, TextureFormat } from "../Texture"; import { Texture, TextureFormat } from "../Texture";
import { import {
vertex_format_normal_offset, VERTEX_FORMATS,
vertex_format_size,
vertex_format_tex_offset,
VERTEX_NORMAL_LOC, VERTEX_NORMAL_LOC,
VERTEX_POS_LOC, VERTEX_POS_LOC,
VERTEX_TEX_LOC, VERTEX_TEX_LOC,
VertexFormat, VertexFormatType,
} from "../VertexFormat"; } from "../VertexFormat";
export type WebglMesh = { export type WebglMesh = {
@ -20,7 +18,7 @@ export class WebglGfx implements Gfx<WebglMesh, WebGLTexture> {
constructor(private readonly gl: WebGL2RenderingContext) {} constructor(private readonly gl: WebGL2RenderingContext) {}
create_gfx_mesh( create_gfx_mesh(
format: VertexFormat, format_type: VertexFormatType,
vertex_data: ArrayBuffer, vertex_data: ArrayBuffer,
index_data: ArrayBuffer, index_data: ArrayBuffer,
texture?: Texture, texture?: Texture,
@ -46,35 +44,32 @@ export class WebglGfx implements Gfx<WebglMesh, WebGLTexture> {
gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer); gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertex_data, gl.STATIC_DRAW); gl.bufferData(gl.ARRAY_BUFFER, vertex_data, gl.STATIC_DRAW);
const vertex_size = vertex_format_size(format); const format = VERTEX_FORMATS[format_type];
const vertex_size = format.size;
gl.vertexAttribPointer(VERTEX_POS_LOC, 3, gl.FLOAT, true, vertex_size, 0); gl.vertexAttribPointer(VERTEX_POS_LOC, 3, gl.FLOAT, true, vertex_size, 0);
gl.enableVertexAttribArray(VERTEX_POS_LOC); gl.enableVertexAttribArray(VERTEX_POS_LOC);
const normal_offset = vertex_format_normal_offset(format); if (format.normal_offset != undefined) {
if (normal_offset !== -1) {
gl.vertexAttribPointer( gl.vertexAttribPointer(
VERTEX_NORMAL_LOC, VERTEX_NORMAL_LOC,
3, 3,
gl.FLOAT, gl.FLOAT,
true, true,
vertex_size, vertex_size,
normal_offset, format.normal_offset,
); );
gl.enableVertexAttribArray(VERTEX_NORMAL_LOC); gl.enableVertexAttribArray(VERTEX_NORMAL_LOC);
} }
const tex_offset = vertex_format_tex_offset(format); if (format.tex_offset != undefined) {
if (tex_offset !== -1) {
gl.vertexAttribPointer( gl.vertexAttribPointer(
VERTEX_TEX_LOC, VERTEX_TEX_LOC,
2, 2,
gl.UNSIGNED_SHORT, gl.UNSIGNED_SHORT,
true, true,
vertex_size, vertex_size,
tex_offset, format.tex_offset,
); );
gl.enableVertexAttribArray(VERTEX_TEX_LOC); gl.enableVertexAttribArray(VERTEX_TEX_LOC);
} }

View File

@ -7,7 +7,7 @@ import pos_tex_frag_shader_source from "./pos_tex.frag";
import { GfxRenderer } from "../GfxRenderer"; import { GfxRenderer } from "../GfxRenderer";
import { WebglGfx, WebglMesh } from "./WebglGfx"; import { WebglGfx, WebglMesh } from "./WebglGfx";
import { Projection } from "../Camera"; import { Projection } from "../Camera";
import { VertexFormat } from "../VertexFormat"; import { VertexFormat, VertexFormatType } from "../VertexFormat";
import { SceneNode } from "../Scene"; import { SceneNode } from "../Scene";
export class WebglRenderer extends GfxRenderer { export class WebglRenderer extends GfxRenderer {
@ -17,7 +17,7 @@ export class WebglRenderer extends GfxRenderer {
readonly gfx: WebglGfx; readonly gfx: WebglGfx;
constructor(projection: Projection) { constructor(projection: Projection) {
super(projection); super(document.createElement("canvas"), projection);
const gl = this.canvas_element.getContext("webgl2"); const gl = this.canvas_element.getContext("webgl2");
@ -32,12 +32,12 @@ export class WebglRenderer extends GfxRenderer {
gl.clearColor(0.1, 0.1, 0.1, 1); gl.clearColor(0.1, 0.1, 0.1, 1);
this.shader_programs = []; this.shader_programs = [];
this.shader_programs[VertexFormat.PosNorm] = new ShaderProgram( this.shader_programs[VertexFormatType.PosNorm] = new ShaderProgram(
gl, gl,
pos_norm_vert_shader_source, pos_norm_vert_shader_source,
pos_norm_frag_shader_source, pos_norm_frag_shader_source,
); );
this.shader_programs[VertexFormat.PosTex] = new ShaderProgram( this.shader_programs[VertexFormatType.PosTex] = new ShaderProgram(
gl, gl,
pos_tex_vert_shader_source, pos_tex_vert_shader_source,
pos_tex_frag_shader_source, pos_tex_frag_shader_source,

View File

@ -1,6 +1,7 @@
import { Gfx } from "../Gfx"; import { Gfx } from "../Gfx";
import { Texture, TextureFormat } from "../Texture"; import { Texture, TextureFormat } from "../Texture";
import { VertexFormat } from "../VertexFormat"; import { VERTEX_FORMATS, VertexFormatType } from "../VertexFormat";
import { assert } from "../../util";
export type WebgpuMesh = { export type WebgpuMesh = {
readonly uniform_buffer: GPUBuffer; readonly uniform_buffer: GPUBuffer;
@ -12,29 +13,38 @@ export type WebgpuMesh = {
export class WebgpuGfx implements Gfx<WebgpuMesh, GPUTexture> { export class WebgpuGfx implements Gfx<WebgpuMesh, GPUTexture> {
constructor( constructor(
private readonly device: GPUDevice, private readonly device: GPUDevice,
private readonly bind_group_layout: GPUBindGroupLayout, private readonly bind_group_layouts: readonly GPUBindGroupLayout[],
) {} ) {}
create_gfx_mesh( create_gfx_mesh(
format: VertexFormat, format_type: VertexFormatType,
vertex_data: ArrayBuffer, vertex_data: ArrayBuffer,
index_data: ArrayBuffer, index_data: ArrayBuffer,
texture?: Texture, texture?: Texture,
): WebgpuMesh { ): WebgpuMesh {
const format = VERTEX_FORMATS[format_type];
const uniform_buffer = this.device.createBuffer({ const uniform_buffer = this.device.createBuffer({
size: 4 * 16, size: format.uniform_buffer_size,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, // eslint-disable-line no-undef usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, // eslint-disable-line no-undef
}); });
const bind_group = this.device.createBindGroup({ const bind_group_entries: GPUBindGroupEntry[] = [
layout: this.bind_group_layout,
entries: [
{ {
binding: 0, binding: 0,
resource: { resource: {
buffer: uniform_buffer, buffer: uniform_buffer,
}, },
}, },
];
if (format.tex_offset != undefined) {
assert(
texture,
() => `Vertex format ${VertexFormatType[format_type]} requires a texture.`,
);
bind_group_entries.push(
{ {
binding: 1, binding: 1,
resource: this.device.createSampler({ resource: this.device.createSampler({
@ -44,24 +54,29 @@ export class WebgpuGfx implements Gfx<WebgpuMesh, GPUTexture> {
}, },
{ {
binding: 2, binding: 2,
resource: (texture!.gfx_texture as GPUTexture).createView(), resource: (texture.gfx_texture as GPUTexture).createView(),
}, },
], );
}
const bind_group = this.device.createBindGroup({
layout: this.bind_group_layouts[format_type],
entries: bind_group_entries,
}); });
const vertex_buffer = this.device.createBuffer({ const [vertex_buffer, vertex_array_buffer] = this.device.createBufferMapped({
size: vertex_data.byteLength, size: vertex_data.byteLength,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX, // eslint-disable-line no-undef usage: GPUBufferUsage.VERTEX, // eslint-disable-line no-undef
}); });
new Uint8Array(vertex_array_buffer).set(new Uint8Array(vertex_data));
vertex_buffer.unmap();
vertex_buffer.setSubData(0, new Uint8Array(vertex_data)); const [index_buffer, index_array_buffer] = this.device.createBufferMapped({
const index_buffer = this.device.createBuffer({
size: index_data.byteLength, size: index_data.byteLength,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.INDEX, // eslint-disable-line no-undef usage: GPUBufferUsage.INDEX, // eslint-disable-line no-undef
}); });
new Uint8Array(index_array_buffer).set(new Uint8Array(index_data));
index_buffer.setSubData(0, new Uint16Array(index_data)); index_buffer.unmap();
return { return {
uniform_buffer, uniform_buffer,
@ -85,20 +100,19 @@ export class WebgpuGfx implements Gfx<WebgpuMesh, GPUTexture> {
height: number, height: number,
data: ArrayBuffer, data: ArrayBuffer,
): GPUTexture { ): GPUTexture {
if (format === TextureFormat.RGBA_S3TC_DXT1 || format === TextureFormat.RGBA_S3TC_DXT3) { let texture_format: string;
// Chrome's WebGPU implementation doesn't support compressed textures yet. Use a dummy let bytes_per_pixel: number;
// texture instead.
const ab = new ArrayBuffer(16);
const ba = new Uint32Array(ab);
ba[0] = 0xffff0000; switch (format) {
ba[1] = 0xff00ff00; case TextureFormat.RGBA_S3TC_DXT1:
ba[2] = 0xff0000ff; texture_format = "bc1-rgba-unorm";
ba[3] = 0xff00ffff; bytes_per_pixel = 2;
break;
width = 2; case TextureFormat.RGBA_S3TC_DXT3:
height = 2; texture_format = "bc2-rgba-unorm";
data = ab; bytes_per_pixel = 4;
break;
} }
const texture = this.device.createTexture({ const texture = this.device.createTexture({
@ -107,18 +121,14 @@ export class WebgpuGfx implements Gfx<WebgpuMesh, GPUTexture> {
height, height,
depth: 1, depth: 1,
}, },
format: "rgba8unorm", format: (texture_format as any) as GPUTextureFormat,
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.SAMPLED, // eslint-disable-line no-undef usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.SAMPLED, // eslint-disable-line no-undef
}); });
// Bytes per row must be a multiple of 256.
const bytes_per_row = Math.ceil((4 * width) / 256) * 256; const bytes_per_row = Math.ceil((4 * width) / 256) * 256;
const data_size = bytes_per_row * height; const data_size = bytes_per_row * 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; let buffer_data: Uint8Array;
if (data_size === data.byteLength) { if (data_size === data.byteLength) {
@ -130,19 +140,23 @@ export class WebgpuGfx implements Gfx<WebgpuMesh, GPUTexture> {
for (let y = 0; y < height; y++) { for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) { for (let x = 0; x < width; x++) {
const idx = 4 * x + bytes_per_row * y; const idx = bytes_per_pixel * x + bytes_per_row * y;
buffer_data[idx] = orig_data[orig_idx]; for (let i = 0; i < bytes_per_pixel; i++) {
buffer_data[idx + 1] = orig_data[orig_idx + 1]; buffer_data[idx + i] = orig_data[orig_idx + i];
buffer_data[idx + 2] = orig_data[orig_idx + 2]; }
buffer_data[idx + 3] = orig_data[orig_idx + 3];
orig_idx += 4; orig_idx += bytes_per_pixel;
} }
} }
} }
buffer.setSubData(0, buffer_data); const [buffer, array_buffer] = this.device.createBufferMapped({
size: data_size,
usage: GPUBufferUsage.COPY_SRC, // eslint-disable-line no-undef
});
new Uint8Array(array_buffer).set(buffer_data);
buffer.unmap();
const command_encoder = this.device.createCommandEncoder(); const command_encoder = this.device.createCommandEncoder();
command_encoder.copyBufferToTexture( command_encoder.copyBufferToTexture(

View File

@ -1,76 +1,77 @@
import { LogManager } from "../../Logger"; import {
import { vertex_format_size, VertexFormat } from "../VertexFormat"; VERTEX_FORMATS,
VERTEX_NORMAL_LOC,
VERTEX_POS_LOC,
VERTEX_TEX_LOC,
VertexFormat,
VertexFormatType,
} from "../VertexFormat";
import { GfxRenderer } from "../GfxRenderer"; import { GfxRenderer } from "../GfxRenderer";
import { mat4_multiply } from "../../math/linear_algebra"; import { Mat4, mat4_multiply } from "../../math/linear_algebra";
import { WebgpuGfx, WebgpuMesh } from "./WebgpuGfx"; import { WebgpuGfx, WebgpuMesh } from "./WebgpuGfx";
import { ShaderLoader } from "./ShaderLoader"; import { ShaderLoader } from "./ShaderLoader";
import { HttpClient } from "../../HttpClient"; import { HttpClient } from "../../HttpClient";
import { Projection } from "../Camera"; import { Projection } from "../Camera";
import { Mesh } from "../Mesh";
const logger = LogManager.get("core/rendering/webgpu/WebgpuRenderer"); type PipelineDetails = {
readonly pipeline: GPURenderPipeline;
readonly bind_group_layout: GPUBindGroupLayout;
};
/** export async function create_webgpu_renderer(
* Uses the experimental WebGPU API for rendering. projection: Projection,
*/ http_client: HttpClient,
export class WebgpuRenderer extends GfxRenderer { ): Promise<WebgpuRenderer> {
private disposed: boolean = false;
/**
* Is defined when the renderer is fully initialized.
*/
private gpu?: {
gfx: WebgpuGfx;
device: GPUDevice;
swap_chain: GPUSwapChain;
pipeline: GPURenderPipeline;
};
private shader_loader: ShaderLoader;
get gfx(): WebgpuGfx {
return this.gpu!.gfx;
}
constructor(projection: Projection, http_client: HttpClient) {
super(projection);
this.shader_loader = new ShaderLoader(http_client);
this.initialize();
}
private async initialize(): Promise<void> {
try {
if (window.navigator.gpu == undefined) { if (window.navigator.gpu == undefined) {
logger.error("WebGPU not supported on this device."); throw new Error("WebGPU not supported on this device.");
return;
} }
const context = this.canvas_element.getContext("gpupresent") as GPUCanvasContext | null; const canvas_element = document.createElement("canvas");
const context = canvas_element.getContext("gpupresent") as GPUCanvasContext | null;
if (context == null) { if (context == null) {
logger.error("Failed to initialize gpupresent context."); throw new Error("Failed to initialize gpupresent context.");
return;
} }
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 vertex_shader_source = await this.shader_loader.load("vertex_shader.vert"); extensions: ["textureCompressionBC"] as any as GPUExtensionName[],
const fragment_shader_source = await this.shader_loader.load("fragment_shader.frag"); });
const shader_loader = new ShaderLoader(http_client);
if (!this.disposed) { const texture_format = "bgra8unorm";
const swap_chain_format = "bgra8unorm";
const swap_chain = context.configureSwapChain({ const swap_chain = context.configureSwapChain({
device: device, device,
format: swap_chain_format, format: texture_format,
}); });
const bind_group_layout = device.createBindGroupLayout({ const pipelines: PipelineDetails[] = await Promise.all(
entries: [ VERTEX_FORMATS.map(format =>
create_pipeline(format, device, texture_format, shader_loader),
),
);
return new WebgpuRenderer(canvas_element, projection, device, swap_chain, pipelines);
}
async function create_pipeline(
format: VertexFormat,
device: GPUDevice,
texture_format: GPUTextureFormat,
shader_loader: ShaderLoader,
): Promise<PipelineDetails> {
const bind_group_layout_entries: GPUBindGroupLayoutEntry[] = [
{ {
binding: 0, binding: 0,
visibility: GPUShaderStage.VERTEX, // eslint-disable-line no-undef visibility: GPUShaderStage.VERTEX, // eslint-disable-line no-undef
type: "uniform-buffer", type: "uniform-buffer",
}, },
];
if (format.tex_offset != undefined) {
bind_group_layout_entries.push(
{ {
binding: 1, binding: 1,
visibility: GPUShaderStage.FRAGMENT, // eslint-disable-line no-undef visibility: GPUShaderStage.FRAGMENT, // eslint-disable-line no-undef
@ -81,9 +82,54 @@ export class WebgpuRenderer extends GfxRenderer {
visibility: GPUShaderStage.FRAGMENT, // eslint-disable-line no-undef visibility: GPUShaderStage.FRAGMENT, // eslint-disable-line no-undef
type: "sampled-texture", type: "sampled-texture",
}, },
], );
}
const bind_group_layout = device.createBindGroupLayout({
entries: bind_group_layout_entries,
}); });
let shader_name: string;
switch (format.type) {
case VertexFormatType.PosNorm:
shader_name = "pos_norm";
break;
case VertexFormatType.PosTex:
shader_name = "pos_tex";
break;
case VertexFormatType.PosNormTex:
shader_name = "pos_norm_tex";
break;
}
const vertex_shader_source = await shader_loader.load(`${shader_name}.vert`);
const fragment_shader_source = await shader_loader.load(`${shader_name}.frag`);
const vertex_attributes: GPUVertexAttributeDescriptor[] = [
{
format: "float3",
offset: 0,
shaderLocation: VERTEX_POS_LOC,
},
];
if (format.normal_offset != undefined) {
vertex_attributes.push({
format: "float3",
offset: format.normal_offset,
shaderLocation: VERTEX_NORMAL_LOC,
});
}
if (format.tex_offset != undefined) {
vertex_attributes.push({
format: "ushort2norm",
offset: format.tex_offset,
shaderLocation: VERTEX_TEX_LOC,
});
}
const pipeline = device.createRenderPipeline({ const pipeline = device.createRenderPipeline({
layout: device.createPipelineLayout({ bindGroupLayouts: [bind_group_layout] }), layout: device.createPipelineLayout({ bindGroupLayouts: [bind_group_layout] }),
vertexStage: { vertexStage: {
@ -99,67 +145,150 @@ export class WebgpuRenderer extends GfxRenderer {
entryPoint: "main", entryPoint: "main",
}, },
primitiveTopology: "triangle-list", primitiveTopology: "triangle-list",
colorStates: [{ format: swap_chain_format }], colorStates: [{ format: texture_format }],
depthStencilState: {
format: "depth24plus",
depthWriteEnabled: true,
depthCompare: "less",
},
vertexState: { vertexState: {
indexFormat: "uint16", indexFormat: "uint16",
vertexBuffers: [ vertexBuffers: [
{ {
arrayStride: vertex_format_size(VertexFormat.PosTex), arrayStride: format.size,
stepMode: "vertex", stepMode: "vertex",
attributes: [ attributes: vertex_attributes,
{
format: "float3",
offset: 0,
shaderLocation: 0,
},
{
format: "ushort2norm",
offset: 12,
shaderLocation: 1,
},
],
}, },
], ],
}, },
}); });
this.gpu = { return { pipeline, bind_group_layout };
gfx: new WebgpuGfx(device, bind_group_layout), }
/**
* Uses the experimental WebGPU API for rendering.
*/
export class WebgpuRenderer extends GfxRenderer {
private disposed: boolean = false;
private depth_texture!: GPUTexture;
readonly gfx: WebgpuGfx;
constructor(
canvas_element: HTMLCanvasElement,
projection: Projection,
private readonly device: GPUDevice,
private readonly swap_chain: GPUSwapChain,
private readonly pipelines: readonly {
pipeline: GPURenderPipeline;
bind_group_layout: GPUBindGroupLayout;
}[],
) {
super(canvas_element, projection);
this.gfx = new WebgpuGfx(
device, device,
swap_chain, pipelines.map(p => p.bind_group_layout),
pipeline, );
};
this.set_size(this.width, this.height); this.set_size(this.width, this.height);
} }
} catch (e) {
logger.error("Failed to initialize WebGPU renderer.", e);
}
}
dispose(): void { dispose(): void {
this.disposed = true; this.disposed = true;
this.depth_texture.destroy();
super.dispose(); super.dispose();
} }
set_size(width: number, height: number): void { set_size(width: number, height: number): void {
// 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;
}
this.depth_texture?.destroy();
this.depth_texture = this.device.createTexture({
size: {
width,
height,
depth: 1,
},
format: "depth24plus",
usage: GPUTextureUsage.OUTPUT_ATTACHMENT | GPUTextureUsage.COPY_SRC, // eslint-disable-line no-undef
});
super.set_size(width, height); super.set_size(width, height);
} }
protected render(): void { protected render(): void {
if (this.gpu) { const command_encoder = this.device.createCommandEncoder();
const { device, swap_chain, pipeline } = this.gpu;
const command_encoder = device.createCommandEncoder(); // Traverse the scene graph and sort the meshes into vertex format-specific buckets.
const texture_view = swap_chain.getCurrentTexture().createView(); const draw_data: { mesh: Mesh; mvp_mat: Mat4 }[][] = VERTEX_FORMATS.map(() => []);
const camera_project_mat = mat4_multiply(
this.camera.projection_matrix,
this.camera.view_matrix,
);
let uniform_buffer_size = 0;
this.scene.traverse((node, parent_mat) => {
const mat = mat4_multiply(parent_mat, node.transform);
if (node.mesh) {
uniform_buffer_size += VERTEX_FORMATS[node.mesh.format].uniform_buffer_size;
draw_data[node.mesh.format].push({
mesh: node.mesh,
mvp_mat: mat,
});
}
return mat;
}, camera_project_mat);
let uniform_buffer: GPUBuffer | undefined;
// Upload uniform data.
if (uniform_buffer_size > 0) {
let uniform_array_buffer: ArrayBuffer;
[uniform_buffer, uniform_array_buffer] = this.device.createBufferMapped({
size: uniform_buffer_size,
usage: GPUBufferUsage.COPY_SRC, // eslint-disable-line no-undef
});
const uniform_array = new Float32Array(uniform_array_buffer);
let uniform_buffer_pos = 0;
for (const vertex_format of VERTEX_FORMATS) {
for (const { mesh, mvp_mat } of draw_data[vertex_format.type]) {
const copy_pos = 4 * uniform_buffer_pos;
uniform_array.set(mvp_mat.data, uniform_buffer_pos);
uniform_buffer_pos += mvp_mat.data.length;
if (vertex_format.normal_offset != undefined) {
const normal_mat = mvp_mat.normal_mat3();
uniform_array.set(normal_mat.data, uniform_buffer_pos);
uniform_buffer_pos += normal_mat.data.length;
}
command_encoder.copyBufferToBuffer(
uniform_buffer,
copy_pos,
(mesh.gfx_mesh as WebgpuMesh).uniform_buffer,
0,
vertex_format.uniform_buffer_size,
);
}
}
uniform_buffer.unmap();
}
const texture_view = this.swap_chain.getCurrentTexture().createView();
const pass_encoder = command_encoder.beginRenderPass({ const pass_encoder = command_encoder.beginRenderPass({
colorAttachments: [ colorAttachments: [
{ {
@ -167,33 +296,31 @@ export class WebgpuRenderer extends GfxRenderer {
loadValue: { r: 0.1, g: 0.1, b: 0.1, a: 1 }, loadValue: { r: 0.1, g: 0.1, b: 0.1, a: 1 },
}, },
], ],
depthStencilAttachment: {
attachment: this.depth_texture.createView(),
depthLoadValue: 1,
depthStoreOp: "store",
stencilLoadValue: "load",
stencilStoreOp: "store",
},
}); });
pass_encoder.setPipeline(pipeline); // Render all meshes per vertex format.
for (const vertex_format of VERTEX_FORMATS) {
pass_encoder.setPipeline(this.pipelines[vertex_format.type].pipeline);
const camera_project_mat = mat4_multiply( for (const { mesh } of draw_data[vertex_format.type]) {
this.camera.projection_matrix, const gfx_mesh = mesh.gfx_mesh as WebgpuMesh;
this.camera.view_matrix,
);
this.scene.traverse((node, parent_mat) => {
const mat = mat4_multiply(parent_mat, node.transform);
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.setBindGroup(0, gfx_mesh.bind_group);
pass_encoder.setVertexBuffer(0, gfx_mesh.vertex_buffer); pass_encoder.setVertexBuffer(0, gfx_mesh.vertex_buffer);
pass_encoder.setIndexBuffer(gfx_mesh.index_buffer); pass_encoder.setIndexBuffer(gfx_mesh.index_buffer);
pass_encoder.drawIndexed(node.mesh.index_count, 1, 0, 0, 0); pass_encoder.drawIndexed(mesh.index_count, 1, 0, 0, 0);
}
} }
return mat;
}, camera_project_mat);
pass_encoder.endPass(); pass_encoder.endPass();
this.device.defaultQueue.submit([command_encoder.finish()]);
device.defaultQueue.submit([command_encoder.finish()]); uniform_buffer?.destroy();
}
} }
} }

View File

@ -10,7 +10,9 @@ export enum QuestEventActionType {
Lock, Lock,
} }
export const QuestEventActionTypes = enum_values<QuestEventActionType>(QuestEventActionType); export const QuestEventActionTypes: readonly QuestEventActionType[] = enum_values(
QuestEventActionType,
);
export type QuestEventActionModel = export type QuestEventActionModel =
| QuestEventActionSpawnNpcsModel | QuestEventActionSpawnNpcsModel

View File

@ -46,12 +46,14 @@ export function initialize_viewer(
let renderer: Renderer; let renderer: Renderer;
if (gui_store.feature_active("webgpu")) { if (gui_store.feature_active("webgpu")) {
const { WebgpuRenderer } = await import("../core/rendering/webgpu/WebgpuRenderer"); const { create_webgpu_renderer } = await import(
"../core/rendering/webgpu/WebgpuRenderer"
);
const { ModelGfxRenderer } = await import("./rendering/ModelGfxRenderer"); const { ModelGfxRenderer } = await import("./rendering/ModelGfxRenderer");
renderer = new ModelGfxRenderer( renderer = new ModelGfxRenderer(
store, store,
new WebgpuRenderer(Projection.Perspective, http_client), await create_webgpu_renderer(Projection.Perspective, http_client),
); );
} else if (gui_store.feature_active("webgl")) { } else if (gui_store.feature_active("webgl")) {
const { WebglRenderer } = await import("../core/rendering/webgl/WebglRenderer"); const { WebglRenderer } = await import("../core/rendering/webgl/WebglRenderer");
@ -82,10 +84,12 @@ export function initialize_viewer(
let renderer: Renderer; let renderer: Renderer;
if (gui_store.feature_active("webgpu")) { if (gui_store.feature_active("webgpu")) {
const { WebgpuRenderer } = await import("../core/rendering/webgpu/WebgpuRenderer"); const { create_webgpu_renderer } = await import(
"../core/rendering/webgpu/WebgpuRenderer"
);
renderer = new TextureRenderer( renderer = new TextureRenderer(
controller, controller,
new WebgpuRenderer(Projection.Orthographic, http_client), await create_webgpu_renderer(Projection.Orthographic, http_client),
); );
} else { } else {
const { WebglRenderer } = await import("../core/rendering/webgl/WebglRenderer"); const { WebglRenderer } = await import("../core/rendering/webgl/WebglRenderer");

View File

@ -2,7 +2,7 @@ import { Disposer } from "../../core/observable/Disposer";
import { LogManager } from "../../core/Logger"; import { LogManager } from "../../core/Logger";
import { TextureController } from "../controllers/texture/TextureController"; import { TextureController } from "../controllers/texture/TextureController";
import { XvrTexture } from "../../core/data_formats/parsing/ninja/texture"; import { XvrTexture } from "../../core/data_formats/parsing/ninja/texture";
import { VertexFormat } from "../../core/rendering/VertexFormat"; import { VertexFormatType } from "../../core/rendering/VertexFormat";
import { Mesh } from "../../core/rendering/Mesh"; import { Mesh } from "../../core/rendering/Mesh";
import { GfxRenderer } from "../../core/rendering/GfxRenderer"; import { GfxRenderer } from "../../core/rendering/GfxRenderer";
import { Renderer } from "../../core/rendering/Renderer"; import { Renderer } from "../../core/rendering/Renderer";
@ -81,7 +81,7 @@ export class TextureRenderer implements Renderer {
} }
private create_quad(tex: XvrTexture): Mesh { private create_quad(tex: XvrTexture): Mesh {
return Mesh.builder(VertexFormat.PosTex) return Mesh.builder(VertexFormatType.PosTex)
.vertex(new Vec3(0, 0, 0), new Vec2(0, 1)) .vertex(new Vec3(0, 0, 0), new Vec2(0, 1))
.vertex(new Vec3(tex.width, 0, 0), new Vec2(1, 1)) .vertex(new Vec3(tex.width, 0, 0), new Vec2(1, 1))

View File

@ -8,7 +8,7 @@ export class StubGfxRenderer extends GfxRenderer {
} }
constructor() { constructor() {
super(Projection.Orthographic); super(document.createElement("canvas"), Projection.Orthographic);
} }
protected render(): void {} // eslint-disable-line protected render(): void {} // eslint-disable-line