UNPKG

anim-to-bvh

Version:

Anim to BVH converter(mostly for Second Life, including Bento bones). Anim, BVH parsers.

314 lines (235 loc) 9.15 kB
import {AnimData, BVHNode} from "./model"; import {toEulers, distributeValue} from "./utils"; export function parseAnim(arrayBuffer: ArrayBuffer): AnimData { const view = new DataView(arrayBuffer); let offset = 0; function readU16() { const value = view.getUint16(offset, true); offset += 2; return value; } function readU32() { const value = view.getUint16(offset, true); offset += 4; return value; } function readS32() { const value = view.getInt32(offset, true); offset += 4; return value; } function readF32() { const value = view.getFloat32(offset, true); offset += 4; return value; } function readString() { let str = ""; while (offset < view.byteLength) { const char = view.getUint8(offset++); if (char === 0) break; // NULL-terminated str += String.fromCharCode(char); } return str; } function readFixedString(size: number) { let str = ""; for (let i = 0; i < size; i++) { const char = view.getUint8(offset++); if (char !== 0) str += String.fromCharCode(char); } return str; } const version = readU16(); const sub_version = readU16(); const base_priority = readS32(); const duration = readF32(); const emote_name = readString(); const loop_in_point = readF32(); const loop_out_point = readF32(); const loop = readS32(); const ease_in_duration = readF32(); const ease_out_duration = readF32(); const hand_pose = readU32(); const num_joints = readU32(); const joints = []; for (let i = 0; i < num_joints; i++) { const joint_name = readString(); const joint_priority = readS32(); const num_rot_keys = readS32(); const rotation_keys = []; for (let j = 0; j < num_rot_keys; j++) { const time = readU16(); const rot_x = readU16(); const rot_y = readU16(); const rot_z = readU16(); rotation_keys.push({time: intToFloat(time, 0, duration), x: intToFloat(rot_x, -1, 1), y: intToFloat(rot_y, -1, 1), z: intToFloat(rot_z, -1, 1)}); } const num_pos_keys = readS32(); const position_keys = []; for (let j = 0; j < num_pos_keys; j++) { const time = readU16(); const pos_x = readU16(); const pos_y = readU16(); const pos_z = readU16(); position_keys.push({time: intToFloat(time, 0, duration), x: intToFloat(pos_x, -5, 5), y: intToFloat(pos_y, -5, 5), z: intToFloat(pos_z, -5, 5)}); } joints.push({ joint_name, joint_priority, rotation_keys, position_keys }); } const num_constraints = readS32(); const constraints = []; for (let i = 0; i < num_constraints; i++) { const chain_length = view.getUint8(offset++); const constraint_type = view.getUint8(offset++); const source_volume = readFixedString(16); const source_offset = [readF32(), readF32(), readF32()]; const target_volume = readFixedString(16); const target_offset = [readF32(), readF32(), readF32()]; const target_dir = [readF32(), readF32(), readF32()]; const ease_in_start = readF32(); const ease_in_stop = readF32(); const ease_out_start = readF32(); const ease_out_stop = readF32(); constraints.push({ chain_length, constraint_type, source_volume, source_offset, target_volume, target_offset, target_dir, ease_in_start, ease_in_stop, ease_out_start, ease_out_stop }); } joints.forEach((item: any) => item.rotation_keys.forEach((rot: any) => { if(!item.euler_keys) { item.euler_keys = []; } item.euler_keys.push(toEulers(rot)); })); return { version, sub_version, duration, emote_name, loop, joints, constraints }; } function intToFloat(val: number, min: number, max: number): number { const one = (max - min) / 65535.0; const result = min + val * one; if(Math.abs(result) < one) { return 0; } return result; } function enumerate(content: string, key: string, alter: string): string { let result: string = content; let count = 0; while(result.includes("\"" + key + "\"")) { result = result.replace("\"" + key + "\"", "\"" + alter + count + "\""); count += 1; } return result; } function compose(node: any, name: string): any { const children: any[] = []; const result: any = {}; let cnts: string[] = []; let jnts: string[] = []; if(Object.keys(node).includes("End Site")) { node.jnt1 = "end"; } Object.keys(node).forEach(item => { if(item.includes("cnt")) { cnts.push(item); return; } if(item.includes("jnt")) { jnts.push(item); } }); cnts = cnts.sort((val1, val2) => {return parseInt(val1.replace("cnt", "")) - parseInt(val2.replace("cnt", ""));}); jnts = jnts.sort((val1, val2) => {return parseInt(val1.replace("jnt", "")) - parseInt(val2.replace("jnt", ""));}); jnts.forEach((item, i) => children.push(compose(node[cnts[i]], node[item].trim()))); if(node.OFFSET) { const offset: any = node.OFFSET.trim().split(" ").filter((item: any) => !isNaN(parseFloat(item))).map(parseFloat); result.offset = {x: offset[0], y: offset[1], z: offset[2]}; } if(node.CHANNELS) { const channels: any = node.CHANNELS.trim().split(" ").map((item: any) => item.trim()).filter((item: any) => !!item).filter((item: any) => isNaN(parseFloat(item))); result.channels = channels; } result.bvhName = name; if(children.length) { result.children = children; } return result; } function cleanup(data: any) { data.jnt0 = data.ROOT; return compose(data, "root"); } function parseFrames(rows: string[]): number[][] { const splitedRows: string[][] = rows.map(item => item.split(" ").map(item => item.trim()).filter(item => !!item)); return splitedRows.map(item => item.map(parseFloat)); } function parseFramesPart(framesPart: string): {framesLength: number, frameDuration: number, frames: number[][]} { const framesRows: string[] = framesPart.split("\n"); let timeIndex: number = -1; for(let i = 0; i < framesRows.length; i++) { if(framesRows[i].toLowerCase().includes("time")) { timeIndex = i; break; } } if(timeIndex < 0) { return {framesLength: 0, frameDuration: 0, frames: []}; } const framesLength: number = <number>parseInt(framesRows[timeIndex - 1].split(" ").map((item: string) => item.trim()).filter((item: string) => !!item).filter((item: string) => !isNaN(<number><any>item))[0]); const frameDuration: number = <number>parseFloat(framesRows[timeIndex].split(" ").map((item: string) => item.trim()).filter((item: string) => !!item).filter((item: string) => !isNaN(<number><any>item))[0]); while(!framesRows[0].toLowerCase().includes("time")) { framesRows.shift(); } framesRows.shift(); const frames: number[][] = parseFrames(framesRows); return {framesLength, frameDuration, frames} } export function distributeSingleFrame(hierarchy: BVHNode, frame: number[]) { hierarchy.children?.toReversed().forEach((child: any) => distributeSingleFrame(child, frame)); const position = {x: 0, y: 0, z: 0}; const rotation = {x: 0, y: 0, z: 0}; hierarchy.channels?.toReversed().forEach((item: any) => { const value: number = <number><any>frame.pop(); distributeValue(position, rotation, item, value); }); if(!hierarchy.bvhFrames) { hierarchy.bvhFrames = []; } hierarchy.bvhFrames.push({position, rotation}); } export function parseBVH(text: string): BVHNode { const parts: string[] = text.replaceAll("\r", "").replaceAll("\t", " ").split("MOTION"); let result = parts[0].split("HIERARCHY")[1]; "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ".split("").forEach(item => { result = result.replaceAll(item + "\n", item + "\",\n"); }); result = result.replaceAll("JOINT","\"JOINT\":\""); result = result.replaceAll("OFFSET","\"OFFSET\":\""); result = result.replaceAll("CHANNELS","\"CHANNELS\":\""); result = result.replaceAll("ROOT","\"ROOT\":\""); result = result.replaceAll("End Site","\"End Site\":\""); result = result.replaceAll("{","\"content\": {"); result = result.split("}").map(item => { if(item.trim().endsWith(",")) { return item.trim() + "\"dummy\": {}"; } return item; }).join("}"); result = result.split("}").map(item => { if(item.trim().startsWith("\"JOINT\"")) { return "," + item.trim(); } return item; }).join("}"); let count = 0; result = enumerate(result, "JOINT", "jnt"); result = enumerate(result, "content", "cnt"); const hierarchy: BVHNode = cleanup(JSON.parse("{" + result + "}")).children[0]; const animation = parseFramesPart(parts[1]); hierarchy.bvhTimes = []; for(let i = 0; i < animation.framesLength; i++) { hierarchy.bvhTimes.push(animation.frameDuration * i); } animation.frames.forEach(item => distributeSingleFrame(hierarchy, item)); return hierarchy; }