diff --git a/src/core/math/index.ts b/src/core/math/index.ts index 8d12b245..40afcdd2 100644 --- a/src/core/math/index.ts +++ b/src/core/math/index.ts @@ -1,7 +1,5 @@ -import { assert } from "../util"; - const TO_DEG = 180 / Math.PI; -const TO_RAD = 1 / TO_DEG; +const TO_RAD = Math.PI / 180; /** * Converts radians to degrees. @@ -24,75 +22,3 @@ export function deg_to_rad(deg: number): number { export function floor_mod(dividend: number, divisor: number): number { return ((dividend % divisor) + divisor) % divisor; } - -export class Vec2 { - constructor(public x: number, public y: number) {} -} - -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) {} -} - -/** - * 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, - ) - } - - constructor(readonly data: Float32Array) { - assert(data.length === 16, "values should be of length 16."); - } -} - -export function mat4_product(a: Mat4, b: Mat4): Mat4 { - const c = new Mat4(new Float32Array(16)); - mat4_product_into_array(c.data, a, b); - return c; -} - -export function mat4_multiply(a: Mat4, b: Mat4): void { - const array = new Float32Array(16); - mat4_product_into_array(array, a, b); - a.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]; - } - } - } -} - -export class Quat { - constructor(public x: number, public y: number, public z: number, public w: number) {} -} diff --git a/src/core/math/linear_algebra.ts b/src/core/math/linear_algebra.ts new file mode 100644 index 00000000..6d151c37 --- /dev/null +++ b/src/core/math/linear_algebra.ts @@ -0,0 +1,333 @@ +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) {} +} + +/** + * 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_product(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(a: Mat3, b: Mat3): void { + const array = new Float32Array(9); + mat3_product_into_array(array, a, b); + a.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 the result in `v`. + */ +export function mat3_vec3_multiply(m: Mat3, v: 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; + v.x = x; + v.y = y; + v.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; + } + + 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_product(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 the result in `a`. + */ +export function mat4_multiply(a: Mat4, b: Mat4): void { + const array = new Float32Array(16); + mat4_product_into_array(array, a, b); + a.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 the result in `v`. Assumes `m` is affine. + */ +export function mat4_vec3_multiply(m: Mat4, v: 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); + v.x = x; + v.y = y; + v.z = z; +} diff --git a/src/core/math/quaternions.test.ts b/src/core/math/quaternions.test.ts new file mode 100644 index 00000000..db4e8cd5 --- /dev/null +++ b/src/core/math/quaternions.test.ts @@ -0,0 +1,31 @@ +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); + } +}); diff --git a/src/core/math/quaternions.ts b/src/core/math/quaternions.ts new file mode 100644 index 00000000..93b012e9 --- /dev/null +++ b/src/core/math/quaternions.ts @@ -0,0 +1,51 @@ +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) {} +} + +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, + ); +} diff --git a/src/core/rendering/Camera.ts b/src/core/rendering/Camera.ts index 35b48c01..0b51d5ed 100644 --- a/src/core/rendering/Camera.ts +++ b/src/core/rendering/Camera.ts @@ -1,5 +1,4 @@ -import { Mat4, Vec3 } from "../math"; -import { Mat4Transform, Transform } from "./Transform"; +import { Mat4, Vec3 } from "../math/linear_algebra"; export class Camera { private readonly look_at: Vec3 = new Vec3(0, 0, 0); @@ -7,17 +6,17 @@ export class Camera { private y_rot: number = 0; private z_rot: number = 0; private _zoom: number = 1; - private readonly _transform = new Mat4Transform(Mat4.identity()); + private readonly _mat4 = Mat4.identity(); - get transform(): Transform { - return this._transform; + get mat4(): Mat4 { + return this._mat4; } pan(x: number, y: number, z: number): this { this.look_at.x += x; this.look_at.y += y; this.look_at.z += z; - this.update_transform(); + this.update_matrix(); return this; } @@ -29,7 +28,7 @@ export class Camera { this.look_at.x *= factor; this.look_at.y *= factor; this.look_at.z *= factor; - this.update_transform(); + this.update_matrix(); return this; } @@ -41,16 +40,16 @@ export class Camera { this.y_rot = 0; this.z_rot = 0; this._zoom = 1; - this.update_transform(); + this.update_matrix(); return this; } - private update_transform(): void { - this._transform.data[12] = -this.look_at.x; - this._transform.data[13] = -this.look_at.y; - this._transform.data[14] = -this.look_at.z; - this._transform.data[0] = this._zoom; - this._transform.data[5] = this._zoom; - this._transform.data[10] = this._zoom; + private update_matrix(): void { + this._mat4.data[12] = -this.look_at.x; + this._mat4.data[13] = -this.look_at.y; + this._mat4.data[14] = -this.look_at.z; + this._mat4.data[0] = this._zoom; + this._mat4.data[5] = this._zoom; + this._mat4.data[10] = this._zoom; } } diff --git a/src/core/rendering/GfxRenderer.ts b/src/core/rendering/GfxRenderer.ts index 1bf26466..e9366b47 100644 --- a/src/core/rendering/GfxRenderer.ts +++ b/src/core/rendering/GfxRenderer.ts @@ -1,10 +1,9 @@ import { Renderer } from "./Renderer"; -import { VertexFormat } from "./VertexFormat"; -import { MeshBuilder } from "./MeshBuilder"; import { Scene } from "./Scene"; import { Camera } from "./Camera"; import { Gfx } from "./Gfx"; -import { Mat4, Vec2, vec2_diff } from "../math"; +import { Mat4, Vec2, vec2_diff } from "../math/linear_algebra"; +import { deg_to_rad } from "../math"; export abstract class GfxRenderer implements Renderer { private pointer_pos?: Vec2; @@ -20,7 +19,7 @@ export abstract class GfxRenderer implements Renderer { readonly camera = new Camera(); readonly canvas_element: HTMLCanvasElement = document.createElement("canvas"); - protected constructor() { + protected constructor(private readonly perspective_projection: boolean) { this.canvas_element.width = 800; this.canvas_element.height = 600; this.canvas_element.addEventListener("mousedown", this.mousedown); @@ -28,17 +27,42 @@ export abstract class GfxRenderer implements Renderer { } dispose(): void { - this.scene.destroy(); + this.destroy_scene(); } set_size(width: number, height: number): void { - // prettier-ignore - this.projection_mat = Mat4.of( - 2/width, 0, 0, 0, - 0, 2/height, 0, 0, - 0, 0, 2/10, 0, - 0, 0, 0, 1, - ); + if (this.perspective_projection) { + const fov = 75; + const aspect = width / height; + + const n = 0.1; + const f = 2000; + const t = n * Math.tan(deg_to_rad(0.5 * fov)); + const b = -t; + const r = aspect * t; + const l = -r; + + // prettier-ignore + this.projection_mat = Mat4.of( + 2*n / (r-l), 0, (r+l) / (r-l), 0, + 0, 2*n / (t-b), (t+b) / (t-b), 0, + 0, 0, -(f+n) / (f-n), -(2*f*n) / (f-n), + 0, 0, -1, 0, + ); + } else { + const w = width; + const h = height; + const n = -1000; + const f = 1000; + + // prettier-ignore + this.projection_mat = Mat4.of( + 2/w, 0, 0, 0, + 0, 2/h, 0, 0, + 0, 0, 2/(n-f), 0, + 0, 0, 0, 1, + ); + } this.schedule_render(); } @@ -68,8 +92,18 @@ export abstract class GfxRenderer implements Renderer { protected abstract render(): void; - mesh_builder(vertex_format: VertexFormat): MeshBuilder { - return new MeshBuilder(this.gfx, vertex_format); + /** + * 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 => { @@ -99,10 +133,18 @@ export abstract class GfxRenderer implements Renderer { }; private wheel = (evt: WheelEvent): void => { - if (evt.deltaY < 0) { - this.camera.zoom(1.1); + if (this.perspective_projection) { + if (evt.deltaY < 0) { + this.camera.pan(0, 0, -10); + } else { + this.camera.pan(0, 0, 10); + } } else { - this.camera.zoom(0.9); + if (evt.deltaY < 0) { + this.camera.zoom(1.1); + } else { + this.camera.zoom(0.9); + } } this.schedule_render(); diff --git a/src/core/rendering/Mesh.ts b/src/core/rendering/Mesh.ts index 397f4f1d..22b56ec0 100644 --- a/src/core/rendering/Mesh.ts +++ b/src/core/rendering/Mesh.ts @@ -1,12 +1,33 @@ import { VertexFormat } 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: VertexFormat.PosNorm): PosNormMeshBuilder; + static builder(format: VertexFormat.PosTex): PosTexMeshBuilder; + static builder(format: VertexFormat.PosNormTex): PosNormTexMeshBuilder; + static builder(format: VertexFormat): MeshBuilder { + switch (format) { + case VertexFormat.PosNorm: + return new PosNormMeshBuilder(); + case VertexFormat.PosTex: + return new PosTexMeshBuilder(); + case VertexFormat.PosNormTex: + return new PosNormTexMeshBuilder(); + } + } + /* eslint-enable no-dupe-class-members */ + gfx_mesh: unknown; constructor( - private readonly gfx: Gfx, readonly format: VertexFormat, readonly vertex_data: ArrayBuffer, readonly index_data: ArrayBuffer, @@ -14,17 +35,20 @@ export class Mesh { readonly texture?: Texture, ) {} - upload(): void { + upload(gfx: Gfx): void { this.texture?.upload(); - this.gfx_mesh = this.gfx.create_gfx_mesh( - this.format, - this.vertex_data, - this.index_data, - this.texture, - ); + + if (this.gfx_mesh == undefined) { + this.gfx_mesh = gfx.create_gfx_mesh( + this.format, + this.vertex_data, + this.index_data, + this.texture, + ); + } } - destroy(): void { - this.gfx.destroy_gfx_mesh(this.gfx_mesh); + destroy(gfx: Gfx): void { + gfx.destroy_gfx_mesh(this.gfx_mesh); } } diff --git a/src/core/rendering/MeshBuilder.ts b/src/core/rendering/MeshBuilder.ts index e01913d7..0b167b1f 100644 --- a/src/core/rendering/MeshBuilder.ts +++ b/src/core/rendering/MeshBuilder.ts @@ -1,76 +1,107 @@ import { Texture } from "./Texture"; -import { vertex_format_size, vertex_format_tex_offset, VertexFormat } from "./VertexFormat"; -import { assert } from "../util"; +import { + vertex_format_normal_offset, + vertex_format_size, + vertex_format_tex_offset, + VertexFormat, +} from "./VertexFormat"; import { Mesh } from "./Mesh"; -import { Gfx } from "./Gfx"; +import { Vec2, Vec3 } from "../math/linear_algebra"; -export class MeshBuilder { - private readonly vertex_data: { - x: number; - y: number; - z: number; - u?: number; - v?: number; +export abstract class MeshBuilder { + protected readonly vertex_data: { + pos: Vec3; + normal?: Vec3; + tex?: Vec2; }[] = []; - private readonly index_data: number[] = []; - private _texture?: Texture; + protected readonly index_data: number[] = []; + protected _texture?: Texture; - constructor(private readonly gfx: Gfx, private readonly format: VertexFormat) {} - - vertex(x: number, y: number, z: number, u?: number, v?: number): this { - switch (this.format) { - case VertexFormat.PosTex: - assert( - u != undefined && v != undefined, - `Vertex format ${VertexFormat[this.format]} requires texture coordinates.`, - ); - break; - } - - this.vertex_data.push({ x, y, z, u, v }); - return this; + get vertex_count(): number { + return this.vertex_data.length; } + protected constructor(private readonly format: VertexFormat) {} + triangle(v1: number, v2: number, v3: number): this { this.index_data.push(v1, v2, v3); return this; } + build(): Mesh { + const v_size = vertex_format_size(this.format); + const v_normal_offset = vertex_format_normal_offset(this.format); + const v_tex_offset = vertex_format_tex_offset(this.format); + const v_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 !== -1) { + v_view.setFloat32(i + v_normal_offset, normal!.x, true); + v_view.setFloat32(i + v_normal_offset + 4, normal!.y, true); + v_view.setFloat32(i + v_normal_offset + 8, normal!.z, true); + } + + if (v_tex_offset !== -1) { + 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 Uint16Array(2 * Math.ceil(this.index_data.length / 2)); + i_data.set(this.index_data); + + return new Mesh(this.format, v_data, i_data, this.index_data.length, this._texture); + } +} + +export class PosNormMeshBuilder extends MeshBuilder { + constructor() { + super(VertexFormat.PosNorm); + } + + vertex(pos: Vec3, normal: Vec3): this { + this.vertex_data.push({ pos, normal }); + return this; + } +} + +export class PosTexMeshBuilder extends MeshBuilder { + constructor() { + super(VertexFormat.PosTex); + } + + vertex(pos: Vec3, tex: Vec2): this { + this.vertex_data.push({ pos, tex }); + return this; + } + texture(tex: Texture): this { this._texture = tex; return this; } +} - build(): Mesh { - const v_size = vertex_format_size(this.format); - const v_tex_offset = vertex_format_tex_offset(this.format); - const v_data = new ArrayBuffer(this.vertex_data.length * v_size); - const v_view = new DataView(v_data); - let i = 0; +export class PosNormTexMeshBuilder extends MeshBuilder { + constructor() { + super(VertexFormat.PosNormTex); + } - for (const { x, y, z, u, v } of this.vertex_data) { - v_view.setFloat32(i, x, true); - v_view.setFloat32(i + 4, y, true); - v_view.setFloat32(i + 8, z, true); + vertex(pos: Vec3, normal: Vec3, tex: Vec2): this { + this.vertex_data.push({ pos, normal, tex }); + return this; + } - if (v_tex_offset !== -1) { - v_view.setUint16(i + v_tex_offset, u! * 0xffff, true); - v_view.setUint16(i + v_tex_offset + 2, v! * 0xffff, true); - } - - i += v_size; - } - - const i_data = new Uint16Array(2 * Math.ceil(this.index_data.length / 2)); - i_data.set(this.index_data); - - return new Mesh( - this.gfx, - this.format, - v_data, - i_data, - this.index_data.length, - this._texture, - ); + texture(tex: Texture): this { + this._texture = tex; + return this; } } diff --git a/src/core/rendering/Scene.ts b/src/core/rendering/Scene.ts index faaaece3..2a950b0d 100644 --- a/src/core/rendering/Scene.ts +++ b/src/core/rendering/Scene.ts @@ -1,28 +1,14 @@ -import { IdentityTransform, Transform } from "./Transform"; import { Mesh } from "./Mesh"; +import { Mat4 } from "../math/linear_algebra"; export class Scene { - readonly root_node = new Node(this, undefined, new IdentityTransform()); + readonly root_node = new SceneNode(undefined, Mat4.identity()); - /** - * Destroys all GPU objects related to this scene and resets the scene. - */ - destroy(): void { - this.traverse(node => { - node.mesh?.destroy(); - node.mesh?.texture?.destroy(); - node.mesh = undefined; - }, undefined); - - this.root_node.clear_children(); - this.root_node.transform = new IdentityTransform(); - } - - traverse(f: (node: Node, data: T) => T, data: T): void { + traverse(f: (node: SceneNode, data: T) => T, data: T): void { this.traverse_node(this.root_node, f, data); } - private traverse_node(node: Node, f: (node: Node, data: T) => T, data: T): void { + private traverse_node(node: SceneNode, f: (node: SceneNode, data: T) => T, data: T): void { const child_data = f(node, data); for (const child of node.children) { @@ -31,22 +17,19 @@ export class Scene { } } -export class Node { - private readonly _children: Node[] = []; +export class SceneNode { + private readonly _children: SceneNode[]; - get children(): readonly Node[] { + get children(): readonly SceneNode[] { return this._children; } - constructor( - private readonly scene: Scene, - public mesh: Mesh | undefined, - public transform: Transform, - ) {} + constructor(public mesh: Mesh | undefined, public transform: Mat4, ...children: SceneNode[]) { + this._children = children; + } - add_child(mesh: Mesh | undefined, transform: Transform): void { - this._children.push(new Node(this.scene, mesh, transform)); - mesh?.upload(); + add_child(child: SceneNode): void { + this._children.push(child); } clear_children(): void { diff --git a/src/core/rendering/ShaderProgram.ts b/src/core/rendering/ShaderProgram.ts index e8575bd0..a374f1b3 100644 --- a/src/core/rendering/ShaderProgram.ts +++ b/src/core/rendering/ShaderProgram.ts @@ -1,10 +1,12 @@ -import { Mat4 } from "../math"; -import { VERTEX_POS_LOC, VERTEX_TEX_LOC } from "./VertexFormat"; +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 transform_loc: WebGLUniformLocation; + private readonly mat_projection_loc: WebGLUniformLocation; + private readonly mat_camera_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) { @@ -24,6 +26,7 @@ export class ShaderProgram { 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); @@ -32,9 +35,9 @@ export class ShaderProgram { throw new Error("Shader linking failed. Program log:\n" + log); } - const transform_loc = gl.getUniformLocation(program, "transform"); - if (transform_loc == null) throw new Error("Couldn't get transform uniform location."); - this.transform_loc = transform_loc; + this.mat_projection_loc = this.get_required_uniform_location(program, "mat_projection"); + this.mat_camera_loc = this.get_required_uniform_location(program, "mat_camera"); + this.mat_normal_loc = gl.getUniformLocation(program, "mat_normal"); this.tex_sampler_loc = gl.getUniformLocation(program, "tex_sampler"); @@ -50,8 +53,16 @@ export class ShaderProgram { } } - set_transform_uniform(matrix: Mat4): void { - this.gl.uniformMatrix4fv(this.transform_loc, false, matrix.data); + set_mat_projection_uniform(matrix: Mat4): void { + this.gl.uniformMatrix4fv(this.mat_projection_loc, false, matrix.data); + } + + set_mat_camera_uniform(matrix: Mat4): void { + this.gl.uniformMatrix4fv(this.mat_camera_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 { @@ -69,6 +80,15 @@ export class ShaderProgram { 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 { diff --git a/src/core/rendering/Transform.ts b/src/core/rendering/Transform.ts deleted file mode 100644 index 4c45cfcf..00000000 --- a/src/core/rendering/Transform.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Mat4 } from "../math"; - -export interface Transform { - readonly mat4: Mat4; -} - -export class Mat4Transform implements Transform { - readonly data: Float32Array; - - constructor(readonly mat4: Mat4) { - this.data = mat4.data; - } -} - -export class TranslateTransform implements Transform { - readonly mat4: Mat4; - - constructor(x: number, y: number, z: number) { - // prettier-ignore - this.mat4 = Mat4.of( - 1, 0, 0, x, - 0, 1, 0, y, - 0, 0, 1, z, - 0, 0, 0, 1, - ); - } -} - -export class IdentityTransform implements Transform { - readonly mat4: Mat4; - - constructor() { - // prettier-ignore - this.mat4 = Mat4.of( - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1, - ); - } -} diff --git a/src/core/rendering/VertexFormat.ts b/src/core/rendering/VertexFormat.ts index 688f891d..6f034cdb 100644 --- a/src/core/rendering/VertexFormat.ts +++ b/src/core/rendering/VertexFormat.ts @@ -1,25 +1,41 @@ export enum VertexFormat { - Pos, + PosNorm, PosTex, + PosNormTex, } export const VERTEX_POS_LOC = 0; -export const VERTEX_TEX_LOC = 1; +export const VERTEX_NORMAL_LOC = 1; +export const VERTEX_TEX_LOC = 2; export function vertex_format_size(format: VertexFormat): number { switch (format) { - case VertexFormat.Pos: - return 12; + case VertexFormat.PosNorm: + return 24; case VertexFormat.PosTex: return 16; + case VertexFormat.PosNormTex: + return 28; + } +} + +export function vertex_format_normal_offset(format: VertexFormat): number { + switch (format) { + case VertexFormat.PosTex: + return -1; + case VertexFormat.PosNorm: + case VertexFormat.PosNormTex: + return 12; } } export function vertex_format_tex_offset(format: VertexFormat): number { switch (format) { - case VertexFormat.Pos: + case VertexFormat.PosNorm: return -1; case VertexFormat.PosTex: return 12; + case VertexFormat.PosNormTex: + return 24; } } diff --git a/src/core/rendering/conversion/MeshBuilder.ts b/src/core/rendering/conversion/MeshBuilder.ts new file mode 100644 index 00000000..6306dad7 --- /dev/null +++ b/src/core/rendering/conversion/MeshBuilder.ts @@ -0,0 +1,186 @@ +import { + Bone, + BufferGeometry, + Float32BufferAttribute, + Uint16BufferAttribute, + Vector3, +} from "three"; +import { map_get_or_put } from "../../util"; + +export type BuilderData = { + readonly created_by_geometry_builder: boolean; + readonly materials: BuilderMaterial[]; + readonly bones: Bone[]; +}; + +export type BuilderVec2 = { + readonly x: number; + readonly y: number; +}; + +export type BuilderVec3 = { + readonly x: number; + readonly y: number; + readonly z: number; +}; + +export type BuilderMaterial = { + readonly texture_id?: number; + readonly alpha: boolean; + readonly additive_blending: boolean; +}; + +/** + * Maps various material properties to material IDs. + */ +export class MaterialMap { + private readonly materials: BuilderMaterial[] = [{ alpha: false, additive_blending: false }]; + private readonly map = new Map(); + + /** + * Returns an index to an existing material if one exists for the given arguments. Otherwise + * adds a new material and returns its index. + */ + add_material( + texture_id?: number, + alpha: boolean = false, + additive_blending: boolean = false, + ): number { + if (texture_id == undefined) { + return 0; + } else { + const key = (texture_id << 2) | (alpha ? 0b10 : 0) | (additive_blending ? 1 : 0); + return map_get_or_put(this.map, key, () => { + this.materials.push({ texture_id, alpha, additive_blending }); + return this.materials.length - 1; + }); + } + } + + get_materials(): BuilderMaterial[] { + return this.materials; + } +} + +type VertexGroup = { + offset: number; + size: number; + material_index: number; +}; + +export class GeometryBuilder { + private readonly positions: number[] = []; + private readonly normals: number[] = []; + private readonly uvs: number[] = []; + private readonly indices: number[] = []; + private readonly bones: Bone[] = []; + private readonly bone_indices: number[] = []; + private readonly bone_weights: number[] = []; + private readonly groups: VertexGroup[] = []; + /** + * Will contain all material indices used in {@link this.groups} and -1 for the dummy material. + */ + private readonly material_map = new MaterialMap(); + + get vertex_count(): number { + return this.positions.length / 3; + } + + get index_count(): number { + return this.indices.length; + } + + get_position(index: number): Vector3 { + return new Vector3( + this.positions[3 * index], + this.positions[3 * index + 1], + this.positions[3 * index + 2], + ); + } + + get_normal(index: number): Vector3 { + return new Vector3( + this.normals[3 * index], + this.normals[3 * index + 1], + this.normals[3 * index + 2], + ); + } + + add_vertex(position: BuilderVec3, normal: BuilderVec3, uv: BuilderVec2): void { + this.positions.push(position.x, position.y, position.z); + this.normals.push(normal.x, normal.y, normal.z); + this.uvs.push(uv.x, uv.y); + } + + add_index(index: number): void { + this.indices.push(index); + } + + add_bone(bone: Bone): void { + this.bones.push(bone); + } + + add_bone_weight(index: number, weight: number): void { + this.bone_indices.push(index); + this.bone_weights.push(weight); + } + + add_group( + offset: number, + size: number, + texture_id?: number, + alpha: boolean = false, + additive_blending: boolean = false, + ): void { + const last_group = this.groups[this.groups.length - 1]; + const material_index = this.material_map.add_material(texture_id, alpha, additive_blending); + + if (last_group && last_group.material_index === material_index) { + last_group.size += size; + } else { + this.groups.push({ + offset, + size, + material_index, + }); + } + } + + build(): BufferGeometry { + const geom = new BufferGeometry(); + + geom.setAttribute("position", new Float32BufferAttribute(this.positions, 3)); + geom.setAttribute("normal", new Float32BufferAttribute(this.normals, 3)); + geom.setAttribute("uv", new Float32BufferAttribute(this.uvs, 2)); + + geom.setIndex(new Uint16BufferAttribute(this.indices, 1)); + + let bones: Bone[]; + + if (this.bone_indices.length && this.bones.length) { + geom.setAttribute("skinIndex", new Uint16BufferAttribute(this.bone_indices, 4)); + geom.setAttribute("skinWeight", new Float32BufferAttribute(this.bone_weights, 4)); + bones = this.bones; + } else { + bones = []; + } + + for (const group of this.groups) { + geom.addGroup(group.offset, group.size, group.material_index); + } + + // noinspection UnnecessaryLocalVariableJS + const data: BuilderData = { + created_by_geometry_builder: true, + materials: this.material_map.get_materials(), + bones, + }; + + geom.userData = data; + + geom.computeBoundingSphere(); + geom.computeBoundingBox(); + + return geom; + } +} diff --git a/src/core/rendering/conversion/index.ts b/src/core/rendering/conversion/index.ts index 7edbd0b7..d198fe75 100644 --- a/src/core/rendering/conversion/index.ts +++ b/src/core/rendering/conversion/index.ts @@ -1,6 +1,11 @@ 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); +} diff --git a/src/core/rendering/conversion/ninja_geometry.ts b/src/core/rendering/conversion/ninja_geometry.ts index 44132b1a..0a9b9b19 100644 --- a/src/core/rendering/conversion/ninja_geometry.ts +++ b/src/core/rendering/conversion/ninja_geometry.ts @@ -1,28 +1,26 @@ -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"; +import { vec3_to_math } from "./index"; +import { Mesh } from "../Mesh"; +import { SceneNode } from "../Scene"; +import { VertexFormat } from "../VertexFormat"; +import { EulerOrder, Quat } from "../../math/quaternions"; +import { Mat4, Vec2, Vec3 } from "../../math/linear_algebra"; -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); +const DEFAULT_NORMAL = new Vec3(0, 1, 0); +const DEFAULT_UV = new Vec2(0, 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); -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); +export function ninja_object_to_node(object: NjObject): SceneNode { + return new NodeCreator().to_node(object); } type Vertex = { - bone_id: number; - position: Vector3; - normal?: Vector3; + position: Vec3; + normal?: Vec3; bone_weight: number; bone_weight_status: number; calc_continue: boolean; @@ -50,29 +48,10 @@ class VerticesHolder { } } -class GeometryCreator { +class NodeCreator { 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 { + to_node(object: NjObject): SceneNode { const { no_translate, no_rotate, @@ -84,72 +63,50 @@ class GeometryCreator { } = 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 = Mat4.compose( + no_translate ? NO_TRANSLATION : 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 : scale, ); - 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); - } - } + let mesh: Mesh | undefined; if (object.model && !hidden) { - this.model_to_geometry(object.model, matrix); + mesh = this.model_to_mesh(object.model); } - this.bone_id++; + const node = new SceneNode(mesh, matrix); if (!break_child_trace) { for (const child of object.children) { - this.object_to_geometry(child, bone, matrix); + node.add_child(this.to_node(child)); } } + + return node; } - private model_to_geometry(model: NjModel, matrix: Matrix4): void { + private model_to_mesh(model: NjModel): Mesh { if (is_njcm_model(model)) { - this.njcm_model_to_geometry(model, matrix); + return this.njcm_model_to_mesh(model); } else { - this.xj_model_to_geometry(model, matrix); + return this.xj_model_to_mesh(model); } } - private njcm_model_to_geometry(model: NjcmModel, matrix: Matrix4): void { - const normal_matrix = new Matrix3().getNormalMatrix(matrix); - + private njcm_model_to_mesh(model: NjcmModel): Mesh { 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); + const position = vec3_to_math(vertex.position); + const normal = vertex.normal ? vec3_to_math(vertex.normal) : DEFAULT_NORMAL; return { - bone_id: this.bone_id, position, normal, bone_weight: vertex.bone_weight, @@ -160,155 +117,59 @@ class GeometryCreator { this.vertices.put(new_vertices); - for (const mesh of model.meshes) { - const start_index_count = this.builder.index_count; + const builder = Mesh.builder(VertexFormat.PosNorm); + for (const mesh of model.meshes) { 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; + const normal = vertex.normal ?? mesh_vertex.normal ?? DEFAULT_NORMAL; + const index = builder.vertex_count; - this.builder.add_vertex( - vertex.position, - normal, - mesh.has_tex_coords ? mesh_vertex.tex_coords! : DEFAULT_UV, - ); + builder.vertex(vertex.position, normal); - if (i >= 2) { + if (index >= 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); + builder.triangle(index - 2, index - 1, index); } else { - this.builder.add_index(index - 2); - this.builder.add_index(index); - this.builder.add_index(index - 1); + builder.triangle(index - 2, 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, - ); } + + return builder.build(); } - private xj_model_to_geometry(model: XjModel, matrix: Matrix4): void { - const index_offset = this.builder.vertex_count; - const normal_matrix = new Matrix3().getNormalMatrix(matrix); + private xj_model_to_mesh(model: XjModel): Mesh { + const builder = Mesh.builder(VertexFormat.PosNorm); - 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); + for (const { position, normal } of model.vertices) { + builder.vertex(vec3_to_math(position), normal ? vec3_to_math(normal) : DEFAULT_NORMAL); } - 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)); + const a = mesh.indices[j - 2]; + const b = mesh.indices[j - 1]; + const c = mesh.indices[j]; 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); + builder.triangle(b, a, c); } else { - this.builder.add_index(a); - this.builder.add_index(b); - this.builder.add_index(c); + builder.triangle(a, b, 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, - ); } + + return builder.build(); } } diff --git a/src/core/rendering/conversion/ninja_textures.ts b/src/core/rendering/conversion/ninja_textures.ts index ae8dc3e1..f32c0c4d 100644 --- a/src/core/rendering/conversion/ninja_textures.ts +++ b/src/core/rendering/conversion/ninja_textures.ts @@ -1,46 +1,69 @@ import { + CompressedPixelFormat, CompressedTexture, LinearFilter, MirroredRepeatWrapping, RGBA_S3TC_DXT1_Format, RGBA_S3TC_DXT3_Format, - Texture, - CompressedPixelFormat, + 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 xvm_to_textures(xvm: Xvm): Texture[] { - return xvm.textures.map(xvr_texture_to_texture); +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 xvr_texture_to_texture(tex: XvrTexture): Texture { +export function xvm_to_three_textures(xvm: Xvm): ThreeTexture[] { + return xvm.textures.map(xvr_texture_to_three_texture); +} + +export function xvr_texture_to_three_texture(xvr: XvrTexture): ThreeTexture { let format: CompressedPixelFormat; let data_size: number; // Ignore mipmaps. - switch (tex.format[1]) { + switch (xvr.format[1]) { case 6: format = RGBA_S3TC_DXT1_Format; - data_size = (tex.width * tex.height) / 2; + data_size = (xvr.width * xvr.height) / 2; break; case 7: format = RGBA_S3TC_DXT3_Format; - data_size = tex.width * tex.height; + data_size = xvr.width * xvr.height; break; default: - throw new Error(`Format ${tex.format.join(", ")} not supported.`); + throw new Error(`Format ${xvr.format.join(", ")} not supported.`); } const texture_3js = new CompressedTexture( [ { - data: new Uint8Array(tex.data, 0, data_size) as any, - width: tex.width, - height: tex.height, + data: new Uint8Array(xvr.data, 0, data_size) as any, + width: xvr.width, + height: xvr.height, }, ], - tex.width, - tex.height, + xvr.width, + xvr.height, format, ); diff --git a/src/core/rendering/conversion/ninja_three_geometry.ts b/src/core/rendering/conversion/ninja_three_geometry.ts new file mode 100644 index 00000000..44132b1a --- /dev/null +++ b/src/core/rendering/conversion/ninja_three_geometry.ts @@ -0,0 +1,314 @@ +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, + ); + } + } +} diff --git a/src/core/rendering/meshes.ts b/src/core/rendering/meshes.ts new file mode 100644 index 00000000..c8d20ac5 --- /dev/null +++ b/src/core/rendering/meshes.ts @@ -0,0 +1,58 @@ +import { Mesh } from "./Mesh"; +import { VertexFormat } from "./VertexFormat"; +import { Vec3 } from "../math/linear_algebra"; + +export function cube_mesh(): Mesh { + return ( + Mesh.builder(VertexFormat.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() + ); +} diff --git a/src/core/rendering/webgl/WebglGfx.ts b/src/core/rendering/webgl/WebglGfx.ts index 63ba72ab..3f042ef3 100644 --- a/src/core/rendering/webgl/WebglGfx.ts +++ b/src/core/rendering/webgl/WebglGfx.ts @@ -1,8 +1,10 @@ import { Gfx } from "../Gfx"; import { Texture, TextureFormat } from "../Texture"; import { + vertex_format_normal_offset, vertex_format_size, vertex_format_tex_offset, + VERTEX_NORMAL_LOC, VERTEX_POS_LOC, VERTEX_TEX_LOC, VertexFormat, @@ -49,6 +51,20 @@ export class WebglGfx implements Gfx { gl.vertexAttribPointer(VERTEX_POS_LOC, 3, gl.FLOAT, true, vertex_size, 0); gl.enableVertexAttribArray(VERTEX_POS_LOC); + const normal_offset = vertex_format_normal_offset(format); + + if (normal_offset !== -1) { + gl.vertexAttribPointer( + VERTEX_NORMAL_LOC, + 3, + gl.FLOAT, + true, + vertex_size, + normal_offset, + ); + gl.enableVertexAttribArray(VERTEX_NORMAL_LOC); + } + const tex_offset = vertex_format_tex_offset(format); if (tex_offset !== -1) { diff --git a/src/core/rendering/webgl/WebglRenderer.ts b/src/core/rendering/webgl/WebglRenderer.ts index 20936eb9..be1fc89a 100644 --- a/src/core/rendering/webgl/WebglRenderer.ts +++ b/src/core/rendering/webgl/WebglRenderer.ts @@ -1,7 +1,7 @@ -import { mat4_product } from "../../math"; +import { mat4_product } from "../../math/linear_algebra"; import { ShaderProgram } from "../ShaderProgram"; -import pos_vert_shader_source from "./pos.vert"; -import pos_frag_shader_source from "./pos.frag"; +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"; @@ -13,8 +13,8 @@ export class WebglRenderer extends GfxRenderer { readonly gfx: WebglGfx; - constructor() { - super(); + constructor(perspective_projection: boolean) { + super(perspective_projection); const gl = this.canvas_element.getContext("webgl2"); @@ -30,7 +30,7 @@ export class WebglRenderer extends GfxRenderer { gl.clearColor(0.1, 0.1, 0.1, 1); this.shader_programs = [ - new ShaderProgram(gl, pos_vert_shader_source, pos_frag_shader_source), + new ShaderProgram(gl, pos_norm_vert_shader_source, pos_norm_frag_shader_source), new ShaderProgram(gl, pos_tex_vert_shader_source, pos_tex_frag_shader_source), ]; @@ -58,16 +58,16 @@ export class WebglRenderer extends GfxRenderer { gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - const camera_project_mat = mat4_product(this.projection_mat, this.camera.transform.mat4); - this.scene.traverse((node, parent_mat) => { - const mat = mat4_product(parent_mat, node.transform.mat4); + const mat = mat4_product(parent_mat, node.transform); if (node.mesh) { const program = this.shader_programs[node.mesh.format]; program.bind(); - program.set_transform_uniform(mat); + program.set_mat_projection_uniform(this.projection_mat); + program.set_mat_camera_uniform(mat); + program.set_mat_normal_uniform(mat.normal_mat3()); if (node.mesh.texture?.gfx_texture) { gl.activeTexture(gl.TEXTURE0); @@ -86,6 +86,6 @@ export class WebglRenderer extends GfxRenderer { } return mat; - }, camera_project_mat); + }, this.camera.mat4); } } diff --git a/src/core/rendering/webgl/pos.frag b/src/core/rendering/webgl/pos.frag deleted file mode 100644 index efe76aa8..00000000 --- a/src/core/rendering/webgl/pos.frag +++ /dev/null @@ -1,9 +0,0 @@ -#version 300 es - -precision mediump float; - -out vec4 frag_color; - -void main() { - frag_color = vec4(0, 1, 1, 1); -} diff --git a/src/core/rendering/webgl/pos.vert b/src/core/rendering/webgl/pos.vert deleted file mode 100644 index 0bf6f27b..00000000 --- a/src/core/rendering/webgl/pos.vert +++ /dev/null @@ -1,11 +0,0 @@ -#version 300 es - -precision mediump float; - -uniform mat4 transform; - -in vec4 pos; - -void main() { - gl_Position = transform * pos; -} diff --git a/src/core/rendering/webgl/pos_norm.frag b/src/core/rendering/webgl/pos_norm.frag new file mode 100644 index 00000000..0a1e9a3b --- /dev/null +++ b/src/core/rendering/webgl/pos_norm.frag @@ -0,0 +1,23 @@ +#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); + } +} diff --git a/src/core/rendering/webgl/pos_norm.vert b/src/core/rendering/webgl/pos_norm.vert new file mode 100644 index 00000000..7bcd3e03 --- /dev/null +++ b/src/core/rendering/webgl/pos_norm.vert @@ -0,0 +1,17 @@ +#version 300 es + +precision mediump float; + +uniform mat4 mat_projection; +uniform mat4 mat_camera; +uniform mat3 mat_normal; + +in vec4 pos; +in vec3 normal; + +out vec3 frag_normal; + +void main() { + gl_Position = mat_projection * mat_camera * pos; + frag_normal = normalize(mat_normal * normal); +} diff --git a/src/core/rendering/webgl/pos_tex.vert b/src/core/rendering/webgl/pos_tex.vert index 61fbbd01..a7c55ab1 100644 --- a/src/core/rendering/webgl/pos_tex.vert +++ b/src/core/rendering/webgl/pos_tex.vert @@ -2,7 +2,8 @@ precision mediump float; -uniform mat4 transform; +uniform mat4 mat_projection; +uniform mat4 mat_camera; in vec4 pos; in vec2 tex; @@ -10,6 +11,6 @@ in vec2 tex; out vec2 f_tex; void main() { - gl_Position = transform * pos; + gl_Position = mat_projection * mat_camera * pos; f_tex = tex; } diff --git a/src/core/rendering/webgpu/WebgpuRenderer.ts b/src/core/rendering/webgpu/WebgpuRenderer.ts index 78243277..b24cb180 100644 --- a/src/core/rendering/webgpu/WebgpuRenderer.ts +++ b/src/core/rendering/webgpu/WebgpuRenderer.ts @@ -1,7 +1,7 @@ import { LogManager } from "../../Logger"; import { vertex_format_size, VertexFormat } from "../VertexFormat"; import { GfxRenderer } from "../GfxRenderer"; -import { mat4_product } from "../../math"; +import { mat4_product } from "../../math/linear_algebra"; import { WebgpuGfx, WebgpuMesh } from "./WebgpuGfx"; import { ShaderLoader } from "./ShaderLoader"; import { HttpClient } from "../../HttpClient"; @@ -30,8 +30,8 @@ export class WebgpuRenderer extends GfxRenderer { return this.gpu!.gfx; } - constructor(http_client: HttpClient) { - super(); + constructor(perspective_projection: boolean, http_client: HttpClient) { + super(perspective_projection); this.shader_loader = new ShaderLoader(http_client); @@ -175,13 +175,10 @@ export class WebgpuRenderer extends GfxRenderer { pass_encoder.setPipeline(pipeline); - const camera_project_mat = mat4_product( - this.projection_mat, - this.camera.transform.mat4, - ); + const camera_project_mat = mat4_product(this.projection_mat, this.camera.mat4); this.scene.traverse((node, parent_mat) => { - const mat = mat4_product(parent_mat, node.transform.mat4); + const mat = mat4_product(parent_mat, node.transform); if (node.mesh) { const gfx_mesh = node.mesh.gfx_mesh as WebgpuMesh; diff --git a/src/quest_editor/loading/EntityAssetLoader.ts b/src/quest_editor/loading/EntityAssetLoader.ts index cbf69cf2..4bb4effe 100644 --- a/src/quest_editor/loading/EntityAssetLoader.ts +++ b/src/quest_editor/loading/EntityAssetLoader.ts @@ -2,10 +2,10 @@ 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_geometry"; +import { ninja_object_to_buffer_geometry } from "../../core/rendering/conversion/ninja_three_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_textures } from "../../core/rendering/conversion/ninja_textures"; +import { xvm_to_three_textures } from "../../core/rendering/conversion/ninja_textures"; import { object_data, ObjectType } from "../../core/data_formats/parsing/quest/object_types"; import { NpcType } from "../../core/data_formats/parsing/quest/npc_types"; import { @@ -98,7 +98,7 @@ export class EntityAssetLoader implements Disposable { .then(({ data }) => { const cursor = new ArrayBufferCursor(data, Endianness.Little); const xvm = parse_xvm(cursor); - return xvm.success ? xvm_to_textures(xvm.value) : []; + return xvm.success ? xvm_to_three_textures(xvm.value) : []; }) .catch(e => { logger.warn( diff --git a/src/quest_editor/rendering/conversion/areas.ts b/src/quest_editor/rendering/conversion/areas.ts index 9c4f30f3..1b69ca99 100644 --- a/src/quest_editor/rendering/conversion/areas.ts +++ b/src/quest_editor/rendering/conversion/areas.ts @@ -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_geometry"; +import { ninja_object_to_geometry_builder } from "../../../core/rendering/conversion/ninja_three_geometry"; import { SectionModel } from "../../model/SectionModel"; import { AreaVariantModel } from "../../model/AreaVariantModel"; import { vec3_to_threejs } from "../../../core/rendering/conversion"; diff --git a/src/viewer/controllers/TextureController.ts b/src/viewer/controllers/texture/TextureController.ts similarity index 75% rename from src/viewer/controllers/TextureController.ts rename to src/viewer/controllers/texture/TextureController.ts index 81d0d553..8e95ef6d 100644 --- a/src/viewer/controllers/TextureController.ts +++ b/src/viewer/controllers/texture/TextureController.ts @@ -1,19 +1,19 @@ -import { Controller } from "../../core/controllers/Controller"; -import { filename_extension } from "../../core/util"; -import { read_file } from "../../core/files"; -import { is_xvm, parse_xvm, XvrTexture } from "../../core/data_formats/parsing/ninja/texture"; -import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor"; -import { Endianness } from "../../core/data_formats/Endianness"; -import { parse_afs } from "../../core/data_formats/parsing/afs"; -import { LogManager } from "../../core/Logger"; -import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty"; -import { list_property, property } from "../../core/observable"; -import { ListProperty } from "../../core/observable/property/list/ListProperty"; -import { prs_decompress } from "../../core/data_formats/compression/prs/decompress"; -import { failure, Result, result_builder } from "../../core/Result"; -import { Severity } from "../../core/Severity"; -import { Property } from "../../core/observable/property/Property"; -import { WritableProperty } from "../../core/observable/property/WritableProperty"; +import { Controller } from "../../../core/controllers/Controller"; +import { filename_extension } from "../../../core/util"; +import { read_file } from "../../../core/files"; +import { is_xvm, parse_xvm, XvrTexture } from "../../../core/data_formats/parsing/ninja/texture"; +import { ArrayBufferCursor } from "../../../core/data_formats/cursor/ArrayBufferCursor"; +import { Endianness } from "../../../core/data_formats/Endianness"; +import { parse_afs } from "../../../core/data_formats/parsing/afs"; +import { LogManager } from "../../../core/Logger"; +import { WritableListProperty } from "../../../core/observable/property/list/WritableListProperty"; +import { list_property, property } from "../../../core/observable"; +import { ListProperty } from "../../../core/observable/property/list/ListProperty"; +import { prs_decompress } from "../../../core/data_formats/compression/prs/decompress"; +import { failure, Result, result_builder } from "../../../core/Result"; +import { Severity } from "../../../core/Severity"; +import { Property } from "../../../core/observable/property/Property"; +import { WritableProperty } from "../../../core/observable/property/WritableProperty"; const logger = LogManager.get("viewer/controllers/TextureController"); diff --git a/src/viewer/gui/ViewerView.ts b/src/viewer/gui/ViewerView.ts index 80e65872..f0e6a9e1 100644 --- a/src/viewer/gui/ViewerView.ts +++ b/src/viewer/gui/ViewerView.ts @@ -1,6 +1,6 @@ import { TabContainer } from "../../core/gui/TabContainer"; import { ModelView } from "./model/ModelView"; -import { TextureView } from "./TextureView"; +import { TextureView } from "./texture/TextureView"; import { ResizableView } from "../../core/gui/ResizableView"; import { GuiStore } from "../../core/stores/GuiStore"; diff --git a/src/viewer/gui/model/ModelView.ts b/src/viewer/gui/model/ModelView.ts index 599e0161..dd7a7c76 100644 --- a/src/viewer/gui/model/ModelView.ts +++ b/src/viewer/gui/model/ModelView.ts @@ -1,6 +1,5 @@ import "./ModelView.css"; import { RendererWidget } from "../../../core/gui/RendererWidget"; -import { ModelRenderer } from "../../rendering/ModelRenderer"; import { ModelToolBarView } from "./ModelToolBarView"; import { CharacterClassSelectionView } from "./CharacterClassSelectionView"; import { CharacterClassModel } from "../../model/CharacterClassModel"; @@ -9,6 +8,7 @@ import { ModelController } from "../../controllers/model/ModelController"; import { div } from "../../../core/gui/dom"; import { ResizableView } from "../../../core/gui/ResizableView"; import { CharacterClassOptionsView } from "./CharacterClassOptionsView"; +import { Renderer } from "../../../core/rendering/Renderer"; const CHARACTER_CLASS_SELECTION_WIDTH = 100; const CHARACTER_CLASS_OPTIONS_WIDTH = 220; @@ -27,7 +27,7 @@ export class ModelView extends ResizableView { ctrl: ModelController, tool_bar_view: ModelToolBarView, options_view: CharacterClassOptionsView, - renderer: ModelRenderer, + renderer: Renderer, ) { super(); diff --git a/src/viewer/gui/TextureView.test.ts b/src/viewer/gui/texture/TextureView.test.ts similarity index 56% rename from src/viewer/gui/TextureView.test.ts rename to src/viewer/gui/texture/TextureView.test.ts index 54590fe3..eb71a674 100644 --- a/src/viewer/gui/TextureView.test.ts +++ b/src/viewer/gui/texture/TextureView.test.ts @@ -1,8 +1,8 @@ import { TextureView } from "./TextureView"; -import { with_disposer } from "../../../test/src/core/observables/disposable_helpers"; -import { TextureController } from "../controllers/TextureController"; -import { TextureRenderer } from "../rendering/TextureRenderer"; -import { StubGfxRenderer } from "../../../test/src/core/rendering/StubGfxRenderer"; +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"; test("Renders correctly without textures.", () => with_disposer(disposer => { diff --git a/src/viewer/gui/TextureView.ts b/src/viewer/gui/texture/TextureView.ts similarity index 77% rename from src/viewer/gui/TextureView.ts rename to src/viewer/gui/texture/TextureView.ts index 4c6f15e1..2ccd0154 100644 --- a/src/viewer/gui/TextureView.ts +++ b/src/viewer/gui/texture/TextureView.ts @@ -1,11 +1,11 @@ -import { div, Icon } from "../../core/gui/dom"; -import { FileButton } from "../../core/gui/FileButton"; -import { ToolBar } from "../../core/gui/ToolBar"; -import { RendererWidget } from "../../core/gui/RendererWidget"; -import { ResizableView } from "../../core/gui/ResizableView"; -import { TextureController } from "../controllers/TextureController"; -import { ResultDialog } from "../../core/gui/ResultDialog"; -import { Renderer } from "../../core/rendering/Renderer"; +import { div, Icon } from "../../../core/gui/dom"; +import { FileButton } from "../../../core/gui/FileButton"; +import { ToolBar } from "../../../core/gui/ToolBar"; +import { RendererWidget } from "../../../core/gui/RendererWidget"; +import { ResizableView } from "../../../core/gui/ResizableView"; +import { TextureController } from "../../controllers/texture/TextureController"; +import { ResultDialog } from "../../../core/gui/ResultDialog"; +import { Renderer } from "../../../core/rendering/Renderer"; export class TextureView extends ResizableView { readonly element = div({ className: "viewer_TextureView" }); diff --git a/src/viewer/gui/texture/__snapshots__/TextureView.test.ts.snap b/src/viewer/gui/texture/__snapshots__/TextureView.test.ts.snap new file mode 100644 index 00000000..1e0ba2d9 --- /dev/null +++ b/src/viewer/gui/texture/__snapshots__/TextureView.test.ts.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Renders correctly without textures.: Should render a toolbar and a renderer widget. 1`] = ` +
+
+ +
+
+ +
+
+`; diff --git a/src/viewer/index.ts b/src/viewer/index.ts index 2a835b02..dc8b9faf 100644 --- a/src/viewer/index.ts +++ b/src/viewer/index.ts @@ -1,11 +1,11 @@ import { ViewerView } from "./gui/ViewerView"; import { GuiStore } from "../core/stores/GuiStore"; import { HttpClient } from "../core/HttpClient"; -import { DisposableThreeRenderer } from "../core/rendering/ThreeRenderer"; 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"; export function initialize_viewer( http_client: HttpClient, @@ -20,7 +20,6 @@ export function initialize_viewer( async () => { const { ModelController } = await import("./controllers/model/ModelController"); - const { ModelRenderer } = await import("./rendering/ModelRenderer"); const { ModelView } = await import("./gui/model/ModelView"); const { CharacterClassAssetLoader } = await import( "./loading/CharacterClassAssetLoader" @@ -43,17 +42,35 @@ export function initialize_viewer( 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 { WebgpuRenderer } = await import("../core/rendering/webgpu/WebgpuRenderer"); + const { ModelGfxRenderer } = await import("./rendering/ModelGfxRenderer"); + + renderer = new ModelGfxRenderer(store, new WebgpuRenderer(true, 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(true)); + } else { + const { ModelRenderer } = await import("./rendering/ModelRenderer"); + + renderer = new ModelRenderer(store, create_three_renderer()); + } + return new ModelView( model_controller, new ModelToolBarView(model_tool_bar_controller), new CharacterClassOptionsView(character_class_options_controller), - new ModelRenderer(store, create_three_renderer()), + renderer, ); }, async () => { - const { TextureController } = await import("./controllers/TextureController"); - const { TextureView } = await import("./gui/TextureView"); + const { TextureController } = await import("./controllers/texture/TextureController"); + const { TextureView } = await import("./gui/texture/TextureView"); const { TextureRenderer } = await import("./rendering/TextureRenderer"); const controller = disposer.add(new TextureController()); @@ -62,10 +79,10 @@ export function initialize_viewer( if (gui_store.feature_active("webgpu")) { const { WebgpuRenderer } = await import("../core/rendering/webgpu/WebgpuRenderer"); - renderer = new TextureRenderer(controller, new WebgpuRenderer(http_client)); + renderer = new TextureRenderer(controller, new WebgpuRenderer(false, http_client)); } else { const { WebglRenderer } = await import("../core/rendering/webgl/WebglRenderer"); - renderer = new TextureRenderer(controller, new WebglRenderer()); + renderer = new TextureRenderer(controller, new WebglRenderer(false)); } return new TextureView(controller, renderer); diff --git a/src/viewer/rendering/ModelGfxRenderer.ts b/src/viewer/rendering/ModelGfxRenderer.ts new file mode 100644 index 00000000..74ddb5d0 --- /dev/null +++ b/src/viewer/rendering/ModelGfxRenderer.ts @@ -0,0 +1,56 @@ +import { ModelStore } from "../stores/ModelStore"; +import { Disposer } from "../../core/observable/Disposer"; +import { Renderer } from "../../core/rendering/Renderer"; +import { GfxRenderer } from "../../core/rendering/GfxRenderer"; +import { LogManager } from "../../core/Logger"; +import { ninja_object_to_node } from "../../core/rendering/conversion/ninja_geometry"; + +const logger = LogManager.get("viewer/rendering/ModelRenderer"); + +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)); + } + + 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 = ninja_object_to_node(nj_object); + this.renderer.scene.root_node.add_child(node); + + this.renderer.scene.traverse(node => { + node.mesh?.upload(this.renderer.gfx); + }, undefined); + } + + this.renderer.schedule_render(); + }; +} diff --git a/src/viewer/rendering/ModelRenderer.ts b/src/viewer/rendering/ModelRenderer.ts index 60aefa63..d0414096 100644 --- a/src/viewer/rendering/ModelRenderer.ts +++ b/src/viewer/rendering/ModelRenderer.ts @@ -12,9 +12,9 @@ import { } from "three"; import { Disposable } from "../../core/observable/Disposable"; import { NjMotion } from "../../core/data_formats/parsing/ninja/motion"; -import { xvr_texture_to_texture } from "../../core/rendering/conversion/ninja_textures"; +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_geometry"; +import { ninja_object_to_buffer_geometry } from "../../core/rendering/conversion/ninja_three_geometry"; import { create_animation_clip, PSO_FRAME_RATE, @@ -137,7 +137,7 @@ export class ModelRenderer extends ThreeRenderer implements Disposable { const textures = this.store.current_textures.val.map(tex => { if (tex) { try { - return xvr_texture_to_texture(tex); + return xvr_texture_to_three_texture(tex); } catch (e) { logger.error("Couldn't convert XVR texture.", e); } diff --git a/src/viewer/rendering/ModelWebglRenderer.ts b/src/viewer/rendering/ModelWebglRenderer.ts deleted file mode 100644 index 45d502f1..00000000 --- a/src/viewer/rendering/ModelWebglRenderer.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { WebglRenderer } from "../../core/rendering/webgl/WebglRenderer"; -import { ModelStore } from "../stores/ModelStore"; -import { Disposer } from "../../core/observable/Disposer"; - -export class ModelWebglRenderer extends WebglRenderer { - private readonly disposer = new Disposer(); - - constructor(private readonly store: ModelStore) { - super(); - } - - dispose(): void { - super.dispose(); - this.disposer.dispose(); - } -} diff --git a/src/viewer/rendering/TextureRenderer.ts b/src/viewer/rendering/TextureRenderer.ts index 10e482f3..dabf61b8 100644 --- a/src/viewer/rendering/TextureRenderer.ts +++ b/src/viewer/rendering/TextureRenderer.ts @@ -1,13 +1,14 @@ import { Disposer } from "../../core/observable/Disposer"; import { LogManager } from "../../core/Logger"; -import { TextureController } from "../controllers/TextureController"; +import { TextureController } from "../controllers/texture/TextureController"; import { XvrTexture } from "../../core/data_formats/parsing/ninja/texture"; -import { TranslateTransform } from "../../core/rendering/Transform"; import { VertexFormat } from "../../core/rendering/VertexFormat"; -import { Texture, TextureFormat } from "../../core/rendering/Texture"; import { Mesh } from "../../core/rendering/Mesh"; import { GfxRenderer } from "../../core/rendering/GfxRenderer"; import { Renderer } from "../../core/rendering/Renderer"; +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"; const logger = LogManager.get("viewer/rendering/TextureRenderer"); @@ -21,7 +22,7 @@ export class TextureRenderer implements Renderer { this.disposer.add_all( ctrl.textures.observe(({ value: textures }) => { - renderer.scene.destroy(); + renderer.destroy_scene(); renderer.camera.reset(); this.create_quads(textures); renderer.schedule_render(); @@ -61,10 +62,13 @@ export class TextureRenderer implements Renderer { for (const tex of textures) { try { const quad_mesh = this.create_quad(tex); + quad_mesh.upload(this.renderer.gfx); this.renderer.scene.root_node.add_child( - quad_mesh, - new TranslateTransform(x, y + (total_height - tex.height) / 2, 0), + new SceneNode( + quad_mesh, + Mat4.translation(x, y + (total_height - tex.height) / 2, 0), + ), ); } catch (e) { logger.error("Couldn't create quad for texture.", e); @@ -75,45 +79,18 @@ export class TextureRenderer implements Renderer { } private create_quad(tex: XvrTexture): Mesh { - return this.renderer - .mesh_builder(VertexFormat.PosTex) - .vertex(0, 0, 0, 0, 1) - .vertex(tex.width, 0, 0, 1, 1) - .vertex(tex.width, tex.height, 0, 1, 0) - .vertex(0, tex.height, 0, 0, 0) + return Mesh.builder(VertexFormat.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(this.xvr_texture_to_texture(tex)) + .texture(xvr_texture_to_texture(this.renderer.gfx, tex)) .build(); } - - private xvr_texture_to_texture(tex: XvrTexture): Texture { - let format: TextureFormat; - let data_size: number; - - // Ignore mipmaps. - switch (tex.format[1]) { - case 6: - format = TextureFormat.RGBA_S3TC_DXT1; - data_size = (tex.width * tex.height) / 2; - break; - case 7: - format = TextureFormat.RGBA_S3TC_DXT3; - data_size = tex.width * tex.height; - break; - default: - throw new Error(`Format ${tex.format.join(", ")} not supported.`); - } - - return new Texture( - this.renderer.gfx!, - format, - tex.width, - tex.height, - tex.data.slice(0, data_size), - ); - } } diff --git a/test/src/core/rendering/StubGfxRenderer.ts b/test/src/core/rendering/StubGfxRenderer.ts index 53ec9ce1..e6adf9bd 100644 --- a/test/src/core/rendering/StubGfxRenderer.ts +++ b/test/src/core/rendering/StubGfxRenderer.ts @@ -7,7 +7,7 @@ export class StubGfxRenderer extends GfxRenderer { } constructor() { - super(); + super(false); } protected render(): void {} // eslint-disable-line