Added "additive" blending for the materials that need it.

This commit is contained in:
Daan Vanden Bosch 2020-01-18 20:01:22 +01:00
parent 5f7547dae9
commit 06e1a8e60b
7 changed files with 227 additions and 146 deletions

View File

@ -39,6 +39,8 @@ export type NjcmTriangleStrip = {
has_tex_coords: boolean;
has_normal: boolean;
texture_id?: number;
src_alpha?: number;
dst_alpha?: number;
vertices: NjcmMeshVertex[];
};
@ -89,6 +91,8 @@ type NjcmNullChunk = {
type NjcmBitsChunk = {
type: NjcmChunkType.Bits;
src_alpha: number;
dst_alpha: number;
};
type NjcmCachePolygonListChunk = {
@ -116,6 +120,11 @@ type NjcmTinyChunk = {
type NjcmMaterialChunk = {
type: NjcmChunkType.Material;
src_alpha: number;
dst_alpha: number;
diffuse?: NjcmArgb;
ambient?: NjcmArgb;
specular?: NjcmErgb;
};
type NjcmVertexChunk = {
@ -145,6 +154,26 @@ type NjcmChunkVertex = {
calc_continue: boolean;
};
/**
* Channels are in range [0, 1].
*/
type NjcmArgb = {
a: number;
r: number;
g: number;
b: number;
};
/**
* Channels are not normalized.
*/
type NjcmErgb = {
e: number;
r: number;
g: number;
b: number;
};
export function parse_njcm_model(cursor: Cursor, cached_chunk_offsets: number[]): NjcmModel {
const vlist_offset = cursor.u32(); // Vertex list
const plist_offset = cursor.u32(); // Triangle strip index list
@ -175,16 +204,34 @@ export function parse_njcm_model(cursor: Cursor, cached_chunk_offsets: number[])
cursor.seek_start(plist_offset);
let texture_id: number | undefined = undefined;
let src_alpha: number | undefined = undefined;
let dst_alpha: number | undefined = undefined;
for (const chunk of parse_chunks(cursor, cached_chunk_offsets, false)) {
if (chunk.type === NjcmChunkType.Tiny) {
texture_id = chunk.texture_id;
} else if (chunk.type === NjcmChunkType.Strip) {
for (const strip of chunk.triangle_strips) {
strip.texture_id = texture_id;
}
switch (chunk.type) {
case NjcmChunkType.Bits:
src_alpha = chunk.src_alpha;
dst_alpha = chunk.dst_alpha;
break;
meshes.push(...chunk.triangle_strips);
case NjcmChunkType.Tiny:
texture_id = chunk.texture_id;
break;
case NjcmChunkType.Material:
src_alpha = chunk.src_alpha;
dst_alpha = chunk.dst_alpha;
break;
case NjcmChunkType.Strip:
for (const strip of chunk.triangle_strips) {
strip.texture_id = texture_id;
strip.src_alpha = src_alpha;
strip.dst_alpha = dst_alpha;
}
meshes.push(...chunk.triangle_strips);
break;
}
}
}
@ -222,6 +269,8 @@ function parse_chunks(
chunks.push({
type: NjcmChunkType.Bits,
type_id,
src_alpha: (flags >>> 3) & 0b111,
dst_alpha: flags & 0b111,
});
} else if (type_id === 4) {
const cache_index = flags;
@ -265,9 +314,46 @@ function parse_chunks(
});
} else if (17 <= type_id && type_id <= 31) {
size = 2 + 2 * cursor.u16();
let diffuse: NjcmArgb | undefined;
let ambient: NjcmArgb | undefined;
let specular: NjcmErgb | undefined;
if ((flags & 0b1) !== 0) {
diffuse = {
b: cursor.u8() / 255,
g: cursor.u8() / 255,
r: cursor.u8() / 255,
a: cursor.u8() / 255,
};
}
if ((flags & 0b10) !== 0) {
ambient = {
b: cursor.u8() / 255,
g: cursor.u8() / 255,
r: cursor.u8() / 255,
a: cursor.u8() / 255,
};
}
if ((flags & 0b100) !== 0) {
specular = {
b: cursor.u8(),
g: cursor.u8(),
r: cursor.u8(),
e: cursor.u8(),
};
}
chunks.push({
type: NjcmChunkType.Material,
type_id,
src_alpha: (flags >>> 3) & 0b111,
dst_alpha: flags & 0b111,
diffuse,
ambient,
specular,
});
} else if (32 <= type_id && type_id <= 50) {
size = 2 + 4 * cursor.u16();

View File

@ -1,31 +1,67 @@
import {
Bone,
BufferGeometry,
Float32BufferAttribute,
Uint16BufferAttribute,
Vector3,
Bone,
} from "three";
import { map_get_or_put } from "../../util";
export type BuilderData = {
created_by_geometry_builder: boolean;
/**
* Maps material indices to normalized material indices.
*/
normalized_material_indices: Map<number, number>;
bones: Bone[];
readonly created_by_geometry_builder: boolean;
readonly materials: BuilderMaterial[];
readonly bones: Bone[];
};
export type BuilderVec2 = {
x: number;
y: number;
readonly x: number;
readonly y: number;
};
export type BuilderVec3 = {
x: number;
y: number;
z: number;
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;
@ -33,18 +69,18 @@ type VertexGroup = {
};
export class GeometryBuilder {
private positions: number[] = [];
private normals: number[] = [];
private uvs: number[] = [];
private indices: number[] = [];
private bones: Bone[] = [];
private bone_indices: number[] = [];
private bone_weights: number[] = [];
private groups: VertexGroup[] = [];
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 material_indices = new Set<number>([-1]);
private readonly material_map = new MaterialMap();
get vertex_count(): number {
return this.positions.length / 3;
@ -89,26 +125,29 @@ export class GeometryBuilder {
this.bone_weights.push(weight);
}
add_group(offset: number, size: number, material_index?: number): void {
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 mat_idx = material_index == null ? -1 : material_index;
const material_index = this.material_map.add_material(texture_id, alpha, additive_blending);
if (last_group && last_group.material_index === mat_idx) {
if (last_group && last_group.material_index === material_index) {
last_group.size += size;
} else {
this.groups.push({
offset,
size,
material_index: mat_idx,
material_index,
});
this.material_indices.add(mat_idx);
}
}
build(): BufferGeometry {
const geom = new BufferGeometry();
const data = geom.userData as BuilderData;
data.created_by_geometry_builder = true;
geom.setAttribute("position", new Float32BufferAttribute(this.positions, 3));
geom.setAttribute("normal", new Float32BufferAttribute(this.normals, 3));
@ -116,28 +155,28 @@ export class GeometryBuilder {
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));
data.bones = this.bones;
bones = this.bones;
} else {
data.bones = [];
bones = [];
}
// Normalize material indices.
const normalized_mat_idxs = new Map<number, number>();
let i = 0;
for (const mat_idx of [...this.material_indices].sort((a, b) => a - b)) {
normalized_mat_idxs.set(mat_idx, i++);
}
// Use normalized material indices in Three.js groups.
for (const group of this.groups) {
geom.addGroup(group.offset, group.size, normalized_mat_idxs.get(group.material_index));
geom.addGroup(group.offset, group.size, group.material_index);
}
data.normalized_material_indices = normalized_mat_idxs;
// noinspection UnnecessaryLocalVariableJS
const data: BuilderData = {
created_by_geometry_builder: true,
materials: this.material_map.get_materials(),
bones,
};
geom.userData = data;
geom.computeBoundingSphere();
geom.computeBoundingBox();

View File

@ -1,82 +1,78 @@
import {
AdditiveBlending,
BufferGeometry,
DoubleSide,
Material,
Mesh,
MeshBasicMaterial,
MeshLambertMaterial,
Skeleton,
SkinnedMesh,
Texture,
} from "three";
import { BuilderData } from "./GeometryBuilder";
import { MeshBasicMaterialParameters } from "three/src/materials/MeshBasicMaterial";
const DUMMY_MATERIAL = new MeshLambertMaterial({
color: 0x00ff00,
side: DoubleSide,
});
const DEFAULT_MATERIAL = new MeshLambertMaterial({
color: 0xff00ff,
side: DoubleSide,
});
const DEFAULT_SKINNED_MATERIAL = new MeshLambertMaterial({
skinning: true,
color: 0xff00ff,
side: DoubleSide,
});
export function create_mesh(
geometry: BufferGeometry,
material?: Material | Material[],
default_material: Material = DEFAULT_MATERIAL,
): Mesh {
return create(geometry, material, default_material, Mesh);
}
export function create_skinned_mesh(
geometry: BufferGeometry,
material?: Material | Material[],
default_material: Material = DEFAULT_SKINNED_MATERIAL,
): SkinnedMesh {
return create(geometry, material, default_material, SkinnedMesh);
}
function create<M extends Mesh>(
geometry: BufferGeometry,
material: Material | Material[] | undefined,
textures: (Texture | undefined)[],
default_material: Material,
mesh_constructor: new (geometry: BufferGeometry, material: Material | Material[]) => M,
): M {
const {
created_by_geometry_builder,
normalized_material_indices: mat_idxs,
bones,
} = geometry.userData as BuilderData;
skinning: boolean,
): Mesh {
const { created_by_geometry_builder, materials, bones } = geometry.userData as BuilderData;
let mat: Material | Material[];
if (Array.isArray(material)) {
if (created_by_geometry_builder) {
mat = [DUMMY_MATERIAL];
if (textures.length && created_by_geometry_builder) {
mat = [DUMMY_MATERIAL];
for (const [idx, normalized_idx] of mat_idxs.entries()) {
if (normalized_idx > 0) {
mat[normalized_idx] = material[idx] || default_material;
for (let i = 1; i < materials.length; i++) {
const { texture_id, alpha, additive_blending } = materials[i];
const tex = texture_id == undefined ? undefined : textures[texture_id];
if (tex) {
const mat_params: MeshBasicMaterialParameters = {
skinning,
map: tex,
side: DoubleSide,
};
if (alpha) {
mat_params.transparent = true;
mat_params.alphaTest = 0.01;
}
if (additive_blending) {
mat_params.transparent = true;
mat_params.alphaTest = 0.01;
mat_params.blending = AdditiveBlending;
}
mat.push(new MeshBasicMaterial(mat_params));
} else {
mat.push(
new MeshLambertMaterial({
skinning,
side: DoubleSide,
}),
);
}
} else {
mat = material;
}
} else if (material) {
mat = material;
} else {
mat = default_material;
}
const mesh = new mesh_constructor(geometry, mat);
if (created_by_geometry_builder && bones.length && mesh instanceof SkinnedMesh) {
if (created_by_geometry_builder && bones.length && skinning) {
const mesh = new SkinnedMesh(geometry, mat);
mesh.add(bones[0]);
mesh.bind(new Skeleton(bones));
return mesh;
} else {
return new Mesh(geometry, mat);
}
return mesh;
}

View File

@ -217,6 +217,8 @@ class GeometryCreator {
start_index_count,
this.builder.index_count - start_index_count,
mesh.texture_id,
mesh.use_alpha,
mesh.src_alpha !== 4 || mesh.dst_alpha !== 5,
);
}
}

View File

@ -1,19 +1,11 @@
import { QuestEntityModel } from "../../model/QuestEntityModel";
import {
BufferGeometry,
DoubleSide,
Mesh,
MeshBasicMaterial,
MeshLambertMaterial,
Texture,
} from "three";
import { BufferGeometry, DoubleSide, Mesh, MeshLambertMaterial, Texture } from "three";
import { create_mesh } from "../../../core/rendering/conversion/create_mesh";
import {
entity_type_to_string,
EntityType,
is_npc_type,
} from "../../../core/data_formats/parsing/quest/entities";
import { NpcType } from "../../../core/data_formats/parsing/quest/npc_types";
export enum ColorType {
Normal,
@ -45,27 +37,8 @@ export function create_entity_type_mesh(
side: DoubleSide,
});
const mesh = create_mesh(
geometry,
textures.length
? textures.map(
tex =>
new MeshBasicMaterial({
map: tex,
side: DoubleSide,
// TODO: figure out why these NPC types don't render correctly when
// transparency is turned on.
transparent:
type !== NpcType.PofuillySlime && type !== NpcType.PouillySlime,
alphaTest: 0.01,
}),
)
: default_material,
default_material,
);
const mesh = create_mesh(geometry, textures, default_material, false);
mesh.name = entity_type_to_string(type);
return mesh;
}

View File

@ -263,8 +263,6 @@ function texture_ids(
return {
section_id: section_id + 275,
body: [body_idx, body_idx + 1, body_idx + 2, body + 250],
// Eyes don't look correct because NJCM material chunks (which contain alpha blending
// details) aren't parsed yet. Material.blending should be AdditiveBlending.
head: [body_idx + 3, body_idx + 4],
hair: [],
accessories: [],

View File

@ -3,7 +3,6 @@ import {
AnimationMixer,
Clock,
DoubleSide,
MeshBasicMaterial,
MeshLambertMaterial,
Object3D,
PerspectiveCamera,
@ -14,7 +13,7 @@ import {
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 { create_mesh, create_skinned_mesh } from "../../core/rendering/conversion/create_mesh";
import { create_mesh } from "../../core/rendering/conversion/create_mesh";
import { ninja_object_to_buffer_geometry } from "../../core/rendering/conversion/ninja_geometry";
import {
create_animation_clip,
@ -150,25 +149,13 @@ export class ModelRenderer extends Renderer implements Disposable {
const geometry = ninja_object_to_buffer_geometry(nj_object);
const has_skeleton = geometry.getAttribute("skinIndex") != undefined;
const materials = textures.map(tex =>
tex
? new MeshBasicMaterial({
skinning: has_skeleton,
map: tex,
side: DoubleSide,
alphaTest: 0.1,
transparent: true,
})
: new MeshLambertMaterial({
skinning: has_skeleton,
side: DoubleSide,
}),
this.mesh = create_mesh(
geometry,
textures,
has_skeleton ? DEFAULT_SKINNED_MATERIAL : DEFAULT_MATERIAL,
has_skeleton,
);
this.mesh = has_skeleton
? create_skinned_mesh(geometry, materials, DEFAULT_SKINNED_MATERIAL)
: create_mesh(geometry, materials, DEFAULT_MATERIAL);
// Make sure we rotate around the center of the model instead of its origin.
const bb = geometry.boundingBox;
const height = bb.max.y - bb.min.y;