Started working on pure WebGL model viewer.

This commit is contained in:
Daan Vanden Bosch 2020-01-26 23:13:09 +01:00
parent a19a3a4837
commit 3230268962
40 changed files with 1585 additions and 612 deletions

View File

@ -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) {}
}

View File

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

View File

@ -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);
}
});

View File

@ -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,
);
}

View File

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

View File

@ -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();

View File

@ -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);
}
}

View File

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

View File

@ -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<T>(f: (node: Node, data: T) => T, data: T): void {
traverse<T>(f: (node: SceneNode, data: T) => T, data: T): void {
this.traverse_node(this.root_node, f, data);
}
private traverse_node<T>(node: Node, f: (node: Node, data: T) => T, data: T): void {
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) {
@ -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 {

View File

@ -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 {

View File

@ -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,
);
}
}

View File

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

View File

@ -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<number, number>();
/**
* 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;
}
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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,
);

View File

@ -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,
);
}
}
}

View File

@ -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()
);
}

View File

@ -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<WebglMesh, WebGLTexture> {
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) {

View File

@ -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);
}
}

View File

@ -1,9 +0,0 @@
#version 300 es
precision mediump float;
out vec4 frag_color;
void main() {
frag_color = vec4(0, 1, 1, 1);
}

View File

@ -1,11 +0,0 @@
#version 300 es
precision mediump float;
uniform mat4 transform;
in vec4 pos;
void main() {
gl_Position = transform * pos;
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

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

View File

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

View File

@ -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(

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

View File

@ -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");

View File

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

View File

@ -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();

View File

@ -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 => {

View File

@ -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" });

View File

@ -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`] = `
<div
class="viewer_TextureView"
>
<div
class="core_ToolBar"
style="height: 33px;"
>
<button
class="core_Button core_FileButton"
>
<span
class="core_Button_inner"
>
<span
class="core_Button_left"
>
<span>
<span
class="fas fa-file"
/>
</span>
</span>
<span
class="core_Button_center"
>
Open file...
</span>
</span>
</button>
</div>
<div
class="core_RendererWidget"
>
<canvas
height="600"
width="800"
/>
</div>
</div>
`;

View File

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

View File

@ -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();
};
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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),
);
}
}

View File

@ -7,7 +7,7 @@ export class StubGfxRenderer extends GfxRenderer {
}
constructor() {
super();
super(false);
}
protected render(): void {} // eslint-disable-line