Removed custom WebGL and WebGPU renderers. All 3D rendering is now done by THREE.js again.

This commit is contained in:
Daan Vanden Bosch 2020-07-14 21:50:35 +02:00
parent 2eaf4fe455
commit 767397d26d
60 changed files with 422 additions and 3107 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

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

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

View File

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

View File

@ -1,36 +0,0 @@
/* 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() as any) as Glslang;
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)) {
console.info(`Compiling ${file}.`);
let shader_stage: ShaderStage;
switch (file.slice(-4)) {
case "vert":
shader_stage = "vertex";
break;
case "frag":
shader_stage = "fragment";
break;
default:
throw new Error(`Unsupported shader type: ${file.slice(-4)}`);
}
compile_shader(file, shader_stage);
}

View File

@ -26,7 +26,6 @@
"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": {
@ -38,8 +37,6 @@
"@types/node-fetch": "^2.5.7",
"@typescript-eslint/eslint-plugin": "^3.6.1",
"@typescript-eslint/parser": "^3.6.1",
"@webgpu/glslang": "^0.0.15",
"@webgpu/types": "^0.0.27",
"cheerio": "^1.0.0-rc.3",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^6.0.3",
@ -58,7 +55,6 @@
"node-fetch": "^2.6.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"prettier": "^2.0.5",
"raw-loader": "^4.0.1",
"terser-webpack-plugin": "^2.3.7",
"ts-jest": "^26.1.2",
"ts-loader": "^8.0.0",

View File

@ -5,7 +5,7 @@ import { timeout } from "../../test/src/utils";
import { Random } from "../core/Random";
import { Severity } from "../core/Severity";
import { StubClock } from "../../test/src/core/StubClock";
import { STUB_THREE_RENDERER } from "../../test/src/core/rendering/StubThreeRenderer";
import { STUB_RENDERER } from "../../test/src/core/rendering/StubRenderer";
for (const path of [undefined, "/viewer", "/quest_editor", "/hunt_optimizer"]) {
const with_path = path == undefined ? "without specific path" : `with path ${path}`;
@ -28,7 +28,7 @@ for (const path of [undefined, "/viewer", "/quest_editor", "/hunt_optimizer"]) {
new FileSystemHttpClient(),
new Random(() => 0.27),
new StubClock(new Date("2020-01-01T15:40:20Z")),
() => STUB_THREE_RENDERER,
() => STUB_RENDERER,
);
expect(app).toBeDefined();

View File

@ -5,7 +5,7 @@ import { create_item_type_stores } from "../core/stores/ItemTypeStore";
import { create_item_drop_stores } from "../hunt_optimizer/stores/ItemDropStore";
import { ApplicationView } from "./gui/ApplicationView";
import { throttle } from "lodash";
import { DisposableThreeRenderer } from "../core/rendering/ThreeRenderer";
import { DisposableThreeRenderer } from "../core/rendering/Renderer";
import { Disposer } from "../core/observable/Disposer";
import { disposable_custom_listener, disposable_listener } from "../core/gui/dom";
import { Random } from "../core/Random";

View File

@ -1,446 +0,0 @@
import { assert } from "../util";
import { Quat } from "./quaternions";
export class Vec2 {
constructor(public x: number, public y: number) {}
get u(): number {
return this.x;
}
get v(): number {
return this.y;
}
}
export function vec2_diff(v: Vec2, w: Vec2): Vec2 {
return new Vec2(v.x - w.x, v.y - w.y);
}
export class Vec3 {
constructor(public x: number, public y: number, public z: number) {}
magnitude(): number {
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
}
normalize(): void {
const inv_mag = 1 / this.magnitude();
this.x *= inv_mag;
this.y *= inv_mag;
this.z *= inv_mag;
}
}
export function vec3_sub(v: Vec3, w: Vec3): Vec3 {
return new Vec3(v.x - w.x, v.y - w.y, v.z - w.z);
}
/**
* Computes the distance between points `p` and `q`. Equivalent to `vec3_diff(p, q).magnitude()`.
*/
export function vec3_dist(p: Vec3, q: Vec3): number {
const x = p.x - q.x;
const y = p.y - q.y;
const z = p.z - q.z;
return Math.sqrt(x * x + y * y + z * z);
}
/**
* Computes the cross product of `p` and `q`.
*/
export function vec3_cross(p: Vec3, q: Vec3): Vec3 {
return new Vec3(p.y * q.z - p.z * q.y, p.z * q.x - p.x * q.z, p.x * q.y - p.y * q.x);
}
/**
* Computes the dot product of `p` and `q`.
*/
export function vec3_dot(p: Vec3, q: Vec3): number {
return p.x * q.x + p.y * q.y + p.z * q.z;
}
/**
* Computes the cross product of `p` and `q` and stores it in `result`.
*/
export function vec3_cross_into(p: Vec3, q: Vec3, result: Vec3): void {
const x = p.y * q.z - p.z * q.y;
const y = p.z * q.x - p.x * q.z;
const z = p.x * q.y - p.y * q.x;
result.x = x;
result.y = y;
result.z = z;
}
/**
* Stores data in column-major order.
*/
export class Mat3 {
// prettier-ignore
static of(
m00: number, m01: number, m02: number,
m10: number, m11: number, m12: number,
m20: number, m21: number, m22: number,
): Mat3 {
return new Mat3(new Float32Array([
m00, m10, m20,
m01, m11, m21,
m02, m12, m22,
]));
}
static identity(): Mat3 {
// prettier-ignore
return Mat3.of(
1, 0, 0,
0, 1, 0,
0, 0, 1,
)
}
constructor(readonly data: Float32Array) {
assert(data.length === 9, "data should be of length 9.");
}
get(i: number, j: number): number {
return this.data[i + j * 3];
}
set(i: number, j: number, value: number): void {
this.data[i + j * 3] = value;
}
/**
* @returns a copy of this matrix.
*/
clone(): Mat3 {
return new Mat3(new Float32Array(this.data));
}
/**
* Transposes this matrix in-place.
*/
transpose(): void {
let tmp: number;
const m = this.data;
tmp = m[1];
m[1] = m[3];
m[3] = tmp;
tmp = m[2];
m[2] = m[6];
m[6] = tmp;
tmp = m[5];
m[5] = m[7];
m[7] = tmp;
}
/**
* Computes the inverse of this matrix and returns it as a new {@link Mat3}.
*
* @returns the inverse of this matrix.
*/
inverse(): Mat3 {
const m = this.clone();
m.invert();
return m;
}
/**
* Computes the inverse of this matrix in-place. Will revert to identity if this matrix is
* degenerate.
*/
invert(): void {
const n11 = this.data[0];
const n21 = this.data[1];
const n31 = this.data[2];
const n12 = this.data[3];
const n22 = this.data[4];
const n32 = this.data[5];
const n13 = this.data[6];
const n23 = this.data[7];
const n33 = this.data[8];
const t11 = n33 * n22 - n32 * n23;
const t12 = n32 * n13 - n33 * n12;
const t13 = n23 * n12 - n22 * n13;
const det = n11 * t11 + n21 * t12 + n31 * t13;
if (det === 0) {
// Revert to identity if matrix is degenerate.
this.data[0] = 1;
this.data[1] = 0;
this.data[2] = 0;
this.data[3] = 0;
this.data[4] = 1;
this.data[5] = 0;
this.data[6] = 0;
this.data[7] = 0;
this.data[8] = 1;
return;
}
const det_inv = 1 / det;
this.data[0] = t11 * det_inv;
this.data[1] = (n31 * n23 - n33 * n21) * det_inv;
this.data[2] = (n32 * n21 - n31 * n22) * det_inv;
this.data[3] = t12 * det_inv;
this.data[4] = (n33 * n11 - n31 * n13) * det_inv;
this.data[5] = (n31 * n12 - n32 * n11) * det_inv;
this.data[6] = t13 * det_inv;
this.data[7] = (n21 * n13 - n23 * n11) * det_inv;
this.data[8] = (n22 * n11 - n21 * n12) * det_inv;
}
}
export function mat3_multiply(a: Mat3, b: Mat3): Mat3 {
const c = new Mat3(new Float32Array(9));
mat3_product_into_array(c.data, a, b);
return c;
}
export function mat3_multiply_into(a: Mat3, b: Mat3, result: Mat3): void {
const array = new Float32Array(9);
mat3_product_into_array(array, a, b);
result.data.set(array);
}
function mat3_product_into_array(array: Float32Array, a: Mat3, b: Mat3): void {
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
for (let k = 0; k < 3; k++) {
array[i + j * 3] += a.data[i + k * 3] * b.data[k + j * 3];
}
}
}
}
/**
* Computes the product of `m` and `v` and stores it in `result`.
*/
export function mat3_vec3_multiply_into(m: Mat3, v: Vec3, result: Vec3): void {
const x = m.get(0, 0) * v.x + m.get(0, 1) * v.y + m.get(0, 2) * v.z;
const y = m.get(1, 0) * v.x + m.get(1, 1) * v.y + m.get(1, 2) * v.z;
const z = m.get(2, 0) * v.x + m.get(2, 1) * v.y + m.get(2, 2) * v.z;
result.x = x;
result.y = y;
result.z = z;
}
/**
* Stores data in column-major order.
*/
export class Mat4 {
// prettier-ignore
static of(
m00: number, m01: number, m02: number, m03: number,
m10: number, m11: number, m12: number, m13: number,
m20: number, m21: number, m22: number, m23: number,
m30: number, m31: number, m32: number, m33: number,
): Mat4 {
return new Mat4(new Float32Array([
m00, m10, m20, m30,
m01, m11, m21, m31,
m02, m12, m22, m32,
m03, m13, m23, m33,
]));
}
static identity(): Mat4 {
// prettier-ignore
return Mat4.of(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
)
}
static translation(x: number, y: number, z: number): Mat4 {
// prettier-ignore
return Mat4.of(
1, 0, 0, x,
0, 1, 0, y,
0, 0, 1, z,
0, 0, 0, 1,
);
}
static scale(x: number, y: number, z: number): Mat4 {
// prettier-ignore
return Mat4.of(
x, 0, 0, 1,
0, y, 0, 1,
0, 0, z, 1,
0, 0, 0, 1,
);
}
static compose(translation: Vec3, rotation: Quat, scale: Vec3): Mat4 {
const w = rotation.w;
const x = rotation.x;
const y = rotation.y;
const z = rotation.z;
const x2 = x + x;
const y2 = y + y;
const z2 = z + z;
const xx = x * x2;
const xy = x * y2;
const xz = x * z2;
const yy = y * y2;
const yz = y * z2;
const zz = z * z2;
const wx = w * x2;
const wy = w * y2;
const wz = w * z2;
const sx = scale.x;
const sy = scale.y;
const sz = scale.z;
// prettier-ignore
return Mat4.of(
(1 - (yy + zz)) * sx, (xy - wz) * sy, (xz + wy) * sz, translation.x,
(xy + wz) * sx, (1 - (xx + zz)) * sy, (yz - wx) * sz, translation.y,
(xz - wy) * sx, (yz + wx) * sy, (1 - (xx + yy)) * sz, translation.z,
0, 0, 0, 1
)
}
constructor(readonly data: Float32Array) {
assert(data.length === 16, "data should be of length 16.");
}
get(i: number, j: number): number {
return this.data[i + j * 4];
}
set(i: number, j: number, value: number): void {
this.data[i + j * 4] = value;
}
// prettier-ignore
set_all(
m00: number, m01: number, m02: number, m03: number,
m10: number, m11: number, m12: number, m13: number,
m20: number, m21: number, m22: number, m23: number,
m30: number, m31: number, m32: number, m33: number,
):void {
this.data[0] = m00;
this.data[1] = m10;
this.data[2] = m20;
this.data[3] = m30;
this.data[4] = m01;
this.data[5] = m11;
this.data[6] = m21;
this.data[7] = m31;
this.data[8] = m02;
this.data[9] = m12;
this.data[10] = m22;
this.data[11] = m32;
this.data[12] = m03;
this.data[13] = m13;
this.data[14] = m23;
this.data[15] = m33;
}
/**
* Transposes this matrix in-place.
*/
transpose(): void {
let tmp: number;
const m = this.data;
tmp = m[1];
m[1] = m[4];
m[4] = tmp;
tmp = m[2];
m[2] = m[8];
m[8] = tmp;
tmp = m[6];
m[6] = m[9];
m[9] = tmp;
tmp = m[3];
m[3] = m[12];
m[12] = tmp;
tmp = m[7];
m[7] = m[13];
m[13] = tmp;
tmp = m[11];
m[11] = m[14];
m[14] = tmp;
}
clone(): Mat4 {
return new Mat4(new Float32Array(this.data));
}
/**
* Computes a 3 x 3 surface normal transformation matrix.
*/
normal_mat3(): Mat3 {
// prettier-ignore
const m = Mat3.of(
this.data[0], this.data[4], this.data[8],
this.data[1], this.data[5], this.data[9],
this.data[2], this.data[6], this.data[10],
);
m.invert();
m.transpose();
return m;
}
}
export function mat4_multiply(a: Mat4, b: Mat4): Mat4 {
const c = new Mat4(new Float32Array(16));
mat4_product_into_array(c.data, a, b);
return c;
}
/**
* Computes the product of `a` and `b` and stores it in `result`.
*/
export function mat4_multiply_into(a: Mat4, b: Mat4, result: Mat4): void {
const array = new Float32Array(16);
mat4_product_into_array(array, a, b);
result.data.set(array);
}
function mat4_product_into_array(array: Float32Array, a: Mat4, b: Mat4): void {
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
for (let k = 0; k < 4; k++) {
array[i + j * 4] += a.data[i + k * 4] * b.data[k + j * 4];
}
}
}
}
/**
* Computes the product of `m` and `v` and stores it in `result`. Assumes `m` is affine.
*/
export function mat4_vec3_multiply_into(m: Mat4, v: Vec3, result: Vec3): void {
const x = m.get(0, 0) * v.x + m.get(0, 1) * v.y + m.get(0, 2) * v.z + m.get(0, 3);
const y = m.get(1, 0) * v.x + m.get(1, 1) * v.y + m.get(1, 2) * v.z + m.get(1, 3);
const z = m.get(2, 0) * v.x + m.get(2, 1) * v.y + m.get(2, 2) * v.z + m.get(2, 3);
result.x = x;
result.y = y;
result.z = z;
}

View File

@ -1,31 +0,0 @@
import { EulerOrder, Quat, quat_product } from "./quaternions";
test("euler_angles ZYX order", () => {
for (let angle = 0; angle < 2 * Math.PI; angle += Math.PI / 360) {
const x = Quat.euler_angles(angle, 0, 0, EulerOrder.ZYX);
const y = Quat.euler_angles(0, angle, 0, EulerOrder.ZYX);
const z = Quat.euler_angles(0, 0, angle, EulerOrder.ZYX);
const q = quat_product(quat_product(z, y), x);
const q2 = Quat.euler_angles(angle, angle, angle, EulerOrder.ZYX);
expect(q.w).toBeCloseTo(q2.w, 5);
expect(q.x).toBeCloseTo(q2.x, 5);
expect(q.y).toBeCloseTo(q2.y, 5);
expect(q.z).toBeCloseTo(q2.z, 5);
}
});
test("euler_angles ZXY order", () => {
for (let angle = 0; angle < 2 * Math.PI; angle += Math.PI / 360) {
const x = Quat.euler_angles(angle, 0, 0, EulerOrder.ZXY);
const y = Quat.euler_angles(0, angle, 0, EulerOrder.ZXY);
const z = Quat.euler_angles(0, 0, angle, EulerOrder.ZXY);
const q = quat_product(quat_product(z, x), y);
const q2 = Quat.euler_angles(angle, angle, angle, EulerOrder.ZXY);
expect(q.w).toBeCloseTo(q2.w, 5);
expect(q.x).toBeCloseTo(q2.x, 5);
expect(q.y).toBeCloseTo(q2.y, 5);
expect(q.z).toBeCloseTo(q2.z, 5);
}
});

View File

@ -1,57 +0,0 @@
export enum EulerOrder {
ZXY,
ZYX,
}
export class Quat {
/**
* Creates a quaternion from Euler angles.
*
* @param x - Rotation around the x-axis in radians.
* @param y - Rotation around the y-axis in radians.
* @param z - Rotation around the z-axis in radians.
* @param order - Order in which rotations are applied.
*/
static euler_angles(x: number, y: number, z: number, order: EulerOrder): Quat {
const cos_x = Math.cos(x * 0.5);
const sin_x = Math.sin(x * 0.5);
const cos_y = Math.cos(y * 0.5);
const sin_y = Math.sin(y * 0.5);
const cos_z = Math.cos(z * 0.5);
const sin_z = Math.sin(z * 0.5);
switch (order) {
case EulerOrder.ZXY:
return new Quat(
cos_x * cos_y * cos_z - sin_x * sin_y * sin_z,
sin_x * cos_y * cos_z - cos_x * sin_y * sin_z,
cos_x * sin_y * cos_z + sin_x * cos_y * sin_z,
cos_x * cos_y * sin_z + sin_x * sin_y * cos_z,
);
case EulerOrder.ZYX:
return new Quat(
cos_x * cos_y * cos_z + sin_x * sin_y * sin_z,
sin_x * cos_y * cos_z - cos_x * sin_y * sin_z,
cos_x * sin_y * cos_z + sin_x * cos_y * sin_z,
cos_x * cos_y * sin_z - sin_x * sin_y * cos_z,
);
}
}
constructor(public w: number, public x: number, public y: number, public z: number) {}
conjugate(): void {
this.x *= -1;
this.y *= -1;
this.z *= -1;
}
}
export function quat_product(p: Quat, q: Quat): Quat {
return new Quat(
p.w * q.w - p.x * q.x - p.y * q.y - p.z * q.z,
p.w * q.x + p.x * q.w + p.y * q.z - p.z * q.y,
p.w * q.y - p.x * q.z + p.y * q.w + p.z * q.x,
p.w * q.z + p.x * q.y - p.y * q.x + p.z * q.w,
);
}

View File

@ -1,168 +0,0 @@
import { Mat4, Vec3, vec3_cross, vec3_dot, vec3_sub } from "../math/linear_algebra";
import { clamp, deg_to_rad } from "../math";
export enum Projection {
Orthographic,
Perspective,
}
export class Camera {
/**
* Only applicable in perspective mode.
*/
private readonly fov = deg_to_rad(75);
private readonly target: Vec3 = new Vec3(0, 0, 0);
// Spherical coordinates.
private radius = 0;
private azimuth = 0;
private polar = Math.PI / 2;
private _zoom: number = 1;
/**
* Effective field of view in radians. Only applicable in perspective mode.
*/
private get effective_fov(): number {
return 2 * Math.atan(Math.tan(0.5 * this.fov) / this._zoom);
}
readonly view_matrix = Mat4.identity();
readonly projection_matrix = Mat4.identity();
constructor(
private viewport_width: number,
private viewport_height: number,
readonly projection: Projection,
) {
this.set_viewport(viewport_width, viewport_height);
}
set_viewport(width: number, height: number): void {
this.viewport_width = width;
this.viewport_height = height;
switch (this.projection) {
case Projection.Orthographic:
{
const w = width;
const h = height;
const n = 0;
const f = 100;
// prettier-ignore
this.projection_matrix.set_all(
2/w, 0, 0, 0,
0, 2/h, 0, 0,
0, 0, 2/(n-f), 0,
0, 0, 0, 1,
);
}
break;
case Projection.Perspective:
{
const aspect = width / height;
const n /* near */ = 0.1;
const f /* far */ = 2000;
const t /* top */ = (n * Math.tan(0.5 * this.fov)) / this._zoom;
const h /* height */ = 2 * t;
const w /* width */ = 2 * aspect * t;
// prettier-ignore
this.projection_matrix.set_all(
2*n / w, 0, 0, 0,
0, 2*n / h, 0, 0,
0, 0, (n+f) / (n-f), 2*n*f / (n-f),
0, 0, -1, 0,
);
}
break;
}
}
pan(x: number, y: number, z: number): this {
let pan_factor: number;
switch (this.projection) {
case Projection.Orthographic:
pan_factor = 1;
break;
case Projection.Perspective:
pan_factor =
(3 * this.radius * Math.tan(0.5 * this.effective_fov)) / this.viewport_width;
break;
}
x *= pan_factor;
y *= pan_factor;
this.target.x += x;
this.target.y += y;
this.radius += z;
this.update_matrix();
return this;
}
rotate(azimuth: number, polar: number): this {
this.azimuth += azimuth;
const max_pole_dist = Math.PI / 1800; // tenth of a degree.
this.polar = clamp(this.polar + polar, max_pole_dist, Math.PI - max_pole_dist);
this.update_matrix();
return this;
}
/**
* Increase (or decrease) zoom by a factor.
*/
zoom(factor: number): this {
this._zoom *= factor;
this.target.x *= factor;
this.target.y *= factor;
this.target.z *= factor;
this.update_matrix();
return this;
}
reset(): this {
this.target.x = 0;
this.target.y = 0;
this.target.z = 0;
this._zoom = 1;
this.update_matrix();
return this;
}
private update_matrix(): void {
// Convert spherical coordinates to cartesian coordinates.
const radius_sin_polar = this.radius * Math.sin(this.polar);
const camera_pos = new Vec3(
this.target.x + radius_sin_polar * Math.sin(this.azimuth),
this.target.y + this.radius * Math.cos(this.polar),
this.target.z + radius_sin_polar * Math.cos(this.azimuth),
);
// Compute forward (z-axis), right (x-axis) and up (y-axis) vectors.
const forward = vec3_sub(camera_pos, this.target);
forward.normalize();
const right = vec3_cross(new Vec3(0, 1, 0), forward);
right.normalize();
const up = vec3_cross(forward, right);
const zoom = this._zoom;
// prettier-ignore
this.view_matrix.set_all(
right.x * zoom, right.y, right.z, -vec3_dot( right, camera_pos),
up.x, up.y* zoom, up.z, -vec3_dot( up, camera_pos),
forward.x, forward.y, forward.z* zoom, -vec3_dot(forward, camera_pos),
0, 0, 0, 1,
);
}
}

View File

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

View File

@ -1,139 +0,0 @@
import { Renderer } from "./Renderer";
import { Scene } from "./Scene";
import { Camera, Projection } from "./Camera";
import { Gfx } from "./Gfx";
import { Mat4, Vec2, vec2_diff } from "../math/linear_algebra";
export abstract class GfxRenderer implements Renderer {
private pointer_pos?: Vec2;
/**
* Is defined when an animation frame is scheduled.
*/
private animation_frame?: number;
protected width: number = 800;
protected height: number = 600;
abstract readonly gfx: Gfx;
readonly scene = new Scene();
readonly camera: Camera;
readonly canvas_element: HTMLCanvasElement;
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);
this.canvas_element.addEventListener("wheel", this.wheel, { passive: true });
this.camera = new Camera(this.width, this.height, projection);
}
dispose(): void {
this.destroy_scene();
}
set_size(width: number, height: number): void {
this.width = width;
this.height = height;
this.camera.set_viewport(width, height);
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;
/**
* Destroys all GPU objects related to the scene and resets the scene.
*/
destroy_scene(): void {
this.scene.traverse(node => {
node.mesh?.destroy(this.gfx);
node.mesh?.texture?.destroy();
node.mesh = undefined;
}, undefined);
this.scene.root_node.clear_children();
this.scene.root_node.transform = Mat4.identity();
}
private mousedown = (evt: MouseEvent): void => {
this.pointer_pos = new Vec2(evt.clientX, evt.clientY);
window.addEventListener("mousemove", this.mousemove);
window.addEventListener("mouseup", this.mouseup);
window.addEventListener("contextmenu", this.contextmenu);
};
private mousemove = (evt: MouseEvent): void => {
const new_pos = new Vec2(evt.clientX, evt.clientY);
const diff = vec2_diff(new_pos, this.pointer_pos!);
if (evt.buttons === 1) {
this.camera.pan(-diff.x, diff.y, 0);
} else if (evt.buttons === 2) {
this.camera.rotate(-diff.x / (20 * Math.PI), -diff.y / (20 * Math.PI));
}
this.pointer_pos = new_pos;
this.schedule_render();
};
private mouseup = (evt: MouseEvent): void => {
evt.preventDefault();
this.pointer_pos = undefined;
window.removeEventListener("mousemove", this.mousemove);
window.removeEventListener("mouseup", this.mouseup);
};
private wheel = (evt: WheelEvent): void => {
switch (this.camera.projection) {
case Projection.Orthographic:
if (evt.deltaY < 0) {
this.camera.zoom(1.1);
} else {
this.camera.zoom(0.9);
}
break;
case Projection.Perspective:
if (evt.deltaY < 0) {
this.camera.pan(0, 0, -2);
} else {
this.camera.pan(0, 0, 2);
}
break;
}
this.schedule_render();
};
private contextmenu = (evt: Event): void => {
evt.preventDefault();
window.removeEventListener("contextmenu", this.contextmenu);
};
}

View File

@ -1,54 +0,0 @@
import { VertexFormatType } from "./VertexFormat";
import { Texture } from "./Texture";
import { Gfx } from "./Gfx";
import {
MeshBuilder,
PosNormMeshBuilder,
PosNormTexMeshBuilder,
PosTexMeshBuilder,
} from "./MeshBuilder";
export class Mesh {
/* eslint-disable no-dupe-class-members */
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 VertexFormatType.PosNorm:
return new PosNormMeshBuilder();
case VertexFormatType.PosTex:
return new PosTexMeshBuilder();
case VertexFormatType.PosNormTex:
return new PosNormTexMeshBuilder();
}
}
/* eslint-enable no-dupe-class-members */
gfx_mesh: unknown;
constructor(
readonly format: VertexFormatType,
readonly vertex_data: ArrayBuffer,
readonly index_data: ArrayBuffer,
readonly index_count: number,
readonly texture?: Texture,
) {}
upload(gfx: Gfx): void {
this.texture?.upload();
if (this.gfx_mesh == undefined) {
this.gfx_mesh = gfx.create_gfx_mesh(
this.format,
this.vertex_data,
this.index_data,
this.texture,
);
}
}
destroy(gfx: Gfx): void {
gfx.destroy_gfx_mesh(this.gfx_mesh);
}
}

View File

@ -1,106 +0,0 @@
import { Texture } from "./Texture";
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;
tex?: Vec2;
}[] = [];
protected readonly index_data: number[] = [];
protected _texture?: Texture;
get vertex_count(): number {
return this.vertex_data.length;
}
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);
return this;
}
build(): Mesh {
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;
for (const { pos, normal, tex } of this.vertex_data) {
v_view.setFloat32(i, pos.x, true);
v_view.setFloat32(i + 4, pos.y, true);
v_view.setFloat32(i + 8, pos.z, true);
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 != undefined) {
v_view.setUint16(i + v_tex_offset, tex!.x * 0xffff, true);
v_view.setUint16(i + v_tex_offset + 2, tex!.y * 0xffff, true);
}
i += v_size;
}
// Make index data divisible by 4 for WebGPU.
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.type, v_data, i_data, this.index_data.length, this._texture);
}
}
export class PosNormMeshBuilder extends MeshBuilder {
constructor() {
super(VertexFormatType.PosNorm);
}
vertex(pos: Vec3, normal: Vec3): this {
this.vertex_data.push({ pos, normal });
return this;
}
}
export class PosTexMeshBuilder extends MeshBuilder {
constructor() {
super(VertexFormatType.PosTex);
}
vertex(pos: Vec3, tex: Vec2): this {
this.vertex_data.push({ pos, tex });
return this;
}
texture(tex: Texture): this {
this._texture = tex;
return this;
}
}
export class PosNormTexMeshBuilder extends MeshBuilder {
constructor() {
super(VertexFormatType.PosNormTex);
}
vertex(pos: Vec3, normal: Vec3, tex: Vec2): this {
this.vertex_data.push({ pos, normal, tex });
return this;
}
texture(tex: Texture): this {
this._texture = tex;
return this;
}
}

View File

@ -1,11 +1,140 @@
import CameraControls from "camera-controls";
import * as THREE from "three";
import {
Clock,
Color,
Group,
HemisphereLight,
OrthographicCamera,
PerspectiveCamera,
Scene,
Vector2,
Vector3,
} from "three";
import { Disposable } from "../observable/Disposable";
export interface Renderer extends Disposable {
readonly canvas_element: HTMLCanvasElement;
CameraControls.install({
// Hack to make panning and orbiting work the way we want.
THREE: {
...THREE,
MOUSE: { ...THREE.MOUSE, LEFT: THREE.MOUSE.RIGHT, RIGHT: THREE.MOUSE.LEFT },
},
});
start_rendering(): void;
export interface DisposableThreeRenderer extends THREE.WebGLRenderer, Disposable {}
stop_rendering(): void;
/**
* Uses THREE.js for rendering.
*/
export abstract class Renderer implements Disposable {
private _debug = false;
set_size(width: number, height: number): void;
get debug(): boolean {
return this._debug;
}
set debug(debug: boolean) {
this._debug = debug;
}
abstract readonly camera: PerspectiveCamera | OrthographicCamera;
readonly controls!: CameraControls;
readonly scene = new Scene();
readonly light_holder = new Group();
private readonly renderer: DisposableThreeRenderer;
private render_scheduled = false;
private animation_frame_handle?: number = undefined;
private readonly light = new HemisphereLight(0xffffff, 0x505050, 1.0);
private readonly controls_clock = new Clock();
private readonly size = new Vector2(0, 0);
protected constructor(three_renderer: DisposableThreeRenderer) {
this.renderer = three_renderer;
this.renderer.domElement.tabIndex = 0;
this.renderer.domElement.addEventListener("mousedown", this.on_mouse_down);
this.renderer.domElement.style.outline = "none";
this.scene.background = new Color(0x181818);
this.light_holder.add(this.light);
this.scene.add(this.light_holder);
}
get canvas_element(): HTMLCanvasElement {
return this.renderer.domElement;
}
set_size(width: number, height: number): void {
this.size.set(width, height);
this.renderer.setSize(width, height);
this.schedule_render();
}
pointer_pos_to_device_coords(pos: Vector2): void {
pos.set((pos.x / this.size.width) * 2 - 1, (pos.y / this.size.height) * -2 + 1);
}
start_rendering(): void {
if (this.animation_frame_handle == undefined) {
this.schedule_render();
this.animation_frame_handle = requestAnimationFrame(this.call_render);
}
}
stop_rendering(): void {
if (this.animation_frame_handle != undefined) {
cancelAnimationFrame(this.animation_frame_handle);
this.animation_frame_handle = undefined;
}
}
schedule_render = (): void => {
this.render_scheduled = true;
};
reset_camera(position: Vector3, look_at: Vector3): void {
this.controls.setLookAt(
position.x,
position.y,
position.z,
look_at.x,
look_at.y,
look_at.z,
);
}
dispose(): void {
this.renderer.dispose();
this.controls.dispose();
}
protected init_camera_controls(): void {
(this.controls as CameraControls) = new CameraControls(
this.camera,
this.renderer.domElement,
);
this.controls.dampingFactor = 1;
this.controls.draggingDampingFactor = 1;
}
protected render(): void {
this.renderer.render(this.scene, this.camera);
}
private on_mouse_down = (e: Event): void => {
if (e.currentTarget) (e.currentTarget as HTMLElement).focus();
};
private call_render = (): void => {
const controls_updated = this.controls.update(this.controls_clock.getDelta());
const should_render = this.render_scheduled || controls_updated;
this.render_scheduled = false;
if (should_render) {
this.render();
}
this.animation_frame_handle = requestAnimationFrame(this.call_render);
};
}

View File

@ -1,38 +0,0 @@
import { Mesh } from "./Mesh";
import { Mat4 } from "../math/linear_algebra";
export class Scene {
readonly root_node = new SceneNode(undefined, Mat4.identity());
traverse<T>(f: (node: SceneNode, data: T) => T, data: T): void {
this.traverse_node(this.root_node, f, data);
}
private traverse_node<T>(node: SceneNode, f: (node: SceneNode, 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 SceneNode {
private readonly _children: SceneNode[];
get children(): readonly SceneNode[] {
return this._children;
}
constructor(public mesh: Mesh | undefined, public transform: Mat4, ...children: SceneNode[]) {
this._children = children;
}
add_child(child: SceneNode): void {
this._children.push(child);
}
clear_children(): void {
this._children.splice(0);
}
}

View File

@ -1,109 +0,0 @@
import { Mat3, Mat4 } from "../math/linear_algebra";
import { VERTEX_NORMAL_LOC, VERTEX_POS_LOC, VERTEX_TEX_LOC } from "./VertexFormat";
export class ShaderProgram {
private readonly gl: WebGL2RenderingContext;
private readonly program: WebGLProgram;
private readonly mat_projection_loc: WebGLUniformLocation;
private readonly mat_model_view_loc: WebGLUniformLocation;
private readonly mat_normal_loc: WebGLUniformLocation | null;
private readonly tex_sampler_loc: WebGLUniformLocation | null;
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.");
this.program = program;
let vertex_shader: WebGLShader | null = null;
let frag_shader: WebGLShader | null = null;
try {
vertex_shader = create_shader(gl, gl.VERTEX_SHADER, vertex_source);
gl.attachShader(program, vertex_shader);
frag_shader = create_shader(gl, gl.FRAGMENT_SHADER, frag_source);
gl.attachShader(program, frag_shader);
gl.bindAttribLocation(program, VERTEX_POS_LOC, "pos");
gl.bindAttribLocation(program, VERTEX_NORMAL_LOC, "normal");
gl.bindAttribLocation(program, VERTEX_TEX_LOC, "tex");
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const log = gl.getProgramInfoLog(program);
throw new Error("Shader linking failed. Program log:\n" + log);
}
this.mat_projection_loc = this.get_required_uniform_location(program, "mat_projection");
this.mat_model_view_loc = this.get_required_uniform_location(program, "mat_model_view");
this.mat_normal_loc = gl.getUniformLocation(program, "mat_normal");
this.tex_sampler_loc = gl.getUniformLocation(program, "tex_sampler");
gl.detachShader(program, vertex_shader);
gl.detachShader(program, frag_shader);
} catch (e) {
gl.deleteProgram(program);
throw e;
} finally {
// Always delete shaders after we're done.
gl.deleteShader(vertex_shader);
gl.deleteShader(frag_shader);
}
}
set_mat_projection_uniform(matrix: Mat4): void {
this.gl.uniformMatrix4fv(this.mat_projection_loc, false, matrix.data);
}
set_mat_model_view_uniform(matrix: Mat4): void {
this.gl.uniformMatrix4fv(this.mat_model_view_loc, false, matrix.data);
}
set_mat_normal_uniform(matrix: Mat3): void {
this.gl.uniformMatrix3fv(this.mat_normal_loc, false, matrix.data);
}
set_texture_uniform(unit: GLenum): void {
this.gl.uniform1i(this.tex_sampler_loc, unit - this.gl.TEXTURE0);
}
bind(): void {
this.gl.useProgram(this.program);
}
unbind(): void {
this.gl.useProgram(null);
}
delete(): void {
this.gl.deleteProgram(this.program);
}
private get_required_uniform_location(
program: WebGLProgram,
uniform: string,
): WebGLUniformLocation {
const loc = this.gl.getUniformLocation(program, uniform);
if (loc == null) throw new Error(`Couldn't get ${uniform} uniform location.`);
return loc;
}
}
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}.`);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const log = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new Error("Vertex shader compilation failed. Shader log:\n" + log);
}
return shader;
}

View File

@ -1,33 +0,0 @@
import { Gfx } from "./Gfx";
export enum TextureFormat {
RGBA_S3TC_DXT1,
RGBA_S3TC_DXT3,
}
export class Texture {
gfx_texture: unknown;
constructor(
private readonly gfx: Gfx,
private readonly format: TextureFormat,
private readonly width: number,
private readonly height: number,
private readonly data: ArrayBuffer,
) {}
upload(): void {
if (this.gfx_texture == undefined) {
this.gfx_texture = this.gfx.create_texture(
this.format,
this.width,
this.height,
this.data,
);
}
}
destroy(): void {
this.gfx.destroy_texture(this.gfx_texture);
}
}

View File

@ -1,141 +0,0 @@
import CameraControls from "camera-controls";
import * as THREE from "three";
import {
Clock,
Color,
Group,
HemisphereLight,
OrthographicCamera,
PerspectiveCamera,
Scene,
Vector2,
Vector3,
} from "three";
import { Disposable } from "../observable/Disposable";
import { Renderer } from "./Renderer";
CameraControls.install({
// Hack to make panning and orbiting work the way we want.
THREE: {
...THREE,
MOUSE: { ...THREE.MOUSE, LEFT: THREE.MOUSE.RIGHT, RIGHT: THREE.MOUSE.LEFT },
},
});
export interface DisposableThreeRenderer extends THREE.WebGLRenderer, Disposable {}
/**
* Uses THREE.js for rendering.
*/
export abstract class ThreeRenderer implements Renderer {
private _debug = false;
get debug(): boolean {
return this._debug;
}
set debug(debug: boolean) {
this._debug = debug;
}
abstract readonly camera: PerspectiveCamera | OrthographicCamera;
readonly controls!: CameraControls;
readonly scene = new Scene();
readonly light_holder = new Group();
private readonly renderer: DisposableThreeRenderer;
private render_scheduled = false;
private animation_frame_handle?: number = undefined;
private readonly light = new HemisphereLight(0xffffff, 0x505050, 1.0);
private readonly controls_clock = new Clock();
private readonly size = new Vector2(0, 0);
protected constructor(three_renderer: DisposableThreeRenderer) {
this.renderer = three_renderer;
this.renderer.domElement.tabIndex = 0;
this.renderer.domElement.addEventListener("mousedown", this.on_mouse_down);
this.renderer.domElement.style.outline = "none";
this.scene.background = new Color(0x181818);
this.light_holder.add(this.light);
this.scene.add(this.light_holder);
}
get canvas_element(): HTMLCanvasElement {
return this.renderer.domElement;
}
set_size(width: number, height: number): void {
this.size.set(width, height);
this.renderer.setSize(width, height);
this.schedule_render();
}
pointer_pos_to_device_coords(pos: Vector2): void {
pos.set((pos.x / this.size.width) * 2 - 1, (pos.y / this.size.height) * -2 + 1);
}
start_rendering(): void {
if (this.animation_frame_handle == undefined) {
this.schedule_render();
this.animation_frame_handle = requestAnimationFrame(this.call_render);
}
}
stop_rendering(): void {
if (this.animation_frame_handle != undefined) {
cancelAnimationFrame(this.animation_frame_handle);
this.animation_frame_handle = undefined;
}
}
schedule_render = (): void => {
this.render_scheduled = true;
};
reset_camera(position: Vector3, look_at: Vector3): void {
this.controls.setLookAt(
position.x,
position.y,
position.z,
look_at.x,
look_at.y,
look_at.z,
);
}
dispose(): void {
this.renderer.dispose();
this.controls.dispose();
}
protected init_camera_controls(): void {
(this.controls as CameraControls) = new CameraControls(
this.camera,
this.renderer.domElement,
);
this.controls.dampingFactor = 1;
this.controls.draggingDampingFactor = 1;
}
protected render(): void {
this.renderer.render(this.scene, this.camera);
}
private on_mouse_down = (e: Event): void => {
if (e.currentTarget) (e.currentTarget as HTMLElement).focus();
};
private call_render = (): void => {
const controls_updated = this.controls.update(this.controls_clock.getDelta());
const should_render = this.render_scheduled || controls_updated;
this.render_scheduled = false;
if (should_render) {
this.render();
}
this.animation_frame_handle = requestAnimationFrame(this.call_render);
};
}

View File

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

View File

@ -1,11 +1,6 @@
import { Vec3 } from "../../data_formats/vector";
import { Vector3 } from "three";
import { Vec3 as MathVec3 } from "../../math/linear_algebra";
export function vec3_to_threejs(v: Vec3): Vector3 {
return new Vector3(v.x, v.y, v.z);
}
export function vec3_to_math(v: Vec3): MathVec3 {
return new MathVec3(v.x, v.y, v.z);
}

View File

@ -1,30 +1,28 @@
import { Bone, BufferGeometry, Euler, Matrix3, Matrix4, Quaternion, Vector2, Vector3 } from "three";
import { vec3_to_threejs } from "./index";
import { is_njcm_model, NjModel, NjObject } from "../../data_formats/parsing/ninja";
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 { VertexFormatType } from "../VertexFormat";
import { EulerOrder, Quat } from "../../math/quaternions";
import {
mat3_vec3_multiply_into,
Mat4,
mat4_multiply,
mat4_vec3_multiply_into,
Vec3,
} from "../../math/linear_algebra";
import { GeometryBuilder } from "./GeometryBuilder";
const DEFAULT_NORMAL = new Vec3(0, 1, 0);
const NO_TRANSLATION = new Vec3(0, 0, 0);
const NO_ROTATION = new Quat(1, 0, 0, 0);
const NO_SCALE = new Vec3(1, 1, 1);
const DEFAULT_NORMAL = new Vector3(0, 1, 0);
const DEFAULT_UV = new Vector2(0, 0);
const NO_TRANSLATION = new Vector3(0, 0, 0);
const NO_ROTATION = new Quaternion(0, 0, 0, 1);
const NO_SCALE = new Vector3(1, 1, 1);
export function ninja_object_to_mesh(object: NjObject): Mesh {
return new MeshCreator().to_mesh(object);
export function ninja_object_to_geometry_builder(object: NjObject, builder: GeometryBuilder): void {
new GeometryCreator(builder).to_geometry_builder(object);
}
export function ninja_object_to_buffer_geometry(object: NjObject): BufferGeometry {
return new GeometryCreator(new GeometryBuilder()).create_buffer_geometry(object);
}
type Vertex = {
position: Vec3;
normal?: Vec3;
bone_id: number;
position: Vector3;
normal?: Vector3;
bone_weight: number;
bone_weight_status: number;
calc_continue: boolean;
@ -52,16 +50,29 @@ class VerticesHolder {
}
}
class MeshCreator {
class GeometryCreator {
private readonly vertices = new VerticesHolder();
private readonly builder = Mesh.builder(VertexFormatType.PosNorm);
private readonly builder: GeometryBuilder;
private bone_id = 0;
to_mesh(object: NjObject): Mesh {
this.object_to_mesh(object, Mat4.identity());
constructor(builder: GeometryBuilder) {
this.builder = builder;
}
to_geometry_builder(object: NjObject): void {
this.object_to_geometry(object, undefined, new Matrix4());
}
create_buffer_geometry(object: NjObject): BufferGeometry {
this.to_geometry_builder(object);
return this.builder.build();
}
private object_to_mesh(object: NjObject, parent_matrix: Mat4): void {
private object_to_geometry(
object: NjObject,
parent_bone: Bone | undefined,
parent_matrix: Matrix4,
): void {
const {
no_translate,
no_rotate,
@ -69,59 +80,76 @@ class MeshCreator {
hidden,
break_child_trace,
zxy_rotation_order,
skip,
} = object.evaluation_flags;
const { position, rotation, scale } = object;
const matrix = mat4_multiply(
parent_matrix,
Mat4.compose(
no_translate ? NO_TRANSLATION : vec3_to_math(position),
no_rotate
? NO_ROTATION
: Quat.euler_angles(
rotation.x,
rotation.y,
rotation.z,
zxy_rotation_order ? EulerOrder.ZXY : EulerOrder.ZYX,
),
no_scale ? NO_SCALE : vec3_to_math(scale),
),
const euler = new Euler(
rotation.x,
rotation.y,
rotation.z,
zxy_rotation_order ? "ZXY" : "ZYX",
);
const matrix = new Matrix4()
.compose(
no_translate ? NO_TRANSLATION : vec3_to_threejs(position),
no_rotate ? NO_ROTATION : new Quaternion().setFromEuler(euler),
no_scale ? NO_SCALE : vec3_to_threejs(scale),
)
.premultiply(parent_matrix);
let bone: Bone | undefined;
if (skip) {
bone = parent_bone;
} else {
bone = new Bone();
bone.name = this.bone_id.toString();
bone.position.set(position.x, position.y, position.z);
bone.setRotationFromEuler(euler);
bone.scale.set(scale.x, scale.y, scale.z);
this.builder.add_bone(bone);
if (parent_bone) {
parent_bone.add(bone);
}
}
if (object.model && !hidden) {
this.model_to_mesh(object.model, matrix);
this.model_to_geometry(object.model, matrix);
}
this.bone_id++;
if (!break_child_trace) {
for (const child of object.children) {
this.object_to_mesh(child, matrix);
this.object_to_geometry(child, bone, matrix);
}
}
}
private model_to_mesh(model: NjModel, matrix: Mat4): void {
private model_to_geometry(model: NjModel, matrix: Matrix4): void {
if (is_njcm_model(model)) {
this.njcm_model_to_mesh(model, matrix);
this.njcm_model_to_geometry(model, matrix);
} else {
this.xj_model_to_mesh(model, matrix);
this.xj_model_to_geometry(model, matrix);
}
}
private njcm_model_to_mesh(model: NjcmModel, matrix: Mat4): void {
const normal_matrix = matrix.normal_mat3();
private njcm_model_to_geometry(model: NjcmModel, matrix: Matrix4): void {
const normal_matrix = new Matrix3().getNormalMatrix(matrix);
const new_vertices = model.vertices.map(vertex => {
const position = vec3_to_math(vertex.position);
mat4_vec3_multiply_into(matrix, position, position);
const position = vec3_to_threejs(vertex.position);
const normal = vertex.normal ? vec3_to_threejs(vertex.normal) : new Vector3(0, 1, 0);
let normal: Vec3 | undefined = undefined;
if (vertex.normal) {
normal = vec3_to_math(vertex.normal);
mat3_vec3_multiply_into(normal_matrix, normal, normal);
}
position.applyMatrix4(matrix);
normal.applyMatrix3(normal_matrix);
return {
bone_id: this.bone_id,
position,
normal,
bone_weight: vertex.bone_weight,
@ -133,61 +161,152 @@ class MeshCreator {
this.vertices.put(new_vertices);
for (const mesh of model.meshes) {
const start_index_count = this.builder.index_count;
for (let i = 0; i < mesh.vertices.length; ++i) {
const mesh_vertex = mesh.vertices[i];
const vertices = this.vertices.get(mesh_vertex.index);
if (vertices.length) {
const vertex = vertices[0];
const normal =
vertex.normal ??
(mesh_vertex.normal ? vec3_to_math(mesh_vertex.normal) : DEFAULT_NORMAL);
const normal = vertex.normal ?? mesh_vertex.normal ?? DEFAULT_NORMAL;
const index = this.builder.vertex_count;
this.builder.vertex(vertex.position, normal);
this.builder.add_vertex(
vertex.position,
normal,
mesh.has_tex_coords ? mesh_vertex.tex_coords! : DEFAULT_UV,
);
if (i >= 2) {
if (i % 2 === (mesh.clockwise_winding ? 1 : 0)) {
this.builder.triangle(index - 2, index - 1, index);
this.builder.add_index(index - 2);
this.builder.add_index(index - 1);
this.builder.add_index(index);
} else {
this.builder.triangle(index - 2, index, index - 1);
this.builder.add_index(index - 2);
this.builder.add_index(index);
this.builder.add_index(index - 1);
}
}
const bones = [
[0, 0],
[0, 0],
[0, 0],
[0, 0],
];
for (let j = vertices.length - 1; j >= 0; j--) {
const vertex = vertices[j];
bones[vertex.bone_weight_status] = [vertex.bone_id, vertex.bone_weight];
}
const total_weight = bones.reduce((total, [, weight]) => total + weight, 0);
for (const [bone_index, bone_weight] of bones) {
this.builder.add_bone_weight(
bone_index,
total_weight > 0 ? bone_weight / total_weight : bone_weight,
);
}
}
}
this.builder.add_group(
start_index_count,
this.builder.index_count - start_index_count,
mesh.texture_id,
mesh.use_alpha,
mesh.src_alpha !== 4 || mesh.dst_alpha !== 5,
);
}
}
private xj_model_to_mesh(model: XjModel, matrix: Mat4): void {
private xj_model_to_geometry(model: XjModel, matrix: Matrix4): void {
const index_offset = this.builder.vertex_count;
const normal_matrix = matrix.normal_mat3();
const normal_matrix = new Matrix3().getNormalMatrix(matrix);
for (const { position, normal } of model.vertices) {
const p = vec3_to_math(position);
mat4_vec3_multiply_into(matrix, p, p);
for (const { position, normal, uv } of model.vertices) {
const p = vec3_to_threejs(position).applyMatrix4(matrix);
const n = normal ? vec3_to_math(normal) : new Vec3(0, 1, 0);
mat3_vec3_multiply_into(normal_matrix, n, n);
const local_n = normal ? vec3_to_threejs(normal) : new Vector3(0, 1, 0);
const n = local_n.applyMatrix3(normal_matrix);
this.builder.vertex(p, n);
const tuv = uv || DEFAULT_UV;
this.builder.add_vertex(p, n, tuv);
}
let current_mat_idx: number | undefined;
let current_src_alpha: number | undefined;
let current_dst_alpha: number | undefined;
for (const mesh of model.meshes) {
const start_index_count = this.builder.index_count;
let clockwise = false;
for (let j = 2; j < mesh.indices.length; ++j) {
const a = index_offset + mesh.indices[j - 2];
const b = index_offset + mesh.indices[j - 1];
const c = index_offset + mesh.indices[j];
const pa = this.builder.get_position(a);
const pb = this.builder.get_position(b);
const pc = this.builder.get_position(c);
const na = this.builder.get_normal(a);
const nb = this.builder.get_normal(b);
const nc = this.builder.get_normal(c);
// Calculate a surface normal and reverse the vertex winding if at least 2 of the
// vertex normals point in the opposite direction. This hack fixes the winding for
// most models.
const normal = pb.clone().sub(pa).cross(pc.clone().sub(pa));
if (clockwise) {
this.builder.triangle(b, a, c);
normal.negate();
}
const opposite_count =
(normal.dot(na) < 0 ? 1 : 0) +
(normal.dot(nb) < 0 ? 1 : 0) +
(normal.dot(nc) < 0 ? 1 : 0);
if (opposite_count >= 2) {
clockwise = !clockwise;
}
if (clockwise) {
this.builder.add_index(b);
this.builder.add_index(a);
this.builder.add_index(c);
} else {
this.builder.triangle(a, b, c);
this.builder.add_index(a);
this.builder.add_index(b);
this.builder.add_index(c);
}
clockwise = !clockwise;
}
if (mesh.material_properties.texture_id != undefined) {
current_mat_idx = mesh.material_properties.texture_id;
}
if (mesh.material_properties.src_alpha != undefined) {
current_src_alpha = mesh.material_properties.src_alpha;
}
if (mesh.material_properties.dst_alpha != undefined) {
current_dst_alpha = mesh.material_properties.dst_alpha;
}
this.builder.add_group(
start_index_count,
this.builder.index_count - start_index_count,
current_mat_idx,
true,
current_src_alpha !== 4 || current_dst_alpha !== 5,
);
}
}
}

View File

@ -8,29 +8,6 @@ import {
Texture as ThreeTexture,
} from "three";
import { Xvm, XvrTexture } from "../../data_formats/parsing/ninja/texture";
import { Texture, TextureFormat } from "../Texture";
import { Gfx } from "../Gfx";
export function xvr_texture_to_texture(gfx: Gfx, xvr: XvrTexture): Texture {
let format: TextureFormat;
let data_size: number;
// Ignore mipmaps.
switch (xvr.format[1]) {
case 6:
format = TextureFormat.RGBA_S3TC_DXT1;
data_size = (xvr.width * xvr.height) / 2;
break;
case 7:
format = TextureFormat.RGBA_S3TC_DXT3;
data_size = xvr.width * xvr.height;
break;
default:
throw new Error(`Format ${xvr.format.join(", ")} not supported.`);
}
return new Texture(gfx, format, xvr.width, xvr.height, xvr.data.slice(0, data_size));
}
export function xvm_to_three_textures(xvm: Xvm): ThreeTexture[] {
return xvm.textures.map(xvr_texture_to_three_texture);

View File

@ -1,312 +0,0 @@
import { Bone, BufferGeometry, Euler, Matrix3, Matrix4, Quaternion, Vector2, Vector3 } from "three";
import { vec3_to_threejs } from "./index";
import { is_njcm_model, NjModel, NjObject } from "../../data_formats/parsing/ninja";
import { NjcmModel } from "../../data_formats/parsing/ninja/njcm";
import { XjModel } from "../../data_formats/parsing/ninja/xj";
import { GeometryBuilder } from "./GeometryBuilder";
const DEFAULT_NORMAL = new Vector3(0, 1, 0);
const DEFAULT_UV = new Vector2(0, 0);
const NO_TRANSLATION = new Vector3(0, 0, 0);
const NO_ROTATION = new Quaternion(0, 0, 0, 1);
const NO_SCALE = new Vector3(1, 1, 1);
export function ninja_object_to_geometry_builder(object: NjObject, builder: GeometryBuilder): void {
new GeometryCreator(builder).to_geometry_builder(object);
}
export function ninja_object_to_buffer_geometry(object: NjObject): BufferGeometry {
return new GeometryCreator(new GeometryBuilder()).create_buffer_geometry(object);
}
type Vertex = {
bone_id: number;
position: Vector3;
normal?: Vector3;
bone_weight: number;
bone_weight_status: number;
calc_continue: boolean;
};
class VerticesHolder {
private readonly vertices_stack: Vertex[][] = [];
put(vertices: Vertex[]): void {
this.vertices_stack.push(vertices);
}
get(index: number): Vertex[] {
const vertices: Vertex[] = [];
for (let i = this.vertices_stack.length - 1; i >= 0; i--) {
const vertex = this.vertices_stack[i][index];
if (vertex) {
vertices.push(vertex);
}
}
return vertices;
}
}
class GeometryCreator {
private readonly vertices = new VerticesHolder();
private readonly builder: GeometryBuilder;
private bone_id = 0;
constructor(builder: GeometryBuilder) {
this.builder = builder;
}
to_geometry_builder(object: NjObject): void {
this.object_to_geometry(object, undefined, new Matrix4());
}
create_buffer_geometry(object: NjObject): BufferGeometry {
this.to_geometry_builder(object);
return this.builder.build();
}
private object_to_geometry(
object: NjObject,
parent_bone: Bone | undefined,
parent_matrix: Matrix4,
): void {
const {
no_translate,
no_rotate,
no_scale,
hidden,
break_child_trace,
zxy_rotation_order,
skip,
} = object.evaluation_flags;
const { position, rotation, scale } = object;
const euler = new Euler(
rotation.x,
rotation.y,
rotation.z,
zxy_rotation_order ? "ZXY" : "ZYX",
);
const matrix = new Matrix4()
.compose(
no_translate ? NO_TRANSLATION : vec3_to_threejs(position),
no_rotate ? NO_ROTATION : new Quaternion().setFromEuler(euler),
no_scale ? NO_SCALE : vec3_to_threejs(scale),
)
.premultiply(parent_matrix);
let bone: Bone | undefined;
if (skip) {
bone = parent_bone;
} else {
bone = new Bone();
bone.name = this.bone_id.toString();
bone.position.set(position.x, position.y, position.z);
bone.setRotationFromEuler(euler);
bone.scale.set(scale.x, scale.y, scale.z);
this.builder.add_bone(bone);
if (parent_bone) {
parent_bone.add(bone);
}
}
if (object.model && !hidden) {
this.model_to_geometry(object.model, matrix);
}
this.bone_id++;
if (!break_child_trace) {
for (const child of object.children) {
this.object_to_geometry(child, bone, matrix);
}
}
}
private model_to_geometry(model: NjModel, matrix: Matrix4): void {
if (is_njcm_model(model)) {
this.njcm_model_to_geometry(model, matrix);
} else {
this.xj_model_to_geometry(model, matrix);
}
}
private njcm_model_to_geometry(model: NjcmModel, matrix: Matrix4): void {
const normal_matrix = new Matrix3().getNormalMatrix(matrix);
const new_vertices = model.vertices.map(vertex => {
const position = vec3_to_threejs(vertex.position);
const normal = vertex.normal ? vec3_to_threejs(vertex.normal) : new Vector3(0, 1, 0);
position.applyMatrix4(matrix);
normal.applyMatrix3(normal_matrix);
return {
bone_id: this.bone_id,
position,
normal,
bone_weight: vertex.bone_weight,
bone_weight_status: vertex.bone_weight_status,
calc_continue: vertex.calc_continue,
};
});
this.vertices.put(new_vertices);
for (const mesh of model.meshes) {
const start_index_count = this.builder.index_count;
for (let i = 0; i < mesh.vertices.length; ++i) {
const mesh_vertex = mesh.vertices[i];
const vertices = this.vertices.get(mesh_vertex.index);
if (vertices.length) {
const vertex = vertices[0];
const normal = vertex.normal ?? mesh_vertex.normal ?? DEFAULT_NORMAL;
const index = this.builder.vertex_count;
this.builder.add_vertex(
vertex.position,
normal,
mesh.has_tex_coords ? mesh_vertex.tex_coords! : DEFAULT_UV,
);
if (i >= 2) {
if (i % 2 === (mesh.clockwise_winding ? 1 : 0)) {
this.builder.add_index(index - 2);
this.builder.add_index(index - 1);
this.builder.add_index(index);
} else {
this.builder.add_index(index - 2);
this.builder.add_index(index);
this.builder.add_index(index - 1);
}
}
const bones = [
[0, 0],
[0, 0],
[0, 0],
[0, 0],
];
for (let j = vertices.length - 1; j >= 0; j--) {
const vertex = vertices[j];
bones[vertex.bone_weight_status] = [vertex.bone_id, vertex.bone_weight];
}
const total_weight = bones.reduce((total, [, weight]) => total + weight, 0);
for (const [bone_index, bone_weight] of bones) {
this.builder.add_bone_weight(
bone_index,
total_weight > 0 ? bone_weight / total_weight : bone_weight,
);
}
}
}
this.builder.add_group(
start_index_count,
this.builder.index_count - start_index_count,
mesh.texture_id,
mesh.use_alpha,
mesh.src_alpha !== 4 || mesh.dst_alpha !== 5,
);
}
}
private xj_model_to_geometry(model: XjModel, matrix: Matrix4): void {
const index_offset = this.builder.vertex_count;
const normal_matrix = new Matrix3().getNormalMatrix(matrix);
for (const { position, normal, uv } of model.vertices) {
const p = vec3_to_threejs(position).applyMatrix4(matrix);
const local_n = normal ? vec3_to_threejs(normal) : new Vector3(0, 1, 0);
const n = local_n.applyMatrix3(normal_matrix);
const tuv = uv || DEFAULT_UV;
this.builder.add_vertex(p, n, tuv);
}
let current_mat_idx: number | undefined;
let current_src_alpha: number | undefined;
let current_dst_alpha: number | undefined;
for (const mesh of model.meshes) {
const start_index_count = this.builder.index_count;
let clockwise = false;
for (let j = 2; j < mesh.indices.length; ++j) {
const a = index_offset + mesh.indices[j - 2];
const b = index_offset + mesh.indices[j - 1];
const c = index_offset + mesh.indices[j];
const pa = this.builder.get_position(a);
const pb = this.builder.get_position(b);
const pc = this.builder.get_position(c);
const na = this.builder.get_normal(a);
const nb = this.builder.get_normal(b);
const nc = this.builder.get_normal(c);
// Calculate a surface normal and reverse the vertex winding if at least 2 of the
// vertex normals point in the opposite direction. This hack fixes the winding for
// most models.
const normal = pb.clone().sub(pa).cross(pc.clone().sub(pa));
if (clockwise) {
normal.negate();
}
const opposite_count =
(normal.dot(na) < 0 ? 1 : 0) +
(normal.dot(nb) < 0 ? 1 : 0) +
(normal.dot(nc) < 0 ? 1 : 0);
if (opposite_count >= 2) {
clockwise = !clockwise;
}
if (clockwise) {
this.builder.add_index(b);
this.builder.add_index(a);
this.builder.add_index(c);
} else {
this.builder.add_index(a);
this.builder.add_index(b);
this.builder.add_index(c);
}
clockwise = !clockwise;
}
if (mesh.material_properties.texture_id != undefined) {
current_mat_idx = mesh.material_properties.texture_id;
}
if (mesh.material_properties.src_alpha != undefined) {
current_src_alpha = mesh.material_properties.src_alpha;
}
if (mesh.material_properties.dst_alpha != undefined) {
current_dst_alpha = mesh.material_properties.dst_alpha;
}
this.builder.add_group(
start_index_count,
this.builder.index_count - start_index_count,
current_mat_idx,
true,
current_src_alpha !== 4 || current_dst_alpha !== 5,
);
}
}
}

View File

@ -1,58 +0,0 @@
import { Mesh } from "./Mesh";
import { VertexFormatType } from "./VertexFormat";
import { Vec3 } from "../math/linear_algebra";
export function cube_mesh(): Mesh {
return (
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))
.vertex(new Vec3(-1, -1, -1), new Vec3(0, 0, -1))
.vertex(new Vec3(1, -1, -1), new Vec3(0, 0, -1))
.triangle(0, 1, 2)
.triangle(0, 2, 3)
// Back
.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))
.triangle(4, 5, 6)
.triangle(4, 6, 7)
// Top
.vertex(new Vec3(1, 1, 1), new Vec3(0, 1, 0))
.vertex(new Vec3(-1, 1, 1), new Vec3(0, 1, 0))
.vertex(new Vec3(-1, 1, -1), new Vec3(0, 1, 0))
.vertex(new Vec3(1, 1, -1), new Vec3(0, 1, 0))
.triangle(8, 9, 10)
.triangle(8, 10, 11)
// Bottom
.vertex(new Vec3(1, -1, 1), new Vec3(0, -1, 0))
.vertex(new Vec3(1, -1, -1), new Vec3(0, -1, 0))
.vertex(new Vec3(-1, -1, -1), new Vec3(0, -1, 0))
.vertex(new Vec3(-1, -1, 1), new Vec3(0, -1, 0))
.triangle(12, 13, 14)
.triangle(12, 14, 15)
// Right
.vertex(new Vec3(1, 1, 1), new Vec3(1, 0, 0))
.vertex(new Vec3(1, 1, -1), new Vec3(1, 0, 0))
.vertex(new Vec3(1, -1, -1), new Vec3(1, 0, 0))
.vertex(new Vec3(1, -1, 1), new Vec3(1, 0, 0))
.triangle(16, 17, 18)
.triangle(16, 18, 19)
// Left
.vertex(new Vec3(-1, 1, 1), new Vec3(-1, 0, 0))
.vertex(new Vec3(-1, -1, 1), new Vec3(-1, 0, 0))
.vertex(new Vec3(-1, -1, -1), new Vec3(-1, 0, 0))
.vertex(new Vec3(-1, 1, -1), new Vec3(-1, 0, 0))
.triangle(20, 21, 22)
.triangle(20, 22, 23)
.build()
);
}

View File

@ -1,160 +0,0 @@
import { Gfx } from "../Gfx";
import { Texture, TextureFormat } from "../Texture";
import {
VERTEX_FORMATS,
VERTEX_NORMAL_LOC,
VERTEX_POS_LOC,
VERTEX_TEX_LOC,
VertexFormatType,
} 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_type: VertexFormatType,
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 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);
if (format.normal_offset != undefined) {
gl.vertexAttribPointer(
VERTEX_NORMAL_LOC,
3,
gl.FLOAT,
true,
vertex_size,
format.normal_offset,
);
gl.enableVertexAttribArray(VERTEX_NORMAL_LOC);
}
if (format.tex_offset != undefined) {
gl.vertexAttribPointer(
VERTEX_TEX_LOC,
2,
gl.UNSIGNED_SHORT,
true,
vertex_size,
format.tex_offset,
);
gl.enableVertexAttribArray(VERTEX_TEX_LOC);
}
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// Index data.
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, index_buffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index_data, gl.STATIC_DRAW);
gl.bindVertexArray(null);
texture?.upload();
return {
vao,
vertex_buffer,
index_buffer,
};
} catch (e) {
gl.deleteVertexArray(vao);
gl.deleteBuffer(vertex_buffer);
gl.deleteBuffer(index_buffer);
throw e;
}
}
destroy_gfx_mesh(gfx_mesh?: WebglMesh): void {
if (gfx_mesh) {
const gl = this.gl;
gl.deleteVertexArray(gfx_mesh.vao);
gl.deleteBuffer(gfx_mesh.vertex_buffer);
gl.deleteBuffer(gfx_mesh.index_buffer);
}
}
create_texture(
format: TextureFormat,
width: number,
height: number,
data: ArrayBuffer,
): WebGLTexture {
const gl = this.gl;
const ext = gl.getExtension("WEBGL_compressed_texture_s3tc");
if (!ext) {
throw new Error("Extension WEBGL_compressed_texture_s3tc not supported.");
}
const gl_texture = gl.createTexture();
if (gl_texture == null) throw new Error("Failed to create texture.");
let gl_format: GLenum;
switch (format) {
case TextureFormat.RGBA_S3TC_DXT1:
gl_format = ext.COMPRESSED_RGBA_S3TC_DXT1_EXT;
break;
case TextureFormat.RGBA_S3TC_DXT3:
gl_format = ext.COMPRESSED_RGBA_S3TC_DXT3_EXT;
break;
}
gl.bindTexture(gl.TEXTURE_2D, gl_texture);
gl.compressedTexImage2D(
gl.TEXTURE_2D,
0,
gl_format,
width,
height,
0,
new Uint8Array(data),
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.bindTexture(gl.TEXTURE_2D, null);
return gl_texture;
}
destroy_texture(texture?: WebGLTexture): void {
if (texture != undefined) {
this.gl.deleteTexture(texture);
}
}
}

View File

@ -1,135 +0,0 @@
import { Mat4, mat4_multiply } from "../../math/linear_algebra";
import { ShaderProgram } from "../ShaderProgram";
import pos_norm_vert_shader_source from "./pos_norm.vert";
import pos_norm_frag_shader_source from "./pos_norm.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";
import { Projection } from "../Camera";
import { VertexFormatType } from "../VertexFormat";
import { SceneNode } from "../Scene";
export class WebglRenderer extends GfxRenderer {
private readonly gl: WebGL2RenderingContext;
private readonly shader_programs: ShaderProgram[];
readonly gfx: WebglGfx;
constructor(projection: Projection) {
super(document.createElement("canvas"), projection);
const gl = this.canvas_element.getContext("webgl2");
if (gl == null) {
throw new Error("Failed to initialize webgl2 context.");
}
this.gl = gl;
this.gfx = new WebglGfx(gl);
gl.enable(gl.DEPTH_TEST);
gl.clearColor(0.1, 0.1, 0.1, 1);
this.shader_programs = [];
this.shader_programs[VertexFormatType.PosNorm] = new ShaderProgram(
gl,
pos_norm_vert_shader_source,
pos_norm_frag_shader_source,
);
this.shader_programs[VertexFormatType.PosTex] = new ShaderProgram(
gl,
pos_tex_vert_shader_source,
pos_tex_frag_shader_source,
);
this.set_size(800, 600);
}
dispose(): void {
for (const program of this.shader_programs) {
program.delete();
}
super.dispose();
}
set_size(width: number, height: number): void {
this.canvas_element.width = width;
this.canvas_element.height = height;
this.gl.viewport(0, 0, width, height);
super.set_size(width, height);
}
protected render(): void {
const gl = this.gl;
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// this.render_node(this.scene.root_node, this.camera.view_matrix);
this.scene.traverse((node, parent_mat) => {
const mat = mat4_multiply(parent_mat, node.transform);
if (node.mesh) {
const program = this.shader_programs[node.mesh.format];
program.bind();
program.set_mat_projection_uniform(this.camera.projection_matrix);
program.set_mat_model_view_uniform(mat);
program.set_mat_normal_uniform(mat.normal_mat3());
if (node.mesh.texture?.gfx_texture) {
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, node.mesh.texture.gfx_texture as WebGLTexture);
program.set_texture_uniform(gl.TEXTURE0);
}
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);
program.unbind();
}
return mat;
}, this.camera.view_matrix);
}
private render_node(node: SceneNode, parent_mat: Mat4): void {
const gl = this.gl;
const mat = mat4_multiply(parent_mat, node.transform);
if (node.mesh) {
const program = this.shader_programs[node.mesh.format];
program.bind();
program.set_mat_projection_uniform(this.camera.projection_matrix);
program.set_mat_model_view_uniform(mat);
program.set_mat_normal_uniform(mat.normal_mat3());
if (node.mesh.texture?.gfx_texture) {
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, node.mesh.texture.gfx_texture as WebGLTexture);
program.set_texture_uniform(gl.TEXTURE0);
}
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);
program.unbind();
}
for (const child of node.children) {
this.render_node(child, mat);
}
}
}

View File

@ -1,23 +0,0 @@
#version 300 es
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);
in vec3 frag_normal;
out vec4 frag_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) {
frag_color = mix(ground_color, sky_color, a);
} else {
frag_color = mix(ground_color, sky_color, a_back);
}
}

View File

@ -1,17 +0,0 @@
#version 300 es
precision mediump float;
uniform mat4 mat_projection;
uniform mat4 mat_model_view;
uniform mat3 mat_normal;
in vec4 pos;
in vec3 normal;
out vec3 frag_normal;
void main() {
gl_Position = mat_projection * mat_model_view * pos;
frag_normal = normalize(mat_normal * normal);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,187 +0,0 @@
import { Gfx } from "../Gfx";
import { Texture, TextureFormat } from "../Texture";
import { VERTEX_FORMATS, VertexFormatType } from "../VertexFormat";
import { assert } from "../../util";
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_layouts: readonly GPUBindGroupLayout[],
) {}
create_gfx_mesh(
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: format.uniform_buffer_size,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, // eslint-disable-line no-undef
});
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({
magFilter: "linear",
minFilter: "linear",
}),
},
{
binding: 2,
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, vertex_array_buffer] = this.device.createBufferMapped({
size: vertex_data.byteLength,
usage: GPUBufferUsage.VERTEX, // eslint-disable-line no-undef
});
new Uint8Array(vertex_array_buffer).set(new Uint8Array(vertex_data));
vertex_buffer.unmap();
const [index_buffer, index_array_buffer] = this.device.createBufferMapped({
size: index_data.byteLength,
usage: GPUBufferUsage.INDEX, // eslint-disable-line no-undef
});
new Uint8Array(index_array_buffer).set(new Uint8Array(index_data));
index_buffer.unmap();
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 {
let texture_format: string;
let bytes_per_pixel: number;
switch (format) {
case TextureFormat.RGBA_S3TC_DXT1:
texture_format = "bc1-rgba-unorm";
bytes_per_pixel = 2;
break;
case TextureFormat.RGBA_S3TC_DXT3:
texture_format = "bc2-rgba-unorm";
bytes_per_pixel = 4;
break;
}
const texture = this.device.createTexture({
size: {
width,
height,
depth: 1,
},
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;
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 = bytes_per_pixel * x + bytes_per_row * y;
for (let i = 0; i < bytes_per_pixel; i++) {
buffer_data[idx + i] = orig_data[orig_idx + i];
}
orig_idx += bytes_per_pixel;
}
}
}
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(
{
buffer,
bytesPerRow: bytes_per_row,
rowsPerImage: 0,
},
{
texture,
},
{
width,
height,
depth: 1,
},
);
this.device.defaultQueue.submit([command_encoder.finish()]);
buffer.destroy();
return texture;
}
destroy_texture(texture?: GPUTexture): void {
texture?.destroy();
}
}

View File

@ -1,326 +0,0 @@
import {
VERTEX_FORMATS,
VERTEX_NORMAL_LOC,
VERTEX_POS_LOC,
VERTEX_TEX_LOC,
VertexFormat,
VertexFormatType,
} from "../VertexFormat";
import { GfxRenderer } from "../GfxRenderer";
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";
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;
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,
pipelines.map(p => p.bind_group_layout),
);
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 {
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 {
const command_encoder = this.device.createCommandEncoder();
// 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 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({
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

@ -9,7 +9,7 @@ import "@fortawesome/fontawesome-free/js/brands";
import { initialize_application } from "./application";
import { FetchClient } from "./core/HttpClient";
import { WebGLRenderer } from "three";
import { DisposableThreeRenderer } from "./core/rendering/ThreeRenderer";
import { DisposableThreeRenderer } from "./core/rendering/Renderer";
import { Random } from "./core/Random";
import { DateClock } from "./core/Clock";

View File

@ -5,7 +5,7 @@ import { QuestRendererView } from "./QuestRendererView";
import { QuestEntityControls } from "../rendering/QuestEntityControls";
import { AreaAssetLoader } from "../loading/AreaAssetLoader";
import { EntityAssetLoader } from "../loading/EntityAssetLoader";
import { DisposableThreeRenderer } from "../../core/rendering/ThreeRenderer";
import { DisposableThreeRenderer } from "../../core/rendering/Renderer";
export class QuestEditorRendererView extends QuestRendererView {
private readonly entity_controls: QuestEntityControls;

View File

@ -4,7 +4,7 @@ import { QuestRendererView } from "./QuestRendererView";
import { QuestEditorStore } from "../stores/QuestEditorStore";
import { AreaAssetLoader } from "../loading/AreaAssetLoader";
import { EntityAssetLoader } from "../loading/EntityAssetLoader";
import { DisposableThreeRenderer } from "../../core/rendering/ThreeRenderer";
import { DisposableThreeRenderer } from "../../core/rendering/Renderer";
export class QuestRunnerRendererView extends QuestRendererView {
constructor(

View File

@ -7,7 +7,7 @@ import { AreaAssetLoader } from "./loading/AreaAssetLoader";
import { HttpClient } from "../core/HttpClient";
import { EntityImageRenderer } from "./rendering/EntityImageRenderer";
import { EntityAssetLoader } from "./loading/EntityAssetLoader";
import { DisposableThreeRenderer } from "../core/rendering/ThreeRenderer";
import { DisposableThreeRenderer } from "../core/rendering/Renderer";
import { QuestEditorUiPersister } from "./persistence/QuestEditorUiPersister";
import { QuestEditorToolBarView } from "./gui/QuestEditorToolBarView";
import { QuestEditorToolBarController } from "./controllers/QuestEditorToolBarController";

View File

@ -2,7 +2,7 @@ import { BufferGeometry, CylinderBufferGeometry, Texture } from "three";
import { LoadingCache } from "./LoadingCache";
import { Endianness } from "../../core/data_formats/Endianness";
import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor";
import { ninja_object_to_buffer_geometry } from "../../core/rendering/conversion/ninja_three_geometry";
import { ninja_object_to_buffer_geometry } from "../../core/rendering/conversion/ninja_geometry";
import { NjObject, parse_nj, parse_xj } from "../../core/data_formats/parsing/ninja";
import { parse_xvm } from "../../core/data_formats/parsing/ninja/texture";
import { xvm_to_three_textures } from "../../core/rendering/conversion/ninja_textures";

View File

@ -11,7 +11,7 @@ import { create_entity_type_mesh } from "./conversion/entities";
import { sequential } from "../../core/sequential";
import { EntityAssetLoader } from "../loading/EntityAssetLoader";
import { Disposable } from "../../core/observable/Disposable";
import { DisposableThreeRenderer } from "../../core/rendering/ThreeRenderer";
import { DisposableThreeRenderer } from "../../core/rendering/Renderer";
import { LoadingCache } from "../loading/LoadingCache";
import { DisposablePromise } from "../../core/DisposablePromise";

View File

@ -1,4 +1,4 @@
import { DisposableThreeRenderer, ThreeRenderer } from "../../core/rendering/ThreeRenderer";
import { DisposableThreeRenderer, Renderer } from "../../core/rendering/Renderer";
import { Group, Mesh, MeshLambertMaterial, Object3D, PerspectiveCamera } from "three";
import { QuestEntityModel } from "../model/QuestEntityModel";
import { Quest3DModelManager } from "./Quest3DModelManager";
@ -6,7 +6,7 @@ import { Disposer } from "../../core/observable/Disposer";
import { ColorType, EntityUserData, NPC_COLORS, OBJECT_COLORS } from "./conversion/entities";
import { QuestNpcModel } from "../model/QuestNpcModel";
export class QuestRenderer extends ThreeRenderer {
export class QuestRenderer extends Renderer {
private _collision_geometry = new Object3D();
private _render_geometry = new Object3D();
private _entity_models = new Object3D();

View File

@ -13,7 +13,7 @@ import {
import { CollisionObject } from "../../../core/data_formats/parsing/area_collision_geometry";
import { RenderObject } from "../../../core/data_formats/parsing/area_geometry";
import { GeometryBuilder } from "../../../core/rendering/conversion/GeometryBuilder";
import { ninja_object_to_geometry_builder } from "../../../core/rendering/conversion/ninja_three_geometry";
import { ninja_object_to_geometry_builder } from "../../../core/rendering/conversion/ninja_geometry";
import { SectionModel } from "../../model/SectionModel";
import { AreaVariantModel } from "../../model/AreaVariantModel";
import { vec3_to_threejs } from "../../../core/rendering/conversion";

View File

@ -4,7 +4,7 @@ import { CharacterClassAssetLoader } from "../../loading/CharacterClassAssetLoad
import { FileSystemHttpClient } from "../../../../test/src/core/FileSystemHttpClient";
import { ModelView } from "./ModelView";
import { ModelRenderer } from "../../rendering/ModelRenderer";
import { STUB_THREE_RENDERER } from "../../../../test/src/core/rendering/StubThreeRenderer";
import { STUB_RENDERER } from "../../../../test/src/core/rendering/StubRenderer";
import { Random } from "../../../core/Random";
import { ModelStore } from "../../stores/ModelStore";
import { ModelToolBarView } from "./ModelToolBarView";
@ -26,7 +26,7 @@ test("Renders correctly.", () =>
disposer.add(new ModelController(store)),
new ModelToolBarView(disposer.add(new ModelToolBarController(store))),
new CharacterClassOptionsView(disposer.add(new CharacterClassOptionsController(store))),
new ModelRenderer(store, STUB_THREE_RENDERER),
new ModelRenderer(store, STUB_RENDERER),
);
expect(view.element).toMatchSnapshot();

View File

@ -2,14 +2,12 @@ import { TextureView } from "./TextureView";
import { with_disposer } from "../../../../test/src/core/observables/disposable_helpers";
import { TextureController } from "../../controllers/texture/TextureController";
import { TextureRenderer } from "../../rendering/TextureRenderer";
import { StubGfxRenderer } from "../../../../test/src/core/rendering/StubGfxRenderer";
import { STUB_RENDERER } from "../../../../test/src/core/rendering/StubRenderer";
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 StubGfxRenderer())),
);
const view = disposer.add(new TextureView(ctrl, new TextureRenderer(ctrl, STUB_RENDERER)));
expect(view.element).toMatchSnapshot("Should render a toolbar and a renderer widget.");
}));

View File

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

View File

@ -4,9 +4,7 @@ import { HttpClient } from "../core/HttpClient";
import { Disposable } from "../core/observable/Disposable";
import { Disposer } from "../core/observable/Disposer";
import { Random } from "../core/Random";
import { Renderer } from "../core/rendering/Renderer";
import { DisposableThreeRenderer } from "../core/rendering/ThreeRenderer";
import { Projection } from "../core/rendering/Camera";
import { DisposableThreeRenderer } from "../core/rendering/Renderer";
export function initialize_viewer(
http_client: HttpClient,
@ -36,35 +34,14 @@ export function initialize_viewer(
const { CharacterClassOptionsController } = await import(
"./controllers/model/CharacterClassOptionsController"
);
const { ModelRenderer } = await import("./rendering/ModelRenderer");
const asset_loader = disposer.add(new CharacterClassAssetLoader(http_client));
const store = disposer.add(new ModelStore(gui_store, asset_loader, random));
const model_controller = new ModelController(store);
const model_tool_bar_controller = new ModelToolBarController(store);
const character_class_options_controller = new CharacterClassOptionsController(store);
let renderer: Renderer;
if (gui_store.feature_active("webgpu")) {
const { create_webgpu_renderer } = await import(
"../core/rendering/webgpu/WebgpuRenderer"
);
const { ModelGfxRenderer } = await import("./rendering/ModelGfxRenderer");
renderer = new ModelGfxRenderer(
store,
await create_webgpu_renderer(Projection.Perspective, http_client),
);
} else if (gui_store.feature_active("webgl")) {
const { WebglRenderer } = await import("../core/rendering/webgl/WebglRenderer");
const { ModelGfxRenderer } = await import("./rendering/ModelGfxRenderer");
renderer = new ModelGfxRenderer(store, new WebglRenderer(Projection.Perspective));
} else {
const { ModelRenderer } = await import("./rendering/ModelRenderer");
renderer = new ModelRenderer(store, create_three_renderer());
}
const renderer = new ModelRenderer(store, create_three_renderer());
return new ModelView(
model_controller,
@ -80,24 +57,7 @@ export function initialize_viewer(
const { TextureRenderer } = await import("./rendering/TextureRenderer");
const controller = disposer.add(new TextureController());
let renderer: Renderer;
if (gui_store.feature_active("webgpu")) {
const { create_webgpu_renderer } = await import(
"../core/rendering/webgpu/WebgpuRenderer"
);
renderer = new TextureRenderer(
controller,
await create_webgpu_renderer(Projection.Orthographic, http_client),
);
} else {
const { WebglRenderer } = await import("../core/rendering/webgl/WebglRenderer");
renderer = new TextureRenderer(
controller,
new WebglRenderer(Projection.Orthographic),
);
}
const renderer = new TextureRenderer(controller, create_three_renderer());
return new TextureView(controller, renderer);
},

View File

@ -1,88 +0,0 @@
import { ModelStore } from "../stores/ModelStore";
import { Disposer } from "../../core/observable/Disposer";
import { Renderer } from "../../core/rendering/Renderer";
import { GfxRenderer } from "../../core/rendering/GfxRenderer";
import { ninja_object_to_mesh } from "../../core/rendering/conversion/ninja_geometry";
import { SceneNode } from "../../core/rendering/Scene";
import { Mat4 } from "../../core/math/linear_algebra";
export class ModelGfxRenderer implements Renderer {
private readonly disposer = new Disposer();
readonly canvas_element: HTMLCanvasElement;
constructor(private readonly store: ModelStore, private readonly renderer: GfxRenderer) {
this.canvas_element = renderer.canvas_element;
renderer.camera.pan(0, 0, 50);
this.disposer.add_all(store.current_nj_object.observe(this.nj_object_or_xvm_changed));
// TODO: remove
// const cube = cube_mesh();
// cube.upload(this.renderer.gfx);
//
// this.renderer.scene.root_node.add_child(
// new SceneNode(
// undefined,
// Mat4.identity(),
// new SceneNode(
// cube,
// Mat4.compose(
// new Vec3(-3, 0, 0),
// quat_product(
// Quat.euler_angles(Math.PI / 6, 0, 0, EulerOrder.ZYX),
// Quat.euler_angles(0, -Math.PI / 6, 0, EulerOrder.ZYX),
// ),
// new Vec3(1, 1, 1),
// ),
// ),
// new SceneNode(
// cube,
// Mat4.compose(
// new Vec3(3, 0, 0),
// quat_product(
// Quat.euler_angles(-Math.PI / 6, 0, 0, EulerOrder.ZYX),
// Quat.euler_angles(0, Math.PI / 6, 0, EulerOrder.ZYX),
// ),
// new Vec3(1, 1, 1),
// ),
// ),
// ),
// );
}
dispose(): void {
this.disposer.dispose();
}
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 nj_object_or_xvm_changed = (): void => {
this.renderer.destroy_scene();
const nj_object = this.store.current_nj_object.val;
if (nj_object) {
// Convert textures and geometry.
const node = new SceneNode(ninja_object_to_mesh(nj_object), Mat4.identity());
this.renderer.scene.root_node.add_child(node);
this.renderer.scene.traverse(node => {
node.mesh?.upload(this.renderer.gfx);
}, undefined);
}
this.renderer.schedule_render();
};
}

View File

@ -14,12 +14,12 @@ import { Disposable } from "../../core/observable/Disposable";
import { NjMotion } from "../../core/data_formats/parsing/ninja/motion";
import { xvr_texture_to_three_texture } from "../../core/rendering/conversion/ninja_textures";
import { create_mesh } from "../../core/rendering/conversion/create_mesh";
import { ninja_object_to_buffer_geometry } from "../../core/rendering/conversion/ninja_three_geometry";
import { ninja_object_to_buffer_geometry } from "../../core/rendering/conversion/ninja_geometry";
import {
create_animation_clip,
PSO_FRAME_RATE,
} from "../../core/rendering/conversion/ninja_animation";
import { DisposableThreeRenderer, ThreeRenderer } from "../../core/rendering/ThreeRenderer";
import { DisposableThreeRenderer, Renderer } from "../../core/rendering/Renderer";
import { Disposer } from "../../core/observable/Disposer";
import { ChangeEvent } from "../../core/observable/Observable";
import { LogManager } from "../../core/Logger";
@ -40,7 +40,7 @@ const DEFAULT_SKINNED_MATERIAL = new MeshLambertMaterial({
const CAMERA_POSITION = Object.freeze(new Vector3(0, 10, 20));
const CAMERA_LOOK_AT = Object.freeze(new Vector3(0, 0, 0));
export class ModelRenderer extends ThreeRenderer implements Disposable {
export class ModelRenderer extends Renderer implements Disposable {
private readonly disposer = new Disposer();
private readonly clock = new Clock();
private character_class_active: boolean;

View File

@ -2,51 +2,61 @@ 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 { VertexFormatType } from "../../core/rendering/VertexFormat";
import { Mesh } from "../../core/rendering/Mesh";
import { GfxRenderer } from "../../core/rendering/GfxRenderer";
import { Renderer } from "../../core/rendering/Renderer";
import { xvr_texture_to_texture } from "../../core/rendering/conversion/ninja_textures";
import { Mat4, Vec2, Vec3 } from "../../core/math/linear_algebra";
import { SceneNode } from "../../core/rendering/Scene";
import { xvr_texture_to_three_texture } from "../../core/rendering/conversion/ninja_textures";
import {
Mesh,
MeshBasicMaterial,
OrthographicCamera,
PlaneGeometry,
Texture,
Vector2,
Vector3,
} from "three";
import { DisposableThreeRenderer, Renderer } from "../../core/rendering/Renderer";
import { Disposable } from "../../core/observable/Disposable";
const logger = LogManager.get("viewer/rendering/TextureRenderer");
export class TextureRenderer implements Renderer {
const CAMERA_POSITION = Object.freeze(new Vector3(0, 0, 5));
const CAMERA_LOOK_AT = Object.freeze(new Vector3(0, 0, 0));
export class TextureRenderer extends Renderer implements Disposable {
private readonly disposer = new Disposer();
private readonly quad_meshes: Mesh[] = [];
readonly canvas_element: HTMLCanvasElement;
readonly camera = new OrthographicCamera(-400, 400, 300, -300, 1, 10);
constructor(ctrl: TextureController, private readonly renderer: GfxRenderer) {
this.canvas_element = renderer.canvas_element;
renderer.camera.pan(0, 0, 10);
constructor(ctrl: TextureController, three_renderer: DisposableThreeRenderer) {
super(three_renderer);
this.disposer.add_all(
ctrl.textures.observe(({ value: textures }) => {
renderer.destroy_scene();
renderer.camera.reset();
this.scene.remove(...this.quad_meshes);
this.create_quads(textures);
renderer.schedule_render();
this.reset_camera(CAMERA_POSITION, CAMERA_LOOK_AT);
this.schedule_render();
}),
);
}
dispose(): void {
this.renderer.dispose();
this.disposer.dispose();
}
start_rendering(): void {
this.renderer.start_rendering();
}
stop_rendering(): void {
this.renderer.stop_rendering();
this.init_camera_controls();
this.controls.azimuthRotateSpeed = 0;
this.controls.polarRotateSpeed = 0;
}
set_size(width: number, height: number): void {
this.renderer.set_size(width, height);
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.disposer.dispose();
}
private create_quads(textures: readonly XvrTexture[]): void {
@ -62,37 +72,47 @@ export class TextureRenderer implements Renderer {
const y = -Math.floor(total_height / 2);
for (const tex of textures) {
try {
const quad_mesh = this.create_quad(tex);
quad_mesh.upload(this.renderer.gfx);
let texture: Texture | undefined = undefined;
this.renderer.scene.root_node.add_child(
new SceneNode(
quad_mesh,
Mat4.translation(x, y + (total_height - tex.height) / 2, 0),
),
);
try {
texture = xvr_texture_to_three_texture(tex);
} catch (e) {
logger.error("Couldn't create quad for texture.", e);
logger.error("Couldn't convert XVR 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(tex: XvrTexture): Mesh {
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))
.vertex(new Vec3(tex.width, tex.height, 0), new Vec2(1, 0))
.vertex(new Vec3(0, tex.height, 0), new Vec2(0, 0))
.triangle(0, 1, 2)
.triangle(2, 3, 0)
.texture(xvr_texture_to_texture(this.renderer.gfx, tex))
.build();
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;
}
}

View File

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

View File

@ -1,6 +1,6 @@
import { DisposableThreeRenderer } from "../../../../src/core/rendering/ThreeRenderer";
import { DisposableThreeRenderer } from "../../../../src/core/rendering/Renderer";
export const STUB_THREE_RENDERER: DisposableThreeRenderer = {
export const STUB_RENDERER: DisposableThreeRenderer = {
domElement: document.createElement("canvas"),
dispose(): void {}, // eslint-disable-line

View File

@ -17,10 +17,6 @@ module.exports = {
test: /\.(gif|jpg|png|svg|ttf)$/,
loader: "file-loader",
},
{
test: /\.(vert|frag)$/,
loader: "raw-loader",
},
],
},
plugins: [

View File

@ -1065,16 +1065,6 @@
"@webassemblyjs/wast-parser" "1.9.0"
"@xtuc/long" "4.2.2"
"@webgpu/glslang@^0.0.15":
version "0.0.15"
resolved "https://registry.yarnpkg.com/@webgpu/glslang/-/glslang-0.0.15.tgz#f5ccaf6015241e6175f4b90906b053f88483d1f2"
integrity sha512-niT+Prh3Aff8Uf1MVBVUsaNjFj9rJAKDXuoHIKiQbB+6IUP/3J3JIhBNyZ7lDhytvXxw6ppgnwKZdDJ08UMj4Q==
"@webgpu/types@^0.0.27":
version "0.0.27"
resolved "https://registry.yarnpkg.com/@webgpu/types/-/types-0.0.27.tgz#ce3b39f496109fc22dc22786ca5724f52c68d9e0"
integrity sha512-z1laHQvErLFM9nQSxfRuRetMk8iahidOdVQEdHWG9OjMKvXhHk6WnC98En0Bk0pWLQBvhiP1SGg3oHVlnKRucw==
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@ -6870,14 +6860,6 @@ raw-body@2.4.0:
iconv-lite "0.4.24"
unpipe "1.0.0"
raw-loader@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.1.tgz#14e1f726a359b68437e183d5a5b7d33a3eba6933"
integrity sha512-baolhQBSi3iNh1cglJjA0mYzga+wePk7vdEX//1dTFd+v4TsQlQE0jitJSNF1OIP82rdYulH7otaVmdlDaJ64A==
dependencies:
loader-utils "^2.0.0"
schema-utils "^2.6.5"
react-is@^16.12.0:
version "16.12.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"