mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-13 04:36:12 +01:00
YZ correction across collision and glb
This commit is contained in:
7 files changed
+579
-406
No files matched your search
@@ -137,6 +137,13 @@ interface RbAabbox {
|
|||||||
maximum: number[];
|
maximum: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ComponentName = Component["comp.typename"];
|
||||||
|
|
||||||
|
export type ComponentOf<Name extends ComponentName> = Extract<
|
||||||
|
Component,
|
||||||
|
{ "comp.typename": Name }
|
||||||
|
>;
|
||||||
|
|
||||||
export type Component =
|
export type Component =
|
||||||
| {
|
| {
|
||||||
"comp.typename": "TransformComponent";
|
"comp.typename": "TransformComponent";
|
||||||
|
|||||||
@@ -16,13 +16,16 @@ interface PolygonGroupRaw {
|
|||||||
vertices: ArrayBuffer;
|
vertices: ArrayBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
type BlitzkitVertex = { attribute: VertexAttribute; value: number[] }[];
|
export type Vertex = { attribute: VertexAttribute; value: number[] }[];
|
||||||
|
|
||||||
interface BlitzkitPolygonGroup {
|
export interface PolygonGroup {
|
||||||
vertices: BlitzkitVertex[];
|
vertices: Vertex[];
|
||||||
indices: number[];
|
indices: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** polygon group id -> polygon group */
|
||||||
|
export type ScgPolygonGroups = Map<bigint, PolygonGroup>;
|
||||||
|
|
||||||
const vertexAttributesArray = Object.values(VertexAttribute).filter(
|
const vertexAttributesArray = Object.values(VertexAttribute).filter(
|
||||||
(value) => typeof value === "number",
|
(value) => typeof value === "number",
|
||||||
);
|
);
|
||||||
@@ -63,7 +66,7 @@ export class ScgReadStream extends ScpgReadStream {
|
|||||||
scg() {
|
scg() {
|
||||||
const header = this.header();
|
const header = this.header();
|
||||||
const polygonGroupsRaw: PolygonGroupRaw[] = [];
|
const polygonGroupsRaw: PolygonGroupRaw[] = [];
|
||||||
const polygonGroups = new Map<bigint, BlitzkitPolygonGroup>();
|
const polygonGroups = new Map<bigint, PolygonGroup>();
|
||||||
|
|
||||||
for (let index = 0; index < header.nodeCount; index++) {
|
for (let index = 0; index < header.nodeCount; index++) {
|
||||||
const polygonGroupRaw = this.ka() as PolygonGroupRaw;
|
const polygonGroupRaw = this.ka() as PolygonGroupRaw;
|
||||||
@@ -83,7 +86,7 @@ export class ScgReadStream extends ScpgReadStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const verticesStream = new ScpgReadStream(polygonGroupRaw.vertices);
|
const verticesStream = new ScpgReadStream(polygonGroupRaw.vertices);
|
||||||
const vertices: BlitzkitVertex[] = [];
|
const vertices: Vertex[] = [];
|
||||||
const { format, stride } = ScgReadStream.parseVertexFormat(
|
const { format, stride } = ScgReadStream.parseVertexFormat(
|
||||||
polygonGroupRaw.vertexFormat,
|
polygonGroupRaw.vertexFormat,
|
||||||
);
|
);
|
||||||
@@ -110,7 +113,7 @@ export class ScgReadStream extends ScpgReadStream {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return polygonGroups;
|
return polygonGroups satisfies ScgPolygonGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
static parseVertexFormat(formatInt: number) {
|
static parseVertexFormat(formatInt: number) {
|
||||||
|
|||||||
@@ -0,0 +1,503 @@
|
|||||||
|
import {
|
||||||
|
parseHierarchies,
|
||||||
|
VertexAttribute,
|
||||||
|
vertexAttributeVectorSizes,
|
||||||
|
type Component,
|
||||||
|
type ComponentName,
|
||||||
|
type ComponentOf,
|
||||||
|
type Hierarchy,
|
||||||
|
type PolygonGroup,
|
||||||
|
type SC2,
|
||||||
|
type Scg,
|
||||||
|
} from "@blitzkit/core";
|
||||||
|
import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
|
||||||
|
import {
|
||||||
|
Accessor,
|
||||||
|
Document,
|
||||||
|
Material,
|
||||||
|
Node,
|
||||||
|
type Buffer,
|
||||||
|
type Skin,
|
||||||
|
} from "@gltf-transform/core";
|
||||||
|
import { sortBy, times } from "lodash-es";
|
||||||
|
import type { Motion } from "./skinAnimations/motion";
|
||||||
|
import {
|
||||||
|
bindSocket,
|
||||||
|
createMotionClip,
|
||||||
|
createSkeleton,
|
||||||
|
loadMotion,
|
||||||
|
skinPrimitive,
|
||||||
|
type Skeleton,
|
||||||
|
} from "./skinAnimations/skeleton";
|
||||||
|
import type { Garage } from "./switcherStates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared SC2 -> glTF component handling. Every exporter (visual model, armor
|
||||||
|
* collision, ...) goes through {@link Sc2ComponentHandler} so that coordinate
|
||||||
|
* conversion and component semantics can never drift between them.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const vertexAttributeGLTFName: Partial<Record<VertexAttribute, string>> =
|
||||||
|
{
|
||||||
|
[VertexAttribute.VERTEX]: "POSITION",
|
||||||
|
[VertexAttribute.NORMAL]: "NORMAL",
|
||||||
|
[VertexAttribute.TEXCOORD0]: "TEXCOORD_0",
|
||||||
|
[VertexAttribute.TEXCOORD1]: "TEXCOORD_1",
|
||||||
|
[VertexAttribute.TEXCOORD2]: "TEXCOORD_2",
|
||||||
|
[VertexAttribute.TEXCOORD3]: "TEXCOORD_3",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const vertexAttributeGltfVectorSizes = {
|
||||||
|
...vertexAttributeVectorSizes,
|
||||||
|
[VertexAttribute.TANGENT]: 4,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type RenderComponent = ComponentOf<"RenderComponent">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SC2 is Z-up (and right handed); glTF is Y-up. (x, y, z) -> (x, z, -y).
|
||||||
|
*/
|
||||||
|
export function sc2ToGltfVector<T extends [number, number, number]>(
|
||||||
|
vector: T,
|
||||||
|
): [number, number, number] {
|
||||||
|
return [vector[0], vector[2], -vector[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Same conversion for a quaternion's (x, y, z, w). */
|
||||||
|
export function sc2ToGltfQuaternion(
|
||||||
|
q: [number, number, number, number],
|
||||||
|
): [number, number, number, number] {
|
||||||
|
return [q[0], q[2], -q[1], q[3]];
|
||||||
|
}
|
||||||
|
|
||||||
|
const SPATIAL_ATTRIBUTES = new Set<VertexAttribute>([
|
||||||
|
VertexAttribute.VERTEX,
|
||||||
|
VertexAttribute.NORMAL,
|
||||||
|
VertexAttribute.TANGENT,
|
||||||
|
VertexAttribute.BINORMAL,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Groups a polygon group's interleaved vertices by attribute. */
|
||||||
|
export function collectVertexAttributes(polygonGroup: PolygonGroup) {
|
||||||
|
const attributes = new Map<VertexAttribute, number[][]>();
|
||||||
|
|
||||||
|
polygonGroup.vertices.forEach((vertex) => {
|
||||||
|
vertex.forEach(({ attribute, value }) => {
|
||||||
|
if (!attributes.has(attribute)) attributes.set(attribute, []);
|
||||||
|
|
||||||
|
attributes.get(attribute)!.push(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Converts attributes to glTF accessors, correcting the SC2 -> glTF axes. */
|
||||||
|
export function createAttributeAccessors(
|
||||||
|
document: Document,
|
||||||
|
buffer: Buffer,
|
||||||
|
attributes: Map<VertexAttribute, number[][]>,
|
||||||
|
) {
|
||||||
|
const accessors = new Map<string, Accessor>();
|
||||||
|
|
||||||
|
attributes.forEach((value, attribute) => {
|
||||||
|
const name = vertexAttributeGLTFName[attribute];
|
||||||
|
|
||||||
|
if (name === undefined || accessors.has(name)) return;
|
||||||
|
|
||||||
|
const vertexSize = vertexAttributeGltfVectorSizes[attribute];
|
||||||
|
const array = new Float32Array(value.flat());
|
||||||
|
|
||||||
|
if (SPATIAL_ATTRIBUTES.has(attribute)) {
|
||||||
|
for (let i = 0; i < array.length; i += 3) {
|
||||||
|
const y = array[i + 1];
|
||||||
|
const z = array[i + 2];
|
||||||
|
|
||||||
|
array[i + 1] = z;
|
||||||
|
array[i + 2] = -y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accessors.set(
|
||||||
|
name,
|
||||||
|
document
|
||||||
|
.createAccessor(name)
|
||||||
|
.setType(vertexSize === 1 ? "SCALAR" : `VEC${vertexSize}`)
|
||||||
|
.setArray(array)
|
||||||
|
.setBuffer(buffer),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return accessors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Motion files are needed before the hierarchy is walked because they are
|
||||||
|
* loaded asynchronously.
|
||||||
|
*/
|
||||||
|
export async function loadHierarchyMotions(
|
||||||
|
vfs: AbstractVFS,
|
||||||
|
path: string,
|
||||||
|
sc2: SC2,
|
||||||
|
) {
|
||||||
|
const motions = new Map<Hierarchy, Motion>();
|
||||||
|
const loading: Promise<void>[] = [];
|
||||||
|
|
||||||
|
parseHierarchies(sc2["#hierarchy"], null, (hierarchy, components) => {
|
||||||
|
for (const component of components) {
|
||||||
|
if (component["comp.typename"] !== "MotionComponent") continue;
|
||||||
|
|
||||||
|
loading.push(
|
||||||
|
loadMotion(vfs, path, component).then((motion) => {
|
||||||
|
motions.set(hierarchy, motion);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(loading);
|
||||||
|
|
||||||
|
return motions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Sc2RenderMode =
|
||||||
|
| {
|
||||||
|
kind: "visual";
|
||||||
|
/** material key -> material (or an unresolved/shadow entry) */
|
||||||
|
materials: Map<bigint, Material | bigint | undefined>;
|
||||||
|
/** parts keep lod-less (-1) batches */
|
||||||
|
isPart: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
/** armor: untextured, one node per hard joint */
|
||||||
|
kind: "collision";
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Sc2ComponentHandlerOptions {
|
||||||
|
document: Document;
|
||||||
|
buffer: Buffer;
|
||||||
|
scg: Scg;
|
||||||
|
render: Sc2RenderMode;
|
||||||
|
/** enables skeletons, motion clips and joint bindings */
|
||||||
|
skeleton?: {
|
||||||
|
motions: Map<Hierarchy, Motion>;
|
||||||
|
garage?: Garage;
|
||||||
|
};
|
||||||
|
/** throw on component types this handler does not know about */
|
||||||
|
strict?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HierarchyContext {
|
||||||
|
hierarchy: Hierarchy;
|
||||||
|
components: Component[];
|
||||||
|
node: Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Order in which components must be applied; the rest keep file order. */
|
||||||
|
const COMPONENT_PRIORITY: Partial<Record<ComponentName, number>> = {
|
||||||
|
TransformComponent: 0,
|
||||||
|
StateSwitcherComponent: 0,
|
||||||
|
SkeletonComponent: 1,
|
||||||
|
SkeletonJointBindingComponent: 2,
|
||||||
|
RenderComponent: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
function componentPriority(component: Component) {
|
||||||
|
return COMPONENT_PRIORITY[component["comp.typename"]] ?? 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComponentHandler<Name extends ComponentName> = (
|
||||||
|
component: ComponentOf<Name>,
|
||||||
|
context: HierarchyContext,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
type AnyComponentHandler = (
|
||||||
|
component: Component,
|
||||||
|
context: HierarchyContext,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A `null` handler marks a component type that carries nothing the exporters
|
||||||
|
* need. They are listed explicitly so the handler table is checked
|
||||||
|
* exhaustively against the Component union.
|
||||||
|
*/
|
||||||
|
type ComponentHandlers = {
|
||||||
|
[Name in ComponentName]: ComponentHandler<Name> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Sc2ComponentHandler {
|
||||||
|
private readonly skeletons = new Map<Hierarchy, Skeleton>();
|
||||||
|
private readonly jointBindings: [
|
||||||
|
owner: Node,
|
||||||
|
entity: string,
|
||||||
|
joint: Node,
|
||||||
|
skin: Skin,
|
||||||
|
][] = [];
|
||||||
|
|
||||||
|
private readonly handlers: ComponentHandlers = {
|
||||||
|
TransformComponent: (component, { node }) => {
|
||||||
|
node.setTranslation(sc2ToGltfVector(component["tc.localTranslation"]));
|
||||||
|
node.setRotation(sc2ToGltfQuaternion(component["tc.localRotation"]));
|
||||||
|
node.setScale(component["tc.localScale"]);
|
||||||
|
},
|
||||||
|
StateSwitcherComponent: (component, { hierarchy, node }) => {
|
||||||
|
const garage = this.options.skeleton?.garage;
|
||||||
|
|
||||||
|
node.setExtras({
|
||||||
|
// a part's scenarios can pick its state as it loads
|
||||||
|
state: garage
|
||||||
|
? (garage.states.get(hierarchy) ?? null)
|
||||||
|
: component[`ssc.state${component["ssc.activeState"]}`],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
SkeletonComponent: (component, { hierarchy, components, node }) => {
|
||||||
|
const { skeleton: options, document, buffer } = this.options;
|
||||||
|
|
||||||
|
if (!options) return;
|
||||||
|
|
||||||
|
const motion = options.motions.get(hierarchy);
|
||||||
|
const motionComponent = components.find(
|
||||||
|
(other): other is ComponentOf<"MotionComponent"> =>
|
||||||
|
other["comp.typename"] === "MotionComponent",
|
||||||
|
);
|
||||||
|
const created = createSkeleton(
|
||||||
|
document,
|
||||||
|
buffer,
|
||||||
|
node,
|
||||||
|
component,
|
||||||
|
motion,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.skeletons.set(hierarchy, created);
|
||||||
|
|
||||||
|
// only what loops in the garage is exported
|
||||||
|
if (motion && motionComponent && options.garage?.motions.has(hierarchy)) {
|
||||||
|
createMotionClip(
|
||||||
|
document,
|
||||||
|
buffer,
|
||||||
|
`motion${document.getRoot().listAnimations().length}`,
|
||||||
|
created.joints,
|
||||||
|
motion,
|
||||||
|
motionComponent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SkeletonJointBindingComponent: (component, { hierarchy, node }) => {
|
||||||
|
const skeleton = this.skeletons.get(hierarchy);
|
||||||
|
|
||||||
|
if (!skeleton) return;
|
||||||
|
|
||||||
|
times(component.count, (index) => {
|
||||||
|
const binding = component.map[
|
||||||
|
index.toString().padStart(4, "0")
|
||||||
|
] as unknown as { entityUid: string; jointUid: string };
|
||||||
|
const joint = skeleton.joints.get(binding.jointUid);
|
||||||
|
|
||||||
|
if (!joint) throw new Error(`Missing joint ${binding.jointUid}`);
|
||||||
|
|
||||||
|
this.jointBindings.push([
|
||||||
|
node,
|
||||||
|
binding.entityUid,
|
||||||
|
joint,
|
||||||
|
skeleton.skin,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
RenderComponent: (component, context) => {
|
||||||
|
if (this.options.render.kind === "visual") {
|
||||||
|
this.renderVisual(component, context, this.options.render);
|
||||||
|
} else {
|
||||||
|
this.renderCollision(component, context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// lod distances are implied by each batch's lodIndex
|
||||||
|
LodComponent: null,
|
||||||
|
MotionComponent: null, // consumed by SkeletonComponent
|
||||||
|
AnimationComponent: null,
|
||||||
|
SlotComponent: null,
|
||||||
|
NewSlotComponent: null,
|
||||||
|
DecorItemComponent: null,
|
||||||
|
ScenarioComponent: null,
|
||||||
|
CustomPropertiesComponent: null,
|
||||||
|
ActionComponent: null,
|
||||||
|
ParticleEffectComponent: null,
|
||||||
|
TankElementComponent: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(private readonly options: Sc2ComponentHandlerOptions) {}
|
||||||
|
|
||||||
|
/** Applies every component of one hierarchy entry onto its node. */
|
||||||
|
handle(hierarchy: Hierarchy, components: Component[], node: Node) {
|
||||||
|
const context: HierarchyContext = { hierarchy, components, node };
|
||||||
|
|
||||||
|
for (const component of sortBy(components, componentPriority)) {
|
||||||
|
this.dispatch(component, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private dispatch(component: Component, context: HierarchyContext) {
|
||||||
|
const name = component["comp.typename"];
|
||||||
|
// the table is exhaustive per name, but indexing by a union loses that
|
||||||
|
const handler = this.handlers[name] as AnyComponentHandler | null | undefined;
|
||||||
|
|
||||||
|
if (handler === undefined) {
|
||||||
|
if (this.options.strict) {
|
||||||
|
throw new TypeError(`Unhandled component type: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handler?.(component, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Call once after the whole hierarchy has been handled. */
|
||||||
|
resolveJointBindings() {
|
||||||
|
for (const [owner, entity, joint, skin] of this.jointBindings) {
|
||||||
|
// the game only binds the skeleton entity's own children
|
||||||
|
const socket = owner
|
||||||
|
.listChildren()
|
||||||
|
.find(
|
||||||
|
(child) =>
|
||||||
|
child.getName() === entity && !skin.listJoints().includes(child),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!socket) throw new Error(`Missing socket ${entity}`);
|
||||||
|
|
||||||
|
bindSocket(socket, joint, skin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderVisual(
|
||||||
|
component: RenderComponent,
|
||||||
|
{ hierarchy, node }: HierarchyContext,
|
||||||
|
render: Extract<Sc2RenderMode, { kind: "visual" }>,
|
||||||
|
) {
|
||||||
|
const { document, buffer, scg } = this.options;
|
||||||
|
const renderObject = component["rc.renderObj"];
|
||||||
|
|
||||||
|
times(renderObject["ro.batchCount"], (batchIndex) => {
|
||||||
|
const lodIndex = renderObject[`rb${batchIndex}.lodIndex`];
|
||||||
|
|
||||||
|
// -1 means the batch has no lods and is always drawn
|
||||||
|
if (lodIndex !== 0 && !(render.isPart && lodIndex === -1)) return;
|
||||||
|
|
||||||
|
const batchKey = batchIndex.toString().padStart(4, "0");
|
||||||
|
const batch = renderObject["ro.batches"][batchKey];
|
||||||
|
const material = render.materials.get(batch["rb.nmatname"]);
|
||||||
|
const polygonGroup = scg.get(batch["rb.datasource"]);
|
||||||
|
|
||||||
|
// probably shadow material
|
||||||
|
if (!(material instanceof Material)) return;
|
||||||
|
|
||||||
|
if (polygonGroup === undefined) {
|
||||||
|
throw new Error(`Missing polygon group ${batch["rb.datasource"]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributes = collectVertexAttributes(polygonGroup);
|
||||||
|
const primitive = document
|
||||||
|
.createPrimitive()
|
||||||
|
.setIndices(
|
||||||
|
document
|
||||||
|
.createAccessor()
|
||||||
|
.setType("SCALAR")
|
||||||
|
.setArray(new Uint16Array(polygonGroup.indices))
|
||||||
|
.setBuffer(buffer),
|
||||||
|
)
|
||||||
|
.setMaterial(material)
|
||||||
|
.setName(batchKey);
|
||||||
|
|
||||||
|
createAttributeAccessors(document, buffer, attributes).forEach(
|
||||||
|
(accessor, name) => primitive.setAttribute(name, accessor),
|
||||||
|
);
|
||||||
|
|
||||||
|
const lodNode = document
|
||||||
|
.createNode(batchKey)
|
||||||
|
.setMesh(document.createMesh(batch["##name"]).addPrimitive(primitive));
|
||||||
|
const skeleton = this.skeletons.get(hierarchy);
|
||||||
|
|
||||||
|
if (
|
||||||
|
skeleton &&
|
||||||
|
skinPrimitive(
|
||||||
|
document,
|
||||||
|
buffer,
|
||||||
|
primitive,
|
||||||
|
attributes,
|
||||||
|
renderObject[`skinnedObject.batch${batchIndex}.targetsData`],
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
lodNode.setSkin(skeleton.skin);
|
||||||
|
}
|
||||||
|
|
||||||
|
node.addChild(lodNode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderCollision(
|
||||||
|
component: RenderComponent,
|
||||||
|
{ hierarchy, node }: HierarchyContext,
|
||||||
|
) {
|
||||||
|
const { document, buffer, scg } = this.options;
|
||||||
|
const batch = component["rc.renderObj"]["ro.batches"]["0000"];
|
||||||
|
const polygonGroup = scg.get(batch["rb.datasource"]);
|
||||||
|
|
||||||
|
if (!polygonGroup) {
|
||||||
|
console.warn(
|
||||||
|
`Missing polygon group ${batch["rb.datasource"]} (${hierarchy.name}); skipping...`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hardJointIndices = new Set<number>();
|
||||||
|
const vertexHardJointIndices = new Map<number, number>();
|
||||||
|
|
||||||
|
polygonGroup.vertices.forEach((vertex, index) => {
|
||||||
|
vertex.forEach(({ attribute, value }) => {
|
||||||
|
if (attribute !== VertexAttribute.HARD_JOINTINDEX) return;
|
||||||
|
|
||||||
|
hardJointIndices.add(value[0]);
|
||||||
|
vertexHardJointIndices.set(index, value[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// geometry is shared; only the index list differs per hard joint
|
||||||
|
const accessors = createAttributeAccessors(
|
||||||
|
document,
|
||||||
|
buffer,
|
||||||
|
collectVertexAttributes(polygonGroup),
|
||||||
|
);
|
||||||
|
|
||||||
|
hardJointIndices.forEach((hardJointIndex) => {
|
||||||
|
const primitive = document
|
||||||
|
.createPrimitive()
|
||||||
|
.setIndices(
|
||||||
|
document
|
||||||
|
.createAccessor()
|
||||||
|
.setType("SCALAR")
|
||||||
|
.setArray(
|
||||||
|
new Uint16Array(
|
||||||
|
polygonGroup.indices.filter(
|
||||||
|
(index) =>
|
||||||
|
vertexHardJointIndices.get(index) === hardJointIndex,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setBuffer(buffer),
|
||||||
|
);
|
||||||
|
|
||||||
|
accessors.forEach((accessor, name) =>
|
||||||
|
primitive.setAttribute(name, accessor),
|
||||||
|
);
|
||||||
|
|
||||||
|
node.addChild(
|
||||||
|
document
|
||||||
|
.createNode(`${hierarchy.name}_armor_${hardJointIndex}`)
|
||||||
|
.setMesh(document.createMesh(batch["##name"]).addPrimitive(primitive)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { VertexAttribute, type Component } from "@blitzkit/core";
|
import { VertexAttribute, type ComponentOf } from "@blitzkit/core";
|
||||||
import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
|
import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
|
||||||
import {
|
import {
|
||||||
MathUtils,
|
MathUtils,
|
||||||
@@ -17,14 +17,12 @@ import { dirname, normalize } from "path/posix";
|
|||||||
import { Matrix4 } from "three";
|
import { Matrix4 } from "three";
|
||||||
import { MotionChannel, MotionReadStream, type Motion } from "./motion";
|
import { MotionChannel, MotionReadStream, type Motion } from "./motion";
|
||||||
|
|
||||||
type SkeletonComponent = Extract<
|
export interface Skeleton {
|
||||||
Component,
|
skin: Skin;
|
||||||
{ "comp.typename": "SkeletonComponent" }
|
/** joint uid -> joint node */
|
||||||
>;
|
joints: Map<string, Node>;
|
||||||
type MotionComponent = Extract<
|
}
|
||||||
Component,
|
|
||||||
{ "comp.typename": "MotionComponent" }
|
|
||||||
>;
|
|
||||||
type AccessorType = "SCALAR" | "VEC3" | "VEC4" | "MAT4";
|
type AccessorType = "SCALAR" | "VEC3" | "VEC4" | "MAT4";
|
||||||
|
|
||||||
const JOINT_ROOT = 0xffffff;
|
const JOINT_ROOT = 0xffffff;
|
||||||
@@ -50,7 +48,7 @@ function createAccessor(
|
|||||||
export async function loadMotion(
|
export async function loadMotion(
|
||||||
vfs: AbstractVFS,
|
vfs: AbstractVFS,
|
||||||
path: string,
|
path: string,
|
||||||
component: MotionComponent,
|
component: ComponentOf<"MotionComponent">,
|
||||||
) {
|
) {
|
||||||
const file = await vfs.file(
|
const file = await vfs.file(
|
||||||
normalize(`Data/3d/${dirname(path)}/${component["motion.filepath"]}`),
|
normalize(`Data/3d/${dirname(path)}/${component["motion.filepath"]}`),
|
||||||
@@ -68,9 +66,9 @@ export function createSkeleton(
|
|||||||
document: Document,
|
document: Document,
|
||||||
buffer: Buffer,
|
buffer: Buffer,
|
||||||
node: Node,
|
node: Node,
|
||||||
skeleton: SkeletonComponent,
|
skeleton: ComponentOf<"SkeletonComponent">,
|
||||||
motion: Motion | undefined,
|
motion: Motion | undefined,
|
||||||
) {
|
): Skeleton {
|
||||||
const joints = times(
|
const joints = times(
|
||||||
skeleton.jointsCount,
|
skeleton.jointsCount,
|
||||||
(index) => skeleton.joints[index.toString().padStart(4, "0")],
|
(index) => skeleton.joints[index.toString().padStart(4, "0")],
|
||||||
@@ -133,7 +131,7 @@ export function createSkeleton(
|
|||||||
joints: new Map(
|
joints: new Map(
|
||||||
joints.map((joint, index) => [joint["joint.uid"], jointNodes[index]]),
|
joints.map((joint, index) => [joint["joint.uid"], jointNodes[index]]),
|
||||||
),
|
),
|
||||||
};
|
} satisfies Skeleton;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMotionClip(
|
export function createMotionClip(
|
||||||
@@ -142,7 +140,7 @@ export function createMotionClip(
|
|||||||
clip: string,
|
clip: string,
|
||||||
joints: Map<string, Node>,
|
joints: Map<string, Node>,
|
||||||
motion: Motion,
|
motion: Motion,
|
||||||
component: MotionComponent,
|
component: ComponentOf<"MotionComponent">,
|
||||||
) {
|
) {
|
||||||
const animation = document.createAnimation(clip);
|
const animation = document.createAnimation(clip);
|
||||||
const playbackRate = component["motion.playbackRate"];
|
const playbackRate = component["motion.playbackRate"];
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import {
|
import { parseHierarchies, type Hierarchy, type SC2 } from "@blitzkit/core";
|
||||||
parseHierarchies,
|
|
||||||
type Hierarchy,
|
|
||||||
type Sc2ReadStream,
|
|
||||||
} from "@blitzkit/core";
|
|
||||||
import { sortBy, times } from "lodash-es";
|
import { sortBy, times } from "lodash-es";
|
||||||
|
|
||||||
|
/** the replayed end state of a part or tank */
|
||||||
|
export interface Garage {
|
||||||
|
/** state switcher -> chosen state */
|
||||||
|
states: Map<Hierarchy, string | null>;
|
||||||
|
/** entities left looping their motion */
|
||||||
|
motions: Set<Hierarchy>;
|
||||||
|
}
|
||||||
|
|
||||||
interface Value {
|
interface Value {
|
||||||
"any.type": string;
|
"any.type": string;
|
||||||
"any.value": unknown;
|
"any.value": unknown;
|
||||||
@@ -89,7 +93,7 @@ function resolveTarget(owner: Entity, entityName: string) {
|
|||||||
* through their motions, delays and alarms until they settle
|
* through their motions, delays and alarms until they settle
|
||||||
*/
|
*/
|
||||||
function replay(
|
function replay(
|
||||||
sc2: ReturnType<Sc2ReadStream["sc2"]>,
|
sc2: SC2,
|
||||||
preset: string | undefined,
|
preset: string | undefined,
|
||||||
attachments: Map<string, string[]>,
|
attachments: Map<string, string[]>,
|
||||||
animate: boolean,
|
animate: boolean,
|
||||||
@@ -506,7 +510,7 @@ function replay(
|
|||||||
.filter(isShown)
|
.filter(isShown)
|
||||||
.map((entity) => entity.hierarchy as Hierarchy),
|
.map((entity) => entity.hierarchy as Hierarchy),
|
||||||
),
|
),
|
||||||
};
|
} satisfies Garage;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -514,7 +518,7 @@ function replay(
|
|||||||
* as they are when the tank appears
|
* as they are when the tank appears
|
||||||
*/
|
*/
|
||||||
export function switcherStates(
|
export function switcherStates(
|
||||||
sc2: ReturnType<Sc2ReadStream["sc2"]>,
|
sc2: SC2,
|
||||||
preset: string | undefined,
|
preset: string | undefined,
|
||||||
attachments: Map<string, string[]>,
|
attachments: Map<string, string[]>,
|
||||||
) {
|
) {
|
||||||
@@ -532,9 +536,6 @@ export function switcherStates(
|
|||||||
* where a customization part ends up: each state switcher's state and the
|
* where a customization part ends up: each state switcher's state and the
|
||||||
* motions left looping
|
* motions left looping
|
||||||
*/
|
*/
|
||||||
export function partGarage(
|
export function partGarage(sc2: SC2, preset: string | undefined): Garage {
|
||||||
sc2: ReturnType<Sc2ReadStream["sc2"]>,
|
|
||||||
preset: string | undefined,
|
|
||||||
) {
|
|
||||||
return replay(sc2, preset, new Map(), true);
|
return replay(sc2, preset, new Map(), true);
|
||||||
}
|
}
|
||||||
@@ -3,13 +3,12 @@ import {
|
|||||||
Sc2ReadStream,
|
Sc2ReadStream,
|
||||||
ScgReadStream,
|
ScgReadStream,
|
||||||
toUniqueId,
|
toUniqueId,
|
||||||
VertexAttribute,
|
|
||||||
vertexAttributeVectorSizes,
|
|
||||||
type VehicleDefinitionList,
|
type VehicleDefinitionList,
|
||||||
} from "@blitzkit/core";
|
} from "@blitzkit/core";
|
||||||
import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
|
import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
|
||||||
import { Accessor, Document, Node, NodeIO, Scene } from "@gltf-transform/core";
|
import { Document, Node, NodeIO, Scene } from "@gltf-transform/core";
|
||||||
import type { APIContext } from "astro";
|
import type { APIContext } from "astro";
|
||||||
|
import { Sc2ComponentHandler } from "../../../core/blitzkit/sc2Components";
|
||||||
import { vfs } from "../../../core/blitzkit/vfs";
|
import { vfs } from "../../../core/blitzkit/vfs";
|
||||||
|
|
||||||
export { getStaticPaths } from "./_index";
|
export { getStaticPaths } from "./_index";
|
||||||
@@ -54,110 +53,28 @@ async function extractArmor(vfs: AbstractVFS, fileName: string) {
|
|||||||
const document = new Document();
|
const document = new Document();
|
||||||
const scene = document.createScene();
|
const scene = document.createScene();
|
||||||
const buffer = document.createBuffer();
|
const buffer = document.createBuffer();
|
||||||
|
const handler = new Sc2ComponentHandler({
|
||||||
|
document,
|
||||||
|
buffer,
|
||||||
|
scg,
|
||||||
|
render: { kind: "collision" },
|
||||||
|
strict: true,
|
||||||
|
});
|
||||||
|
|
||||||
parseHierarchies<Scene | Node>(
|
parseHierarchies<Scene | Node>(
|
||||||
sc2["#hierarchy"],
|
sc2["#hierarchy"],
|
||||||
scene,
|
scene,
|
||||||
(hierarchy, components, parent) => {
|
(hierarchy, components, parent) => {
|
||||||
components.forEach((component) => {
|
/**
|
||||||
switch (component["comp.typename"]) {
|
* Consumers (nameToArmorId and friends) expect a flat list of
|
||||||
case "TransformComponent":
|
* `<hierarchy>_armor_<n>` nodes under the scene, so the hierarchy node
|
||||||
break;
|
* is only a scratch holder: its armor children are hoisted out of it
|
||||||
|
* and it is never attached.
|
||||||
|
*/
|
||||||
|
const holder = document.createNode(hierarchy.name);
|
||||||
|
|
||||||
case "RenderComponent": {
|
handler.handle(hierarchy, components, holder);
|
||||||
const batch = component["rc.renderObj"]["ro.batches"]["0000"];
|
holder.listChildren().forEach((armor) => scene.addChild(armor));
|
||||||
const polygonGroup = scg.get(batch["rb.datasource"]);
|
|
||||||
|
|
||||||
if (!polygonGroup) {
|
|
||||||
console.warn(
|
|
||||||
`Missing polygon group ${batch["rb.datasource"]} (${hierarchy.name}); skipping...`,
|
|
||||||
);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hardJointIndices = new Set<number>();
|
|
||||||
const vertexHardJointIndices = new Map<number, number>();
|
|
||||||
const attributes = new Map<VertexAttribute, number[][]>();
|
|
||||||
|
|
||||||
polygonGroup.vertices.forEach((vertex, index) => {
|
|
||||||
vertex.forEach(({ attribute, value }) => {
|
|
||||||
if (!attributes.has(attribute)) {
|
|
||||||
attributes.set(attribute, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
attributes.get(attribute)!.push(value);
|
|
||||||
|
|
||||||
if (attribute === VertexAttribute.HARD_JOINTINDEX) {
|
|
||||||
hardJointIndices.add(value[0]);
|
|
||||||
vertexHardJointIndices.set(index, value[0]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (attributes.has(VertexAttribute.TANGENT)) {
|
|
||||||
attributes.set(
|
|
||||||
VertexAttribute.TANGENT,
|
|
||||||
attributes
|
|
||||||
.get(VertexAttribute.TANGENT)!
|
|
||||||
.map((tangent) => [...tangent, 1]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessors = new Map<string, Accessor>();
|
|
||||||
|
|
||||||
attributes.forEach((value, attribute) => {
|
|
||||||
const name = vertexAttributeGLTFName[attribute];
|
|
||||||
if (!name || accessors.has(name)) return;
|
|
||||||
const vertexSize = vertexAttributeGltfVectorSizes[attribute];
|
|
||||||
const accessor = document
|
|
||||||
.createAccessor(name)
|
|
||||||
.setType(vertexSize === 1 ? "SCALAR" : `VEC${vertexSize}`)
|
|
||||||
.setArray(new Float32Array(value.flat()))
|
|
||||||
.setBuffer(buffer);
|
|
||||||
|
|
||||||
accessors.set(name, accessor);
|
|
||||||
});
|
|
||||||
|
|
||||||
hardJointIndices.forEach((hardJointIndex) => {
|
|
||||||
const node = document.createNode(
|
|
||||||
`${hierarchy.name}_armor_${hardJointIndex}`,
|
|
||||||
);
|
|
||||||
const mesh = document.createMesh(batch["##name"]);
|
|
||||||
const indicesAccessor = document
|
|
||||||
.createAccessor()
|
|
||||||
.setType("SCALAR")
|
|
||||||
.setArray(
|
|
||||||
new Uint16Array(
|
|
||||||
polygonGroup.indices.filter(
|
|
||||||
(index) =>
|
|
||||||
vertexHardJointIndices.get(index) === hardJointIndex,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.setBuffer(buffer);
|
|
||||||
const primitive = document
|
|
||||||
.createPrimitive()
|
|
||||||
.setIndices(indicesAccessor);
|
|
||||||
|
|
||||||
accessors.forEach((accessor, name) => {
|
|
||||||
primitive.setAttribute(name, accessor);
|
|
||||||
});
|
|
||||||
|
|
||||||
mesh.addPrimitive(primitive);
|
|
||||||
node.setMesh(mesh);
|
|
||||||
parent.addChild(node);
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new TypeError(
|
|
||||||
`Unhandled component type: ${component["comp.typename"]}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return parent;
|
return parent;
|
||||||
},
|
},
|
||||||
@@ -165,17 +82,3 @@ async function extractArmor(vfs: AbstractVFS, fileName: string) {
|
|||||||
|
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const vertexAttributeGLTFName: Partial<Record<VertexAttribute, string>> =
|
|
||||||
{
|
|
||||||
[VertexAttribute.VERTEX]: "POSITION",
|
|
||||||
[VertexAttribute.NORMAL]: "NORMAL",
|
|
||||||
[VertexAttribute.TEXCOORD0]: "TEXCOORD_0",
|
|
||||||
[VertexAttribute.TEXCOORD1]: "TEXCOORD_1",
|
|
||||||
[VertexAttribute.TEXCOORD2]: "TEXCOORD_2",
|
|
||||||
[VertexAttribute.TEXCOORD3]: "TEXCOORD_3",
|
|
||||||
};
|
|
||||||
export const vertexAttributeGltfVectorSizes = {
|
|
||||||
...vertexAttributeVectorSizes,
|
|
||||||
[VertexAttribute.TANGENT]: 4,
|
|
||||||
} as const;
|
|
||||||
@@ -6,10 +6,9 @@ import {
|
|||||||
Sc2ReadStream,
|
Sc2ReadStream,
|
||||||
ScgReadStream,
|
ScgReadStream,
|
||||||
toUniqueId,
|
toUniqueId,
|
||||||
VertexAttribute,
|
|
||||||
type ConfigArchive,
|
type ConfigArchive,
|
||||||
type Hierarchy,
|
|
||||||
type TankParameters,
|
type TankParameters,
|
||||||
|
type SC2,
|
||||||
type Textures,
|
type Textures,
|
||||||
type VehicleDefinitionList,
|
type VehicleDefinitionList,
|
||||||
} from "@blitzkit/core";
|
} from "@blitzkit/core";
|
||||||
@@ -20,7 +19,6 @@ import {
|
|||||||
Node,
|
Node,
|
||||||
NodeIO,
|
NodeIO,
|
||||||
Scene,
|
Scene,
|
||||||
type Skin,
|
|
||||||
} from "@gltf-transform/core";
|
} from "@gltf-transform/core";
|
||||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||||
import { dedup, prune } from "@gltf-transform/functions";
|
import { dedup, prune } from "@gltf-transform/functions";
|
||||||
@@ -28,20 +26,12 @@ import type { APIContext } from "astro";
|
|||||||
import { isEqual, times } from "lodash-es";
|
import { isEqual, times } from "lodash-es";
|
||||||
import { dirname } from "path";
|
import { dirname } from "path";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import type { Motion } from "../../../core/blitzkit/skinAnimations/motion";
|
|
||||||
import {
|
import {
|
||||||
bindSocket,
|
loadHierarchyMotions,
|
||||||
createMotionClip,
|
Sc2ComponentHandler,
|
||||||
createSkeleton,
|
} from "../../../core/blitzkit/sc2Components";
|
||||||
loadMotion,
|
|
||||||
skinPrimitive,
|
|
||||||
} from "../../../core/blitzkit/skinAnimations/skeleton";
|
|
||||||
import { partGarage } from "../../../core/blitzkit/switcherStates";
|
import { partGarage } from "../../../core/blitzkit/switcherStates";
|
||||||
import { vfs } from "../../../core/blitzkit/vfs";
|
import { vfs } from "../../../core/blitzkit/vfs";
|
||||||
import {
|
|
||||||
vertexAttributeGLTFName,
|
|
||||||
vertexAttributeGltfVectorSizes,
|
|
||||||
} from "./collision.glb";
|
|
||||||
|
|
||||||
export { getStaticPaths } from "./_index";
|
export { getStaticPaths } from "./_index";
|
||||||
|
|
||||||
@@ -110,35 +100,17 @@ export async function extractModel(
|
|||||||
const scene = document.createScene();
|
const scene = document.createScene();
|
||||||
const buffer = document.createBuffer();
|
const buffer = document.createBuffer();
|
||||||
const materials = await extractMaterials(document, sc2, path);
|
const materials = await extractMaterials(document, sc2, path);
|
||||||
const skeletons = new Map<Hierarchy, ReturnType<typeof createSkeleton>>();
|
|
||||||
const jointBindings: [
|
|
||||||
owner: Node,
|
|
||||||
entity: string,
|
|
||||||
joint: Node,
|
|
||||||
skin: Skin,
|
|
||||||
][] = [];
|
|
||||||
const motions = new Map<Hierarchy, Motion>();
|
|
||||||
const garage = isPart ? partGarage(sc2, preset) : undefined;
|
const garage = isPart ? partGarage(sc2, preset) : undefined;
|
||||||
|
const handler = new Sc2ComponentHandler({
|
||||||
if (isPart) {
|
document,
|
||||||
const loading: Promise<void>[] = [];
|
buffer,
|
||||||
|
scg,
|
||||||
parseHierarchies(sc2["#hierarchy"], null, (hierarchy, components) => {
|
render: { kind: "visual", materials, isPart },
|
||||||
for (const component of components) {
|
skeleton: isPart
|
||||||
if (component["comp.typename"] !== "MotionComponent") continue;
|
? { motions: await loadHierarchyMotions(vfs, path, sc2), garage }
|
||||||
|
: undefined,
|
||||||
loading.push(
|
strict: ERROR_ON_UNKNOWN_COMPONENT,
|
||||||
loadMotion(vfs, path, component).then((motion) => {
|
});
|
||||||
motions.set(hierarchy, motion);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(loading);
|
|
||||||
}
|
|
||||||
|
|
||||||
parseHierarchies<Scene | Node>(
|
parseHierarchies<Scene | Node>(
|
||||||
sc2["#hierarchy"],
|
sc2["#hierarchy"],
|
||||||
@@ -152,228 +124,14 @@ export async function extractModel(
|
|||||||
|
|
||||||
const node = document.createNode(hierarchy.name);
|
const node = document.createNode(hierarchy.name);
|
||||||
|
|
||||||
if (isPart) {
|
handler.handle(hierarchy, components, node);
|
||||||
const skeleton = components.find(
|
|
||||||
(component) => component["comp.typename"] === "SkeletonComponent",
|
|
||||||
);
|
|
||||||
const motionComponent = components.find(
|
|
||||||
(component) => component["comp.typename"] === "MotionComponent",
|
|
||||||
);
|
|
||||||
const motion = motions.get(hierarchy);
|
|
||||||
|
|
||||||
if (skeleton) {
|
|
||||||
const created = createSkeleton(
|
|
||||||
document,
|
|
||||||
buffer,
|
|
||||||
node,
|
|
||||||
skeleton,
|
|
||||||
motion,
|
|
||||||
);
|
|
||||||
|
|
||||||
skeletons.set(hierarchy, created);
|
|
||||||
|
|
||||||
// only what loops in the garage is exported
|
|
||||||
if (motion && motionComponent && garage?.motions.has(hierarchy)) {
|
|
||||||
createMotionClip(
|
|
||||||
document,
|
|
||||||
buffer,
|
|
||||||
`motion${document.getRoot().listAnimations().length}`,
|
|
||||||
created.joints,
|
|
||||||
motion,
|
|
||||||
motionComponent,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
components.forEach((component) => {
|
|
||||||
switch (component["comp.typename"]) {
|
|
||||||
case "LodComponent":
|
|
||||||
// found and used later by transform component
|
|
||||||
// TODO: lod component always shows up before transform component; cache it before parsing transform
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "TransformComponent": {
|
|
||||||
const t = component["tc.localTranslation"];
|
|
||||||
const r = component["tc.localRotation"];
|
|
||||||
|
|
||||||
node.setTranslation([t[0], t[2], -t[1]]);
|
|
||||||
node.setRotation([r[0], r[2], -r[1], r[3]]);
|
|
||||||
node.setScale(component["tc.localScale"]);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "StateSwitcherComponent":
|
|
||||||
node.setExtras({
|
|
||||||
// a part's scenarios can pick its state as it loads
|
|
||||||
state: garage
|
|
||||||
? (garage.states.get(hierarchy) ?? null)
|
|
||||||
: component[`ssc.state${component["ssc.activeState"]}`],
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "SkeletonJointBindingComponent": {
|
|
||||||
const skeleton = skeletons.get(hierarchy);
|
|
||||||
|
|
||||||
if (!skeleton) break;
|
|
||||||
|
|
||||||
times(component.count, (index) => {
|
|
||||||
const binding = component.map[
|
|
||||||
index.toString().padStart(4, "0")
|
|
||||||
] as unknown as { entityUid: string; jointUid: string };
|
|
||||||
const joint = skeleton.joints.get(binding.jointUid);
|
|
||||||
|
|
||||||
if (!joint) throw new Error(`Missing joint ${binding.jointUid}`);
|
|
||||||
|
|
||||||
jointBindings.push([
|
|
||||||
node,
|
|
||||||
binding.entityUid,
|
|
||||||
joint,
|
|
||||||
skeleton.skin,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "RenderComponent": {
|
|
||||||
const renderObject = component["rc.renderObj"];
|
|
||||||
|
|
||||||
times(renderObject["ro.batchCount"], (batchIndex): void => {
|
|
||||||
const lodIndex = renderObject[`rb${batchIndex}.lodIndex`];
|
|
||||||
|
|
||||||
// -1 means the batch has no lods and is always drawn
|
|
||||||
if (lodIndex !== 0 && !(isPart && lodIndex === -1)) return;
|
|
||||||
|
|
||||||
const batchKey = batchIndex.toString().padStart(4, "0");
|
|
||||||
const batch = renderObject["ro.batches"][batchKey];
|
|
||||||
const material = materials.get(batch["rb.nmatname"]);
|
|
||||||
const polygonGroup = scg.get(batch["rb.datasource"]);
|
|
||||||
|
|
||||||
if (!(material instanceof Material)) {
|
|
||||||
// probably shadow material
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (polygonGroup === undefined) {
|
|
||||||
throw new Error(
|
|
||||||
`Missing polygon group ${batch["rb.datasource"]}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lodNode = document.createNode(batchKey);
|
|
||||||
const indicesAccessor = document
|
|
||||||
.createAccessor()
|
|
||||||
.setType("SCALAR")
|
|
||||||
.setArray(new Uint16Array(polygonGroup.indices))
|
|
||||||
.setBuffer(buffer);
|
|
||||||
const primitive = document
|
|
||||||
.createPrimitive()
|
|
||||||
.setIndices(indicesAccessor)
|
|
||||||
.setMaterial(material)
|
|
||||||
.setName(batchKey);
|
|
||||||
|
|
||||||
const attributes = new Map<VertexAttribute, number[][]>();
|
|
||||||
|
|
||||||
polygonGroup.vertices.forEach((vertex) => {
|
|
||||||
vertex.forEach(({ attribute, value }) => {
|
|
||||||
if (!attributes.has(attribute)) {
|
|
||||||
attributes.set(attribute, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
attributes.get(attribute)!.push(value);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
attributes.forEach((value, attribute) => {
|
|
||||||
const name = vertexAttributeGLTFName[attribute];
|
|
||||||
|
|
||||||
if (
|
|
||||||
name === undefined ||
|
|
||||||
primitive.getAttribute(name) !== null
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const vertexSize = vertexAttributeGltfVectorSizes[attribute];
|
|
||||||
const processed = new Float32Array(value.flat());
|
|
||||||
|
|
||||||
if (
|
|
||||||
attribute === VertexAttribute.VERTEX ||
|
|
||||||
attribute === VertexAttribute.NORMAL ||
|
|
||||||
attribute === VertexAttribute.TANGENT ||
|
|
||||||
attribute === VertexAttribute.BINORMAL
|
|
||||||
) {
|
|
||||||
for (let i = 0; i < processed.length; i += 3) {
|
|
||||||
const y = processed[i + 1];
|
|
||||||
const z = processed[i + 2];
|
|
||||||
|
|
||||||
processed[i + 1] = z;
|
|
||||||
processed[i + 2] = -y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const attributeAccessor = document
|
|
||||||
.createAccessor(name)
|
|
||||||
.setType(vertexSize === 1 ? "SCALAR" : `VEC${vertexSize}`)
|
|
||||||
.setArray(processed)
|
|
||||||
.setBuffer(buffer);
|
|
||||||
|
|
||||||
primitive.setAttribute(name, attributeAccessor);
|
|
||||||
});
|
|
||||||
|
|
||||||
const skeleton = skeletons.get(hierarchy);
|
|
||||||
const mesh = document
|
|
||||||
.createMesh(batch["##name"])
|
|
||||||
.addPrimitive(primitive);
|
|
||||||
lodNode.setMesh(mesh);
|
|
||||||
|
|
||||||
if (
|
|
||||||
skeleton &&
|
|
||||||
skinPrimitive(
|
|
||||||
document,
|
|
||||||
buffer,
|
|
||||||
primitive,
|
|
||||||
attributes,
|
|
||||||
renderObject[`skinnedObject.batch${batchIndex}.targetsData`],
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
lodNode.setSkin(skeleton.skin);
|
|
||||||
}
|
|
||||||
node.addChild(lodNode);
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
if (ERROR_ON_UNKNOWN_COMPONENT) {
|
|
||||||
throw new Error(
|
|
||||||
`Unhandled component type: ${component["comp.typename"]}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
parent.addChild(node);
|
parent.addChild(node);
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const [owner, entity, joint, skin] of jointBindings) {
|
handler.resolveJointBindings();
|
||||||
// the game only binds the skeleton entity's own children
|
|
||||||
const socket = owner
|
|
||||||
.listChildren()
|
|
||||||
.find(
|
|
||||||
(child) =>
|
|
||||||
child.getName() === entity && !skin.listJoints().includes(child),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!socket) throw new Error(`Missing socket ${entity}`);
|
|
||||||
|
|
||||||
bindSocket(socket, joint, skin);
|
|
||||||
}
|
|
||||||
|
|
||||||
scene.addChild(document.createNode("test"));
|
scene.addChild(document.createNode("test"));
|
||||||
|
|
||||||
@@ -384,7 +142,7 @@ export async function extractModel(
|
|||||||
|
|
||||||
export async function extractMaterials(
|
export async function extractMaterials(
|
||||||
document: Document,
|
document: Document,
|
||||||
sc2: ReturnType<Sc2ReadStream["sc2"]>,
|
sc2: SC2,
|
||||||
path: string,
|
path: string,
|
||||||
config?: string,
|
config?: string,
|
||||||
) {
|
) {
|
||||||
|
|||||||
Reference in new issue
Block a user