220 lines
6.2 KiB
TypeScript
220 lines
6.2 KiB
TypeScript
import {
|
|
AnimationClip,
|
|
Bone,
|
|
Object3D,
|
|
Quaternion,
|
|
SkinnedMesh,
|
|
Vector3,
|
|
} from 'three';
|
|
import { assetManager } from '../assets';
|
|
import { Group } from '../gameobjects/group.object';
|
|
import { MeshPart } from '../gameobjects/mesh.object';
|
|
import { Humanoid } from '../gameobjects/humanoid.object';
|
|
import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
|
|
import {
|
|
CharacterBodyPart,
|
|
CharacterSpecification,
|
|
CharacterTextureType,
|
|
} from '../types/character';
|
|
|
|
const CHARACTER_MAX_VERSION = 1;
|
|
const CHARACTER_MIN_VERSION = 1;
|
|
|
|
const cachedMeta: {
|
|
faceTexture: string;
|
|
root: Object3D | null;
|
|
clips: AnimationClip[];
|
|
specification: CharacterSpecification | null;
|
|
} = {
|
|
faceTexture: '',
|
|
root: null,
|
|
clips: [],
|
|
specification: null,
|
|
};
|
|
|
|
function deepen(obj: Record<string, any>) {
|
|
const result = {};
|
|
|
|
// For each object path (property key) in the object
|
|
for (const objectPath in obj) {
|
|
// Split path into component parts
|
|
const parts = objectPath.split('.');
|
|
|
|
// Create sub-objects along path as needed
|
|
let target: any = result;
|
|
while (parts.length > 1) {
|
|
const part = parts.shift();
|
|
if (!part) continue;
|
|
target = target[part] = target[part] || {};
|
|
}
|
|
|
|
// Set value at end of path
|
|
target[parts[0]] = obj[objectPath];
|
|
}
|
|
|
|
return result as Record<string, any>;
|
|
}
|
|
|
|
function splitKeys(obj: Record<string, string>, keys?: string[]) {
|
|
const tmp: Record<string, string[]> = {};
|
|
Object.keys(obj).forEach((key) => {
|
|
if (!keys || keys.includes(key)) {
|
|
tmp[key] = obj[key].split(',');
|
|
}
|
|
});
|
|
return { ...obj, ...tmp };
|
|
}
|
|
|
|
function convertKey(key: string) {
|
|
return key.replace(/\./g, '');
|
|
}
|
|
|
|
export const parseCharacterSpecificaton = (armature: Object3D) => {
|
|
const temporaryObject = deepen(armature.userData);
|
|
const bodyParts: CharacterBodyPart[] = [];
|
|
|
|
// Check for required properties and version number
|
|
if (
|
|
!temporaryObject.FBLXC?.Version ||
|
|
!temporaryObject.FBLXC?.Parts ||
|
|
!temporaryObject.FBLXC?.Bones ||
|
|
temporaryObject.FBLXC.Version > CHARACTER_MAX_VERSION ||
|
|
temporaryObject.FBLXC.Version < CHARACTER_MIN_VERSION
|
|
) {
|
|
throw new Error('Character model parsing failed: Unsupported');
|
|
}
|
|
|
|
// Comma-separated lists to string arrays
|
|
const fblxc = temporaryObject.FBLXC;
|
|
Object.assign(
|
|
fblxc,
|
|
splitKeys(fblxc, ['Parts', 'ColorParts', 'Bones', 'Accessories'])
|
|
);
|
|
|
|
// Comma-separated lists to string arrays
|
|
if (fblxc.Texture) {
|
|
Object.assign(fblxc.Texture, splitKeys(fblxc.Texture));
|
|
}
|
|
|
|
// Parse accessory arrays
|
|
if (fblxc.Accessory) {
|
|
Object.keys(fblxc.Accessory).forEach((accessory) => {
|
|
for (const [key, value] of Object.entries(fblxc.Accessory[accessory])) {
|
|
if (key === 'Origin') {
|
|
fblxc.Accessory[accessory][key] = new Vector3().fromArray(
|
|
value as number[]
|
|
);
|
|
}
|
|
|
|
if (key === 'Quat') {
|
|
const wComponent = (value as number[]).shift() as number;
|
|
fblxc.Accessory[accessory][key] = new Quaternion().fromArray([
|
|
...(value as number[]),
|
|
wComponent,
|
|
]);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Collect body part information
|
|
for (const [index, part] of (fblxc.Parts as string[]).entries()) {
|
|
const bodyPart = <CharacterBodyPart>{
|
|
meshName: convertKey(part),
|
|
boneName: convertKey(fblxc.Bones?.[index] || part),
|
|
colored: fblxc.ColorParts?.includes(part) || false,
|
|
accessories: Object.keys(fblxc.Accessory)
|
|
.filter((accessory) => fblxc.Accessory[accessory].Parent === part)
|
|
.map((accessory) => ({
|
|
type: accessory,
|
|
origin: fblxc.Accessory[accessory].Origin || new Vector3(),
|
|
quaternion: fblxc.Accessory[accessory].Quat || new Quaternion(),
|
|
})),
|
|
textures: Object.keys(fblxc.Texture)
|
|
.filter((texture) => fblxc.Texture[texture].includes(part))
|
|
.map((texture) => texture),
|
|
};
|
|
bodyParts.push(bodyPart);
|
|
}
|
|
|
|
return <CharacterSpecification>{
|
|
info: fblxc,
|
|
bodyParts,
|
|
};
|
|
};
|
|
|
|
export const loadBaseCharacter = async () => {
|
|
if (cachedMeta.root) return cachedMeta;
|
|
|
|
// TODO: temporary loading method
|
|
const loadMesh = await assetManager.loadMeshData(
|
|
'https://lunasqu.ee/freeblox/character-base.glb'
|
|
);
|
|
const loadFace = await assetManager.createAsset(
|
|
'https://lunasqu.ee/freeblox/face-1.png',
|
|
'CharacterFace1',
|
|
'Texture'
|
|
);
|
|
const armature = loadMesh.scene.getObjectByName('Armature');
|
|
if (!armature) throw new Error('Invalid character asset');
|
|
const specification = parseCharacterSpecificaton(armature);
|
|
|
|
loadFace.texture!.flipY = false;
|
|
loadFace.texture!.needsUpdate = true;
|
|
cachedMeta.faceTexture = loadFace.path!;
|
|
cachedMeta.root = loadMesh.scene;
|
|
cachedMeta.clips = loadMesh.animations;
|
|
cachedMeta.specification = specification;
|
|
return cachedMeta;
|
|
};
|
|
|
|
export const instanceCharacterObject = async (
|
|
name: string,
|
|
pos = new Vector3(0, 1, 0)
|
|
) => {
|
|
const base = await loadBaseCharacter();
|
|
const cloned = SkeletonUtils.clone(base.root!);
|
|
|
|
const baseObject = new Group();
|
|
const armature = cloned.getObjectByName('Armature');
|
|
const bone = armature!.getObjectByName('BRoot');
|
|
|
|
for (const part of base.specification!.bodyParts) {
|
|
const object = armature!.getObjectByName(part.meshName) as Object3D;
|
|
const converted = MeshPart.fromLoaded(object as SkinnedMesh);
|
|
|
|
object.name = object.name + 'Mesh';
|
|
converted.name = part.meshName;
|
|
converted.archivable = false;
|
|
|
|
if (part.textures.includes(CharacterTextureType.Face)) {
|
|
converted.texture = base.faceTexture;
|
|
}
|
|
|
|
baseObject.add(converted);
|
|
}
|
|
|
|
baseObject.animations = base.clips;
|
|
baseObject.add(bone as Bone);
|
|
baseObject.archivable = false;
|
|
baseObject.name = name;
|
|
|
|
const controller = new Humanoid();
|
|
controller.position.set(0, 4.75, 0);
|
|
controller.archivable = false;
|
|
controller.bodyPartNames = base.specification!.bodyParts.map((entry) =>
|
|
convertKey(entry.meshName)
|
|
);
|
|
controller.bodyBoneNames = base.specification!.bodyParts.map((entry) =>
|
|
convertKey(entry.boneName)
|
|
);
|
|
baseObject.add(controller);
|
|
baseObject.position.copy(pos);
|
|
|
|
return baseObject;
|
|
};
|
|
|
|
export const getCharacterController = (object: Object3D) => {
|
|
return object.getObjectByName('Humanoid') as Humanoid;
|
|
};
|