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;
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;

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 { 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_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)) {
console.info(`Compiling ${file}.`);
let shader_stage: ShaderStage;
switch (file.slice(-4)) {

View File

@ -11,7 +11,7 @@ export enum Severity {
Off,
}
export const Severities = enum_values<Severity>(Severity);
export const Severities: readonly Severity[] = enum_values(Severity);
export function severity_from_string(str: string): Severity {
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",
}
export const Servers: Server[] = enum_values(Server);
export const Servers: readonly Server[] = enum_values(Server);
export enum SectionId {
Viridia,
@ -22,7 +22,7 @@ export enum SectionId {
Whitill,
}
export const SectionIds: SectionId[] = enum_values(SectionId);
export const SectionIds: readonly SectionId[] = enum_values(SectionId);
export enum Difficulty {
Normal,
@ -31,4 +31,4 @@ export enum Difficulty {
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 { VertexFormat } from "./VertexFormat";
import { VertexFormatType } from "./VertexFormat";
export interface Gfx<GfxMesh = unknown, GfxTexture = unknown> {
create_gfx_mesh(
format: VertexFormat,
format: VertexFormatType,
vertex_data: ArrayBuffer,
index_data: ArrayBuffer,
texture?: Texture,

View File

@ -17,9 +17,10 @@ export abstract class GfxRenderer implements Renderer {
abstract readonly gfx: Gfx;
readonly scene = new Scene();
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.height = this.height;
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 { Gfx } from "./Gfx";
import {
@ -10,16 +10,16 @@ import {
export class Mesh {
/* eslint-disable no-dupe-class-members */
static builder(format: VertexFormat.PosNorm): PosNormMeshBuilder;
static builder(format: VertexFormat.PosTex): PosTexMeshBuilder;
static builder(format: VertexFormat.PosNormTex): PosNormTexMeshBuilder;
static builder(format: VertexFormat): MeshBuilder {
static builder(format: VertexFormatType.PosNorm): PosNormMeshBuilder;
static builder(format: VertexFormatType.PosTex): PosTexMeshBuilder;
static builder(format: VertexFormatType.PosNormTex): PosNormTexMeshBuilder;
static builder(format: VertexFormatType): MeshBuilder {
switch (format) {
case VertexFormat.PosNorm:
case VertexFormatType.PosNorm:
return new PosNormMeshBuilder();
case VertexFormat.PosTex:
case VertexFormatType.PosTex:
return new PosTexMeshBuilder();
case VertexFormat.PosNormTex:
case VertexFormatType.PosNormTex:
return new PosNormTexMeshBuilder();
}
}
@ -28,7 +28,7 @@ export class Mesh {
gfx_mesh: unknown;
constructor(
readonly format: VertexFormat,
readonly format: VertexFormatType,
readonly vertex_data: ArrayBuffer,
readonly index_data: ArrayBuffer,
readonly index_count: number,

View File

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

View File

@ -1,41 +1,41 @@
export enum VertexFormat {
export enum VertexFormatType {
PosNorm,
PosTex,
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_NORMAL_LOC = 1;
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 { vec3_to_math } from "./index";
import { Mesh } from "../Mesh";
import { VertexFormat } from "../VertexFormat";
import { VertexFormatType } from "../VertexFormat";
import { EulerOrder, Quat } from "../../math/quaternions";
import {
mat3_vec3_multiply_into,
@ -54,7 +54,7 @@ class VerticesHolder {
class MeshCreator {
private readonly vertices = new VerticesHolder();
private readonly builder = Mesh.builder(VertexFormat.PosNorm);
private readonly builder = Mesh.builder(VertexFormatType.PosNorm);
to_mesh(object: NjObject): Mesh {
this.object_to_mesh(object, Mat4.identity());

View File

@ -1,10 +1,10 @@
import { Mesh } from "./Mesh";
import { VertexFormat } from "./VertexFormat";
import { VertexFormatType } from "./VertexFormat";
import { Vec3 } from "../math/linear_algebra";
export function cube_mesh(): Mesh {
return (
Mesh.builder(VertexFormat.PosNorm)
Mesh.builder(VertexFormatType.PosNorm)
// Front
.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 { Texture, TextureFormat } from "../Texture";
import {
vertex_format_normal_offset,
vertex_format_size,
vertex_format_tex_offset,
VERTEX_FORMATS,
VERTEX_NORMAL_LOC,
VERTEX_POS_LOC,
VERTEX_TEX_LOC,
VertexFormat,
VertexFormatType,
} from "../VertexFormat";
export type WebglMesh = {
@ -20,7 +18,7 @@ export class WebglGfx implements Gfx<WebglMesh, WebGLTexture> {
constructor(private readonly gl: WebGL2RenderingContext) {}
create_gfx_mesh(
format: VertexFormat,
format_type: VertexFormatType,
vertex_data: ArrayBuffer,
index_data: ArrayBuffer,
texture?: Texture,
@ -46,35 +44,32 @@ export class WebglGfx implements Gfx<WebglMesh, WebGLTexture> {
gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
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.enableVertexAttribArray(VERTEX_POS_LOC);
const normal_offset = vertex_format_normal_offset(format);
if (normal_offset !== -1) {
if (format.normal_offset != undefined) {
gl.vertexAttribPointer(
VERTEX_NORMAL_LOC,
3,
gl.FLOAT,
true,
vertex_size,
normal_offset,
format.normal_offset,
);
gl.enableVertexAttribArray(VERTEX_NORMAL_LOC);
}
const tex_offset = vertex_format_tex_offset(format);
if (tex_offset !== -1) {
if (format.tex_offset != undefined) {
gl.vertexAttribPointer(
VERTEX_TEX_LOC,
2,
gl.UNSIGNED_SHORT,
true,
vertex_size,
tex_offset,
format.tex_offset,
);
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 { WebglGfx, WebglMesh } from "./WebglGfx";
import { Projection } from "../Camera";
import { VertexFormat } from "../VertexFormat";
import { VertexFormat, VertexFormatType } from "../VertexFormat";
import { SceneNode } from "../Scene";
export class WebglRenderer extends GfxRenderer {
@ -17,7 +17,7 @@ export class WebglRenderer extends GfxRenderer {
readonly gfx: WebglGfx;
constructor(projection: Projection) {
super(projection);
super(document.createElement("canvas"), projection);
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);
this.shader_programs = [];
this.shader_programs[VertexFormat.PosNorm] = new ShaderProgram(
this.shader_programs[VertexFormatType.PosNorm] = new ShaderProgram(
gl,
pos_norm_vert_shader_source,
pos_norm_frag_shader_source,
);
this.shader_programs[VertexFormat.PosTex] = new ShaderProgram(
this.shader_programs[VertexFormatType.PosTex] = new ShaderProgram(
gl,
pos_tex_vert_shader_source,
pos_tex_frag_shader_source,

View File

@ -1,6 +1,7 @@
import { Gfx } from "../Gfx";
import { Texture, TextureFormat } from "../Texture";
import { VertexFormat } from "../VertexFormat";
import { VERTEX_FORMATS, VertexFormatType } from "../VertexFormat";
import { assert } from "../../util";
export type WebgpuMesh = {
readonly uniform_buffer: GPUBuffer;
@ -12,29 +13,38 @@ export type WebgpuMesh = {
export class WebgpuGfx implements Gfx<WebgpuMesh, GPUTexture> {
constructor(
private readonly device: GPUDevice,
private readonly bind_group_layout: GPUBindGroupLayout,
private readonly bind_group_layouts: readonly GPUBindGroupLayout[],
) {}
create_gfx_mesh(
format: VertexFormat,
format_type: VertexFormatType,
vertex_data: ArrayBuffer,
index_data: ArrayBuffer,
texture?: Texture,
): WebgpuMesh {
const format = VERTEX_FORMATS[format_type];
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
});
const bind_group = this.device.createBindGroup({
layout: this.bind_group_layout,
entries: [
{
binding: 0,
resource: {
buffer: uniform_buffer,
},
const bind_group_entries: GPUBindGroupEntry[] = [
{
binding: 0,
resource: {
buffer: uniform_buffer,
},
},
];
if (format.tex_offset != undefined) {
assert(
texture,
() => `Vertex format ${VertexFormatType[format_type]} requires a texture.`,
);
bind_group_entries.push(
{
binding: 1,
resource: this.device.createSampler({
@ -44,24 +54,29 @@ export class WebgpuGfx implements Gfx<WebgpuMesh, GPUTexture> {
},
{
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,
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 = this.device.createBuffer({
const [index_buffer, index_array_buffer] = this.device.createBufferMapped({
size: index_data.byteLength,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.INDEX, // eslint-disable-line no-undef
usage: GPUBufferUsage.INDEX, // eslint-disable-line no-undef
});
index_buffer.setSubData(0, new Uint16Array(index_data));
new Uint8Array(index_array_buffer).set(new Uint8Array(index_data));
index_buffer.unmap();
return {
uniform_buffer,
@ -85,20 +100,19 @@ export class WebgpuGfx implements Gfx<WebgpuMesh, GPUTexture> {
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);
let texture_format: string;
let bytes_per_pixel: number;
ba[0] = 0xffff0000;
ba[1] = 0xff00ff00;
ba[2] = 0xff0000ff;
ba[3] = 0xff00ffff;
switch (format) {
case TextureFormat.RGBA_S3TC_DXT1:
texture_format = "bc1-rgba-unorm";
bytes_per_pixel = 2;
break;
width = 2;
height = 2;
data = ab;
case TextureFormat.RGBA_S3TC_DXT3:
texture_format = "bc2-rgba-unorm";
bytes_per_pixel = 4;
break;
}
const texture = this.device.createTexture({
@ -107,18 +121,14 @@ export class WebgpuGfx implements Gfx<WebgpuMesh, GPUTexture> {
height,
depth: 1,
},
format: "rgba8unorm",
format: (texture_format as any) as GPUTextureFormat,
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 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;
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 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];
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];
for (let i = 0; i < bytes_per_pixel; i++) {
buffer_data[idx + i] = orig_data[orig_idx + i];
}
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();
command_encoder.copyBufferToTexture(

View File

@ -1,199 +1,326 @@
import { LogManager } from "../../Logger";
import { vertex_format_size, VertexFormat } from "../VertexFormat";
import {
VERTEX_FORMATS,
VERTEX_NORMAL_LOC,
VERTEX_POS_LOC,
VERTEX_TEX_LOC,
VertexFormat,
VertexFormatType,
} from "../VertexFormat";
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 { ShaderLoader } from "./ShaderLoader";
import { HttpClient } from "../../HttpClient";
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(
projection: Projection,
http_client: HttpClient,
): Promise<WebgpuRenderer> {
if (window.navigator.gpu == undefined) {
throw new Error("WebGPU not supported on this device.");
}
const canvas_element = document.createElement("canvas");
const context = canvas_element.getContext("gpupresent") as GPUCanvasContext | null;
if (context == null) {
throw new Error("Failed to initialize gpupresent context.");
}
const adapter = await window.navigator.gpu.requestAdapter();
const device = await adapter.requestDevice({
extensions: ["textureCompressionBC"] as any as GPUExtensionName[],
});
const shader_loader = new ShaderLoader(http_client);
const texture_format = "bgra8unorm";
const swap_chain = context.configureSwapChain({
device,
format: texture_format,
});
const pipelines: PipelineDetails[] = await Promise.all(
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,
visibility: GPUShaderStage.VERTEX, // eslint-disable-line no-undef
type: "uniform-buffer",
},
];
if (format.tex_offset != undefined) {
bind_group_layout_entries.push(
{
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 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({
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: texture_format }],
depthStencilState: {
format: "depth24plus",
depthWriteEnabled: true,
depthCompare: "less",
},
vertexState: {
indexFormat: "uint16",
vertexBuffers: [
{
arrayStride: format.size,
stepMode: "vertex",
attributes: vertex_attributes,
},
],
},
});
return { pipeline, bind_group_layout };
}
/**
* Uses the experimental WebGPU API for rendering.
*/
export class WebgpuRenderer extends GfxRenderer {
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;
private depth_texture!: GPUTexture;
get gfx(): WebgpuGfx {
return this.gpu!.gfx;
}
readonly gfx: WebgpuGfx;
constructor(projection: Projection, http_client: HttpClient) {
super(projection);
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.shader_loader = new ShaderLoader(http_client);
this.gfx = new WebgpuGfx(
device,
pipelines.map(p => p.bind_group_layout),
);
this.initialize();
}
private async initialize(): Promise<void> {
try {
if (window.navigator.gpu == undefined) {
logger.error("WebGPU not supported on this device.");
return;
}
const context = this.canvas_element.getContext("gpupresent") as GPUCanvasContext | null;
if (context == null) {
logger.error("Failed to initialize gpupresent context.");
return;
}
const adapter = await window.navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const 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) {
const swap_chain_format = "bgra8unorm";
const swap_chain = context.configureSwapChain({
device: device,
format: swap_chain_format,
});
const bind_group_layout = device.createBindGroupLayout({
entries: [
{
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,
swap_chain,
pipeline,
};
this.set_size(this.width, this.height);
}
} catch (e) {
logger.error("Failed to initialize WebGPU renderer.", e);
}
this.set_size(this.width, this.height);
}
dispose(): void {
this.disposed = true;
this.depth_texture.destroy();
super.dispose();
}
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.height = height;
}
this.canvas_element.width = width;
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);
}
protected render(): void {
if (this.gpu) {
const { device, swap_chain, pipeline } = this.gpu;
const command_encoder = this.device.createCommandEncoder();
const command_encoder = device.createCommandEncoder();
const texture_view = swap_chain.getCurrentTexture().createView();
// Traverse the scene graph and sort the meshes into vertex format-specific buckets.
const draw_data: { mesh: Mesh; mvp_mat: Mat4 }[][] = VERTEX_FORMATS.map(() => []);
const pass_encoder = command_encoder.beginRenderPass({
colorAttachments: [
{
attachment: texture_view,
loadValue: { r: 0.1, g: 0.1, b: 0.1, a: 1 },
},
],
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
});
pass_encoder.setPipeline(pipeline);
const uniform_array = new Float32Array(uniform_array_buffer);
let uniform_buffer_pos = 0;
const camera_project_mat = mat4_multiply(
this.camera.projection_matrix,
this.camera.view_matrix,
);
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;
this.scene.traverse((node, parent_mat) => {
const mat = mat4_multiply(parent_mat, node.transform);
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;
}
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);
command_encoder.copyBufferToBuffer(
uniform_buffer,
copy_pos,
(mesh.gfx_mesh as WebgpuMesh).uniform_buffer,
0,
vertex_format.uniform_buffer_size,
);
}
}
return mat;
}, camera_project_mat);
pass_encoder.endPass();
device.defaultQueue.submit([command_encoder.finish()]);
uniform_buffer.unmap();
}
const texture_view = this.swap_chain.getCurrentTexture().createView();
const pass_encoder = command_encoder.beginRenderPass({
colorAttachments: [
{
attachment: texture_view,
loadValue: { r: 0.1, g: 0.1, b: 0.1, a: 1 },
},
],
depthStencilAttachment: {
attachment: this.depth_texture.createView(),
depthLoadValue: 1,
depthStoreOp: "store",
stencilLoadValue: "load",
stencilStoreOp: "store",
},
});
// Render all meshes per vertex format.
for (const vertex_format of VERTEX_FORMATS) {
pass_encoder.setPipeline(this.pipelines[vertex_format.type].pipeline);
for (const { mesh } of draw_data[vertex_format.type]) {
const gfx_mesh = mesh.gfx_mesh as WebgpuMesh;
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(mesh.index_count, 1, 0, 0, 0);
}
}
pass_encoder.endPass();
this.device.defaultQueue.submit([command_encoder.finish()]);
uniform_buffer?.destroy();
}
}

View File

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

View File

@ -46,12 +46,14 @@ export function initialize_viewer(
let renderer: Renderer;
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");
renderer = new ModelGfxRenderer(
store,
new WebgpuRenderer(Projection.Perspective, http_client),
await create_webgpu_renderer(Projection.Perspective, http_client),
);
} else if (gui_store.feature_active("webgl")) {
const { WebglRenderer } = await import("../core/rendering/webgl/WebglRenderer");
@ -82,10 +84,12 @@ export function initialize_viewer(
let renderer: Renderer;
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(
controller,
new WebgpuRenderer(Projection.Orthographic, http_client),
await create_webgpu_renderer(Projection.Orthographic, http_client),
);
} else {
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 { TextureController } from "../controllers/texture/TextureController";
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 { GfxRenderer } from "../../core/rendering/GfxRenderer";
import { Renderer } from "../../core/rendering/Renderer";
@ -81,7 +81,7 @@ export class TextureRenderer implements Renderer {
}
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(tex.width, 0, 0), new Vec2(1, 1))

View File

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