From a181847647c0984db375f7adcae06a92dd0ada13 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Fri, 19 Jul 2019 19:34:48 +0200 Subject: [PATCH] Ninja render geometry is now parsed correctly. --- src/data_formats/parsing/area_geometry.ts | 26 +- src/data_formats/parsing/ninja/index.ts | 13 + src/rendering/conversion/GeometryBuilder.ts | 115 +++++++++ src/rendering/conversion/areas.ts | 31 +-- src/rendering/conversion/ninja_geometry.ts | 261 ++++++++++++-------- src/rendering/conversion/xj_models.ts | 100 -------- 6 files changed, 306 insertions(+), 240 deletions(-) create mode 100644 src/rendering/conversion/GeometryBuilder.ts delete mode 100644 src/rendering/conversion/xj_models.ts diff --git a/src/data_formats/parsing/area_geometry.ts b/src/data_formats/parsing/area_geometry.ts index 3a174563..4e2d26cb 100644 --- a/src/data_formats/parsing/area_geometry.ts +++ b/src/data_formats/parsing/area_geometry.ts @@ -1,7 +1,7 @@ import { Cursor } from "../cursor/Cursor"; import { Vec3 } from "../vector"; -import { ANGLE_TO_RAD } from "./ninja"; -import { parse_xj_model, XjModel } from "./ninja/xj"; +import { ANGLE_TO_RAD, NjObject, parse_xj_object } from "./ninja"; +import { XjModel } from "./ninja/xj"; import { parse_rel } from "./rel"; export type RenderObject = { @@ -12,7 +12,7 @@ export type RenderSection = { id: number; position: Vec3; rotation: Vec3; - models: XjModel[]; + objects: NjObject[]; }; export type Vertex = { @@ -53,7 +53,7 @@ export function parse_area_geometry(cursor: Cursor): RenderObject { // const animated_geometry_offset_count = cursor.u32(); // Ignore animated_geometry_offset_count and the last 4 bytes. - const models = parse_geometry_table( + const objects = parse_geometry_table( cursor, simple_geometry_offset_table_offset, simple_geometry_offset_count @@ -63,19 +63,20 @@ export function parse_area_geometry(cursor: Cursor): RenderObject { id: section_id, position: section_position, rotation: section_rotation, - models, + objects, }); } return { sections }; } +// TODO: don't reparse the same objects multiple times. Create DAG instead of tree. function parse_geometry_table( cursor: Cursor, table_offset: number, table_entry_count: number -): XjModel[] { - const models: XjModel[] = []; +): NjObject[] { + const objects: NjObject[] = []; for (let i = 0; i < table_entry_count; i++) { cursor.seek_start(table_offset + 16 * i); @@ -88,14 +89,9 @@ function parse_geometry_table( offset = cursor.seek_start(offset).u32(); } - cursor.seek_start(offset + 4); - const geometry_offset = cursor.u32(); - - if (geometry_offset > 0) { - cursor.seek_start(geometry_offset); - models.push(parse_xj_model(cursor)); - } + cursor.seek_start(offset); + objects.push(...parse_xj_object(cursor)); } - return models; + return objects; } diff --git a/src/data_formats/parsing/ninja/index.ts b/src/data_formats/parsing/ninja/index.ts index 415179d6..144b608e 100644 --- a/src/data_formats/parsing/ninja/index.ts +++ b/src/data_formats/parsing/ninja/index.ts @@ -101,14 +101,27 @@ export type NjEvaluationFlags = { shape_skip: boolean; }; +/** + * Parses an NJCM file. + */ export function parse_nj(cursor: Cursor): NjObject[] { return parse_ninja(cursor, parse_njcm_model, []); } +/** + * Parses an NJCM file. + */ export function parse_xj(cursor: Cursor): NjObject[] { return parse_ninja(cursor, parse_xj_model, undefined); } +/** + * Parses a ninja object. + */ +export function parse_xj_object(cursor: Cursor): NjObject[] { + return parse_sibling_objects(cursor, parse_xj_model, undefined); +} + function parse_ninja( cursor: Cursor, parse_model: (cursor: Cursor, context: any) => M, diff --git a/src/rendering/conversion/GeometryBuilder.ts b/src/rendering/conversion/GeometryBuilder.ts new file mode 100644 index 00000000..15f8f034 --- /dev/null +++ b/src/rendering/conversion/GeometryBuilder.ts @@ -0,0 +1,115 @@ +import { BufferGeometry, Float32BufferAttribute, Uint16BufferAttribute, Vector3 } from "three"; + +export type BuilderVec2 = { + x: number; + y: number; +}; + +export type BuilderVec3 = { + x: number; + y: number; + z: number; +}; + +type VertexGroup = { + offset: number; + size: number; + material_index: number; +}; + +export class GeometryBuilder { + private positions: number[] = []; + private normals: number[] = []; + private uvs: number[] = []; + private indices: number[] = []; + private bone_indices: number[] = []; + private bone_weights: number[] = []; + private groups: VertexGroup[] = []; + private _max_material_index?: number; + + get vertex_count(): number { + return this.positions.length / 3; + } + + get index_count(): number { + return this.indices.length; + } + + get max_material_index(): number | undefined { + return this._max_material_index; + } + + 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(index: number, weight: number): void { + this.bone_indices.push(index); + this.bone_weights.push(weight); + } + + add_group(offset: number, size: number, material_id?: number): void { + const last_group = this.groups[this.groups.length - 1]; + const mat_idx = material_id == null ? 0 : material_id + 1; + + if (last_group && last_group.material_index === mat_idx) { + last_group.size += size; + } else { + this.groups.push({ + offset, + size, + material_index: mat_idx, + }); + + this._max_material_index = this._max_material_index + ? Math.max(this._max_material_index, mat_idx) + : mat_idx; + } + } + + build(): BufferGeometry { + const geom = new BufferGeometry(); + + geom.addAttribute("position", new Float32BufferAttribute(this.positions, 3)); + geom.addAttribute("normal", new Float32BufferAttribute(this.normals, 3)); + geom.addAttribute("uv", new Float32BufferAttribute(this.uvs, 2)); + + geom.setIndex(new Uint16BufferAttribute(this.indices, 1)); + + for (const group of this.groups) { + geom.addGroup(group.offset, group.size, group.material_index); + } + + if (this.bone_indices.length) { + geom.addAttribute("skinIndex", new Uint16BufferAttribute(this.bone_indices, 4)); + geom.addAttribute("skinWeight", new Float32BufferAttribute(this.bone_weights, 4)); + } + + geom.computeBoundingSphere(); + geom.computeBoundingBox(); + + return geom; + } +} diff --git a/src/rendering/conversion/areas.ts b/src/rendering/conversion/areas.ts index 9ed34cda..6731b5e3 100644 --- a/src/rendering/conversion/areas.ts +++ b/src/rendering/conversion/areas.ts @@ -1,22 +1,19 @@ import { - BufferGeometry, DoubleSide, Face3, - Float32BufferAttribute, Geometry, Group, - Matrix4, Mesh, MeshBasicMaterial, MeshLambertMaterial, Object3D, - Uint16BufferAttribute, Vector3, } from "three"; import { CollisionObject } from "../../data_formats/parsing/area_collision_geometry"; import { RenderObject } from "../../data_formats/parsing/area_geometry"; import { Section } from "../../domain"; -import { xj_model_to_geometry } from "./xj_models"; +import { ninja_object_to_mesh, ninja_object_to_geometry_builder } from "./ninja_geometry"; +import { GeometryBuilder } from "./GeometryBuilder"; const materials = [ // Wall @@ -120,21 +117,17 @@ export function area_geometry_to_sections_and_object_3d( const group = new Group(); for (const section of object.sections) { - const positions: number[] = []; - const normals: number[] = []; - const indices: number[] = []; + const sec = new Section(section.id, section.position, section.rotation.y); + sections.push(sec); - for (const model of section.models) { - xj_model_to_geometry(model, new Matrix4(), positions, normals, [], indices, []); + const builder = new GeometryBuilder(); + + for (const object of section.objects) { + ninja_object_to_geometry_builder(object, builder); } - const geometry = new BufferGeometry(); - geometry.addAttribute("position", new Float32BufferAttribute(positions, 3)); - geometry.addAttribute("normal", new Float32BufferAttribute(normals, 3)); - geometry.setIndex(new Uint16BufferAttribute(indices, 1)); - const mesh = new Mesh( - geometry, + builder.build(), new MeshLambertMaterial({ color: 0x44aaff, transparent: true, @@ -142,13 +135,9 @@ export function area_geometry_to_sections_and_object_3d( side: DoubleSide, }) ); - mesh.position.set(section.position.x, section.position.y, section.position.z); - mesh.rotation.set(section.rotation.x, section.rotation.y, section.rotation.z); - group.add(mesh); - const sec = new Section(section.id, section.position, section.rotation.y); (mesh.userData as AreaUserData).section = sec; - sections.push(sec); + group.add(mesh); } return [sections, group]; diff --git a/src/rendering/conversion/ninja_geometry.ts b/src/rendering/conversion/ninja_geometry.ts index 9acddd4a..2fdc1136 100644 --- a/src/rendering/conversion/ninja_geometry.ts +++ b/src/rendering/conversion/ninja_geometry.ts @@ -3,23 +3,23 @@ import { BufferGeometry, DoubleSide, Euler, - Float32BufferAttribute, Material, Matrix3, Matrix4, + Mesh, + MeshBasicMaterial, MeshLambertMaterial, Quaternion, Skeleton, SkinnedMesh, - Uint16BufferAttribute, + Vector2, Vector3, - MeshBasicMaterial, - Mesh, } from "three"; import { vec3_to_threejs } from "."; import { is_njcm_model, NjModel, NjObject } from "../../data_formats/parsing/ninja"; import { NjcmModel } from "../../data_formats/parsing/ninja/njcm"; -import { xj_model_to_geometry } from "./xj_models"; +import { XjModel } from "../../data_formats/parsing/ninja/xj"; +import { GeometryBuilder } from "./GeometryBuilder"; const DUMMY_MATERIAL = new MeshBasicMaterial({ color: 0x00ff00, @@ -35,34 +35,42 @@ const DEFAULT_SKINNED_MATERIAL = new MeshLambertMaterial({ side: DoubleSide, }); 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_buffer_geometry(object: NjObject): BufferGeometry { - return new Object3DCreator([]).create_buffer_geometry(object); +export function ninja_object_to_geometry_builder( + object: NjObject, + builder: GeometryBuilder +) { + new ModelCreator(builder).to_geometry_builder(object); } -export function ninja_object_to_mesh(object: NjObject, materials: Material[] = []): Mesh { - return new Object3DCreator(materials).create_mesh(object); +export function ninja_object_to_buffer_geometry(object: NjObject): BufferGeometry { + return new ModelCreator(new GeometryBuilder()).create_buffer_geometry(object); +} + +export function ninja_object_to_mesh( + object: NjObject, + materials: Material[] = [], + default_material: Material = DEFAULT_MATERIAL +): Mesh { + return new ModelCreator(new GeometryBuilder()).create_mesh(object, materials, default_material); } export function ninja_object_to_skinned_mesh( object: NjObject, - materials: Material[] = [] + materials: Material[] = [], + default_material: Material = DEFAULT_SKINNED_MATERIAL ): SkinnedMesh { - return new Object3DCreator(materials).create_skinned_mesh(object); + return new ModelCreator(new GeometryBuilder()).create_skinned_mesh( + object, + materials, + default_material + ); } -export type VertexGroup = { - /** - * Start index. - */ - start: number; - count: number; - material_index: number; -}; - type Vertex = { bone_id: number; position: Vector3; @@ -94,70 +102,57 @@ class VerticesHolder { } } -class Object3DCreator { - private materials: Material[]; +class ModelCreator { private vertices = new VerticesHolder(); private bone_id: number = 0; private bones: Bone[] = []; + private builder: GeometryBuilder; - private positions: number[] = []; - private normals: number[] = []; - private uvs: number[] = []; - private indices: number[] = []; - private bone_indices: number[] = []; - private bone_weights: number[] = []; - private groups: VertexGroup[] = []; + constructor(builder: GeometryBuilder) { + this.builder = builder; + } - constructor(materials: Material[]) { - this.materials = [DUMMY_MATERIAL, ...materials]; + to_geometry_builder(object: NjObject) { + this.object_to_geometry(object, undefined, new Matrix4()); } create_buffer_geometry(object: NjObject): BufferGeometry { - this.object_to_geometry(object, undefined, new Matrix4()); - - const geom = new BufferGeometry(); - - geom.addAttribute("position", new Float32BufferAttribute(this.positions, 3)); - geom.addAttribute("normal", new Float32BufferAttribute(this.normals, 3)); - geom.addAttribute("uv", new Float32BufferAttribute(this.uvs, 2)); - - geom.setIndex(new Uint16BufferAttribute(this.indices, 1)); - - for (const group of this.groups) { - geom.addGroup(group.start, group.count, group.material_index); - } - - geom.computeBoundingSphere(); - geom.computeBoundingBox(); - - return geom; + this.to_geometry_builder(object); + return this.builder.build(); } - create_mesh(object: NjObject): Mesh { + create_mesh( + object: NjObject, + materials: Material[], + default_material: Material + ): Mesh { const geom = this.create_buffer_geometry(object); - const max_mat_idx = this.groups.reduce((max, g) => Math.max(max, g.material_index), 0); + materials = [DUMMY_MATERIAL, ...materials]; + const max_mat_idx = this.builder.max_material_index || 0; - for (let i = this.materials.length - 1; i < max_mat_idx; ++i) { - this.materials.push(DEFAULT_MATERIAL); + for (let i = materials.length - 1; i < max_mat_idx; ++i) { + materials.push(default_material); } - return new Mesh(geom, this.materials); + return new Mesh(geom, materials); } - create_skinned_mesh(object: NjObject): SkinnedMesh { + create_skinned_mesh( + object: NjObject, + materials: Material[], + default_material: Material + ): SkinnedMesh { const geom = this.create_buffer_geometry(object); - geom.addAttribute("skinIndex", new Uint16BufferAttribute(this.bone_indices, 4)); - geom.addAttribute("skinWeight", new Float32BufferAttribute(this.bone_weights, 4)); + materials = [DUMMY_MATERIAL, ...materials]; + const max_mat_idx = this.builder.max_material_index || 0; - const max_mat_idx = this.groups.reduce((max, g) => Math.max(max, g.material_index), 0); - - for (let i = this.materials.length - 1; i < max_mat_idx; ++i) { - this.materials.push(DEFAULT_SKINNED_MATERIAL); + for (let i = materials.length - 1; i < max_mat_idx; ++i) { + materials.push(default_material); } - const mesh = new SkinnedMesh(geom, this.materials); + const mesh = new SkinnedMesh(geom, materials); mesh.add(this.bones[0]); mesh.bind(new Skeleton(this.bones)); @@ -230,15 +225,7 @@ class Object3DCreator { if (is_njcm_model(model)) { this.njcm_model_to_geometry(model, matrix); } else { - xj_model_to_geometry( - model, - matrix, - this.positions, - this.normals, - this.uvs, - this.indices, - this.groups - ); + this.xj_model_to_geometry(model, matrix); } } @@ -247,7 +234,7 @@ class Object3DCreator { const new_vertices = model.vertices.map(vertex => { const position = vec3_to_threejs(vertex.position); - const normal = vertex.normal ? vec3_to_threejs(vertex.normal) : DEFAULT_NORMAL; + const normal = vertex.normal ? vec3_to_threejs(vertex.normal) : new Vector3(0, 1, 0); position.applyMatrix4(matrix); normal.applyMatrix3(normal_matrix); @@ -265,7 +252,7 @@ class Object3DCreator { this.vertices.put(new_vertices); for (const mesh of model.meshes) { - const start_index_count = this.indices.length; + const start_index_count = this.builder.index_count; for (let i = 0; i < mesh.vertices.length; ++i) { const mesh_vertex = mesh.vertices[i]; @@ -274,55 +261,121 @@ class Object3DCreator { if (vertices.length) { const vertex = vertices[0]; const normal = vertex.normal || mesh_vertex.normal || DEFAULT_NORMAL; - const index = this.positions.length / 3; + const index = this.builder.vertex_count; - this.positions.push(vertex.position.x, vertex.position.y, vertex.position.z); - this.normals.push(normal.x, normal.y, normal.z); - - if (mesh.has_tex_coords) { - this.uvs.push(mesh_vertex.tex_coords!.x, mesh_vertex.tex_coords!.y); - } else { - this.uvs.push(0, 0); - } + 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.indices.push(index - 2); - this.indices.push(index - 1); - this.indices.push(index); + this.builder.add_index(index - 2); + this.builder.add_index(index - 1); + this.builder.add_index(index); } else { - this.indices.push(index - 2); - this.indices.push(index); - this.indices.push(index - 1); + this.builder.add_index(index - 2); + this.builder.add_index(index); + this.builder.add_index(index - 1); } } - const bone_indices = [0, 0, 0, 0]; - const bone_weights = [0, 0, 0, 0]; + const bones = [[0, 0], [0, 0], [0, 0], [0, 0]]; for (let j = vertices.length - 1; j >= 0; j--) { const vertex = vertices[j]; - bone_indices[vertex.bone_weight_status] = vertex.bone_id; - bone_weights[vertex.bone_weight_status] = vertex.bone_weight; + bones[vertex.bone_weight_status] = [vertex.bone_id, vertex.bone_weight]; } - this.bone_indices.push(...bone_indices); - this.bone_weights.push(...bone_weights); + for (const [bone_index, bone_weight] of bones) { + this.builder.add_bone(bone_index, bone_weight); + } } } - const last_group = this.groups[this.groups.length - 1]; - const mat_idx = mesh.texture_id == null ? 0 : mesh.texture_id + 1; + this.builder.add_group( + start_index_count, + this.builder.index_count - start_index_count, + mesh.texture_id + ); + } + } - if (last_group && last_group.material_index === mat_idx) { - last_group.count += this.indices.length - start_index_count; - } else { - this.groups.push({ - start: start_index_count, - count: this.indices.length - start_index_count, - material_index: mat_idx, - }); + 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 (let { 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; + + 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 != null) { + current_mat_idx = mesh.material_properties.texture_id; + } + + this.builder.add_group( + start_index_count, + this.builder.index_count - start_index_count, + current_mat_idx + ); } } } diff --git a/src/rendering/conversion/xj_models.ts b/src/rendering/conversion/xj_models.ts deleted file mode 100644 index 28b88302..00000000 --- a/src/rendering/conversion/xj_models.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Matrix3, Matrix4, Vector3 } from "three"; -import { vec3_to_threejs } from "."; -import { XjModel } from "../../data_formats/parsing/ninja/xj"; -import { Vec2 } from "../../data_formats/vector"; -import { VertexGroup } from "./ninja_geometry"; - -const DEFAULT_NORMAL = new Vector3(0, 1, 0); -const DEFAULT_UV = new Vec2(0, 0); - -export function xj_model_to_geometry( - model: XjModel, - matrix: Matrix4, - positions: number[], - normals: number[], - uvs: number[], - indices: number[], - groups: VertexGroup[] -): void { - const index_offset = positions.length / 3; - const normal_matrix = new Matrix3().getNormalMatrix(matrix); - - for (let { position, normal, uv } of model.vertices) { - const p = vec3_to_threejs(position).applyMatrix4(matrix); - positions.push(p.x, p.y, p.z); - - const local_n = normal ? vec3_to_threejs(normal) : DEFAULT_NORMAL; - const n = local_n.applyMatrix3(normal_matrix); - normals.push(n.x, n.y, n.z); - - const tuv = uv || DEFAULT_UV; - uvs.push(tuv.x, tuv.y); - } - - let current_mat_idx = 0; - - for (const mesh of model.meshes) { - const start_index_count = indices.length; - let clockwise = true; - - 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 = new Vector3(positions[3 * a], positions[3 * a + 1], positions[3 * a + 2]); - const pb = new Vector3(positions[3 * b], positions[3 * b + 1], positions[3 * b + 2]); - const pc = new Vector3(positions[3 * c], positions[3 * c + 1], positions[3 * c + 2]); - const na = new Vector3(normals[3 * a], normals[3 * a + 1], normals[3 * a + 2]); - const nb = new Vector3(normals[3 * a], normals[3 * a + 1], normals[3 * a + 2]); - const nc = new Vector3(normals[3 * a], normals[3 * a + 1], normals[3 * a + 2]); - - // 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) { - indices.push(b); - indices.push(a); - indices.push(c); - } else { - indices.push(a); - indices.push(b); - indices.push(c); - } - - clockwise = !clockwise; - } - - const last_group = groups[groups.length - 1]; - - if (mesh.material_properties.texture_id != null) { - current_mat_idx = mesh.material_properties.texture_id + 1; - } - - if (last_group && last_group.material_index === current_mat_idx) { - last_group.count += indices.length - start_index_count; - } else { - groups.push({ - start: start_index_count, - count: indices.length - start_index_count, - material_index: current_mat_idx, - }); - } - } -}