import { UUID } from '../UUID'; import { Vector3 } from '../Vector3'; import { Vector2 } from '../Vector2'; import { Utils } from '../Utils'; import { Buffer } from 'buffer'; import type { LLSubMesh } from './interfaces/LLSubMesh'; import type { LLPhysicsConvex } from './interfaces/LLPhysicsConvex'; import { LLSD } from '../llsd/LLSD'; import { LLSDMap } from '../llsd/LLSDMap'; import { LLSDInteger } from '../llsd/LLSDInteger'; import { LLSDReal } from '../llsd/LLSDReal'; import type { LLSkin } from './interfaces/LLSkin'; import type { LLSDType } from '../llsd/LLSDType'; import { Matrix4 } from '../Matrix4'; export class LLMesh { public version?: number; public lodLevels: Record = {}; public physicsConvex?: LLPhysicsConvex; public physicsHavok?: { weldingData: Buffer, hullMassProps: { CoM: number[], inertia: number[], mass: number, volume: number }, meshDecompMassProps: { CoM: number[], inertia: number[], mass: number, volume: number } } public skin?: LLSkin; public creatorID?: UUID; public date?: Date; public costData?: { hull: number, hull_discounted_vertices: number, mesh: number[], mesh_triangles: number } public submodel_id?: number; public static async from(buf: Buffer): Promise { const llmesh = new LLMesh(); const metadata = { readPos: 0 }; const obj = LLSD.parseBinary(buf, metadata); if (!(obj instanceof LLSDMap)) { throw new Error('Invalid mesh'); } for(const key of obj.keys()) { switch(key) { case 'creator': { const u = obj[key]; if (u instanceof UUID) { llmesh.creatorID = u; } break; } case 'version': { const int = obj[key]; if (int instanceof LLSDInteger) { llmesh.version = int.valueOf(); } break; } case 'submodel_id': { const int = obj[key]; if (int instanceof LLSDInteger) { llmesh.submodel_id = int.valueOf(); } break; } case 'date': { const dt = obj[key]; if (dt instanceof Date) { llmesh.date = dt; } break; } case 'physics_cost_data': { const map = obj[key]; if (map instanceof LLSDMap) { llmesh.costData = { hull: 0, hull_discounted_vertices: 0, mesh: [], mesh_triangles: 0 } if (map.hull instanceof LLSDReal) { llmesh.costData.hull = map.hull.valueOf(); } if (map.hull_discounted_vertices instanceof LLSDInteger) { llmesh.costData.hull_discounted_vertices = map.hull_discounted_vertices.valueOf(); } if (Array.isArray(map.mesh)) { for(const num of map.mesh) { if ((num as unknown) instanceof LLSDReal) { llmesh.costData.mesh.push(num.valueOf()); } } } if (map.mesh_triangles instanceof LLSDInteger) { llmesh.costData.mesh_triangles = map.mesh_triangles.valueOf(); } } break; } case 'physics_shape': case 'physics_mesh': case 'high_lod': case 'medium_lod': case 'low_lod': case 'lowest_lod': case 'physics_convex': case 'physics_havok': case 'skin': { const skin = obj[key]; if (skin instanceof LLSDMap) { const hash = skin.get('hash'); const offset = skin.get('offset'); const size = skin.get('size'); if (offset instanceof LLSDInteger && size instanceof LLSDInteger) { const offsetVal = offset.valueOf(); const sizeVal = size.valueOf(); const startPos = metadata.readPos + offsetVal; const endPos = offsetVal + sizeVal + metadata.readPos; const bufSlice = buf.subarray(startPos, endPos); if (hash instanceof Buffer) { const inflatedHash = Utils.MD5String(bufSlice); if (inflatedHash !== hash.toString('hex')) { throw new Error('Hash mismatch'); } } const inflated = await Utils.inflate(bufSlice); const parsed = LLSD.parseBinary(inflated); if (key === 'physics_havok') { if (parsed instanceof LLSDMap) { llmesh.physicsHavok = { weldingData: Buffer.alloc(0), hullMassProps: { CoM: [], inertia: [], mass: 0, volume: 0 }, meshDecompMassProps: { CoM: [], inertia: [], mass: 0, volume: 0 } } if (parsed.HullMassProps instanceof LLSDMap) { if (Array.isArray(parsed.HullMassProps.CoM)) { for(const num of parsed.HullMassProps.CoM) { if ((num as unknown) instanceof LLSDReal) { llmesh.physicsHavok.hullMassProps.CoM.push(num.valueOf()); } } } if (Array.isArray(parsed.HullMassProps.inertia)) { for(const num of parsed.HullMassProps.inertia) { if ((num as unknown) instanceof LLSDReal) { llmesh.physicsHavok.hullMassProps.inertia.push(num.valueOf()); } } } if (parsed.HullMassProps.mass instanceof LLSDReal) { llmesh.physicsHavok.hullMassProps.mass = parsed.HullMassProps.mass.valueOf(); } if (parsed.HullMassProps.volume instanceof LLSDReal) { llmesh.physicsHavok.hullMassProps.volume = parsed.HullMassProps.volume.valueOf(); } } if (parsed.MeshDecompMassProps instanceof LLSDMap) { if (Array.isArray(parsed.MeshDecompMassProps.CoM)) { for(const num of parsed.MeshDecompMassProps.CoM) { if ((num as unknown) instanceof LLSDReal) { llmesh.physicsHavok.meshDecompMassProps.CoM.push(num.valueOf()); } } } if (Array.isArray(parsed.MeshDecompMassProps.inertia)) { for(const num of parsed.MeshDecompMassProps.inertia) { if ((num as unknown) instanceof LLSDReal) { llmesh.physicsHavok.meshDecompMassProps.inertia.push(num.valueOf()); } } } if (parsed.MeshDecompMassProps.mass instanceof LLSDReal) { llmesh.physicsHavok.meshDecompMassProps.mass = parsed.MeshDecompMassProps.mass.valueOf(); } if (parsed.MeshDecompMassProps.volume instanceof LLSDReal) { llmesh.physicsHavok.meshDecompMassProps.volume = parsed.MeshDecompMassProps.volume.valueOf(); } } if (parsed.WeldingData instanceof Buffer) { llmesh.physicsHavok.weldingData = parsed.WeldingData; } } } else if (key === 'skin') { if (parsed instanceof LLSDMap) { llmesh.skin = this.parseSkin(parsed); } } else if (key === 'physics_convex') { if (parsed instanceof LLSDMap) { llmesh.physicsConvex = this.parsePhysicsConvex(parsed); } } else { if (Array.isArray(parsed)) { const subMeshes: LLSDMap[] = []; for (const sm of parsed) { if (sm instanceof LLSDMap) { subMeshes.push(sm); } } llmesh.lodLevels[key] = this.parseLODLevel(subMeshes) } } } } break; } default: { console.warn('Unrecognised mesh property: ' + key); } } } return llmesh; } public async toAsset(): Promise { const llsd = new LLSDMap(); if (this.creatorID) { llsd.add('creator', this.creatorID); } if (this.version !== undefined) { llsd.add('version', new LLSDInteger(this.version)); } if (this.submodel_id !== undefined) { llsd.add('submodel_id', new LLSDInteger(this.submodel_id)); } if (this.date !== undefined) { llsd.add('date', this.date); } let offset = 0; const bufs = []; for (const lod of Object.keys(this.lodLevels)) { const lodBlob = await this.encodeLODLevel(lod, this.lodLevels[lod]); llsd.add(lod, new LLSDMap([ ['offset', new LLSDInteger(offset)], ['size', new LLSDInteger(lodBlob.length)] ])); offset += lodBlob.length; bufs.push(lodBlob); } if (this.costData) { llsd.add('physics_cost_data', new LLSDMap([ ['hull', new LLSDReal(this.costData.hull)], ['hull_discounted_vertices', new LLSDInteger(this.costData.hull_discounted_vertices)], ['mesh', LLMesh.toLLSDReal(this.costData.mesh)], ['mesh_triangles', new LLSDInteger(this.costData.mesh_triangles)] ])); } if (this.physicsHavok) { const physHavok = await this.encodePhysicsHavok(); llsd.add('physics_havok', new LLSDMap([ ['offset', new LLSDInteger(offset)], ['size', new LLSDInteger(physHavok.length)] ])); offset += physHavok.length; bufs.push(physHavok); } if (this.physicsConvex) { const physBlob = await this.encodePhysicsConvex(this.physicsConvex); llsd.add('physics_convex', new LLSDMap([ ['offset', new LLSDInteger(offset)], ['size', new LLSDInteger(physBlob.length)] ])); offset += physBlob.length; bufs.push(physBlob); } if (this.skin) { const skinBlob = await this.encodeSkin(this.skin); llsd.add('skin', new LLSDMap([ ['offset', new LLSDInteger(offset)], ['size', new LLSDInteger(skinBlob.length)] ])); bufs.push(skinBlob); } bufs.unshift(LLSD.toBinary(llsd)); return Buffer.concat(bufs); } private static parseSkin(mesh: LLSDMap): LLSkin { const skin: LLSkin = { jointNames: [], bindShapeMatrix: new Matrix4(), inverseBindMatrix: [] }; if (Array.isArray(mesh.joint_names)) { for(const joint of mesh.joint_names) { if (typeof (joint as unknown) === 'string') { skin.jointNames.push(joint); } } } if (Array.isArray(mesh.bind_shape_matrix)) { const params = []; for(const num of mesh.bind_shape_matrix) { if ((num as unknown) instanceof LLSDReal) { params.push(num.valueOf()); } } skin.bindShapeMatrix = new Matrix4(params); } if (Array.isArray(mesh.inverse_bind_matrix)) { skin.inverseBindMatrix = []; for (const inv of mesh.inverse_bind_matrix) { const mtrx: number[] = []; if (Array.isArray(inv)) { for(const num of inv) { if ((num as unknown) instanceof LLSDReal) { mtrx.push(num.valueOf()); } } } skin.inverseBindMatrix.push(new Matrix4(mtrx)); } } if (Array.isArray(mesh.alt_inverse_bind_matrix)) { skin.altInverseBindMatrix = []; for (const inv of mesh.alt_inverse_bind_matrix) { const mtrx: number[] = []; if (Array.isArray(inv)) { for(const num of inv) { if ((num as unknown) instanceof LLSDReal) { mtrx.push(num.valueOf()); } } } skin.altInverseBindMatrix.push(new Matrix4(mtrx)); } } if (Array.isArray(mesh.pelvis_offset)) { const mtrx: number[] = []; for(const num of mesh.pelvis_offset) { if ((num as unknown) instanceof LLSDReal) { mtrx.push(num.valueOf()); } } skin.pelvisOffset = new Matrix4(mtrx); } return skin; } private static fixReal(arr: number[]): number[] { const newArr = []; for (let num of arr) { if ((num >> 0) === num && !((num === 0) && ((1 / num) === -Infinity))) { num += 0.0000000001; } newArr.push(num); } return newArr; } private static fixRealLLSD(arr: number[]): LLSDReal[] { const newArr: LLSDReal[]= []; for (let num of arr) { if ((num >> 0) === num && !((num === 0) && ((1 / num) === -Infinity))) { num += 0.0000000001; } newArr.push(new LLSDReal(num)); } return newArr; } private static parsePhysicsConvex(mesh: LLSDMap): LLPhysicsConvex { const conv: LLPhysicsConvex = { boundingVerts: undefined, domain: { min: new Vector3([-0.5, -0.5, -0.5]), max: new Vector3([0.5, 0.5, 0.5]) } }; if (Array.isArray(mesh.Min)) { conv.domain.min.x = mesh.Min[0].valueOf(); conv.domain.min.y = mesh.Min[1].valueOf(); conv.domain.min.z = mesh.Min[2].valueOf(); } if (Array.isArray(mesh.Max)) { conv.domain.max.x = mesh.Max[0].valueOf(); conv.domain.max.y = mesh.Max[1].valueOf(); conv.domain.max.z = mesh.Max[2].valueOf(); } if (mesh.HullList instanceof Buffer) { if (!(mesh.Positions instanceof Buffer)) { throw new Error('Positions must be supplied if hull list is present'); } conv.positions = this.decodeByteDomain3(mesh.Positions, conv.domain.min, conv.domain.max); conv.hullList = Array.from(mesh.HullList); let totalPoints = 0; for (const hull of conv.hullList) { totalPoints += hull; } if (conv.positions.length !== totalPoints) { throw new Error('Hull list expected number of points does not match number of positions: ' + totalPoints + ' vs ' + conv.positions.length); } } if (mesh.BoundingVerts instanceof Buffer) { conv.boundingVerts = this.decodeByteDomain3(mesh.BoundingVerts, conv.domain.min, conv.domain.max); } return conv; } private static parseLODLevel(mesh: LLSDMap[]): LLSubMesh[] { const list: LLSubMesh[] = []; for (const submesh of mesh) { const decoded: LLSubMesh = { positionDomain: { min: new Vector3([-0.5, -0.5, -0.5]), max: new Vector3([0.5, 0.5, 0.5]) } }; if (submesh.NoGeometry !== undefined) { decoded.noGeometry = true; list.push(decoded); } else { decoded.position = []; if (!(submesh.Position instanceof Buffer)) { throw new Error('Submesh does not contain position data'); } if (decoded.positionDomain !== undefined) { if (submesh.PositionDomain instanceof LLSDMap) { if (Array.isArray(submesh.PositionDomain.Max)) { const dom = submesh.PositionDomain.Max; if (dom[0] instanceof LLSDReal) { decoded.positionDomain.max.x = dom[0].valueOf(); } if (dom[1] instanceof LLSDReal) { decoded.positionDomain.max.y = dom[1].valueOf(); } if (dom[2] instanceof LLSDReal) { decoded.positionDomain.max.z = dom[2].valueOf(); } } if (Array.isArray(submesh.PositionDomain.Min)) { const dom = submesh.PositionDomain.Min; if (dom[0] instanceof LLSDReal) { decoded.positionDomain.min.x = dom[0].valueOf(); } if (dom[1] instanceof LLSDReal) { decoded.positionDomain.min.y = dom[1].valueOf(); } if (dom[2] instanceof LLSDReal) { decoded.positionDomain.min.z = dom[2].valueOf(); } } } decoded.position = this.decodeByteDomain3(submesh.Position, decoded.positionDomain.min, decoded.positionDomain.max); } if (submesh.Normal instanceof Buffer) { decoded.normal = this.decodeByteDomain3(submesh.Normal, new Vector3([-1.0, -1.0, -1.0]), new Vector3([1.0, 1.0, 1.0])); if (decoded.normal.length !== decoded.position.length) { throw new Error('Normal length does not match vertex position length'); } } if (submesh.TexCoord0 !== undefined) { decoded.texCoord0Domain = { min: new Vector2([-0.5, -0.5]), max: new Vector2([0.5, 0.5]) }; if (submesh.TexCoord0Domain instanceof LLSDMap) { if (submesh.TexCoord0Domain.Max !== undefined) { const dom = submesh.TexCoord0Domain.Max; if (Array.isArray(dom)) { if (dom[0] instanceof LLSDReal) { decoded.texCoord0Domain.max.x = dom[0].valueOf(); } else { throw new Error('Unexpected type'); } if (dom[1] instanceof LLSDReal) { decoded.texCoord0Domain.max.y = dom[1].valueOf(); } else { throw new Error('Unexpected type'); } } } if (Array.isArray(submesh.TexCoord0Domain.Min)) { const dom = submesh.TexCoord0Domain.Min; if (dom[0] instanceof LLSDReal) { decoded.texCoord0Domain.min.x = dom[0].valueOf(); } else { throw new Error('Unexpected type'); } if (dom[1] instanceof LLSDReal) { decoded.texCoord0Domain.min.y = dom[1].valueOf(); } else { throw new Error('Unexpected type'); } } } else { throw new Error('TexCoord0Domain is required if Texcoord0 is present'); } if (submesh.TexCoord0 instanceof Buffer) { decoded.texCoord0 = this.decodeByteDomain2(submesh.TexCoord0, decoded.texCoord0Domain.min, decoded.texCoord0Domain.max); } } if (!(submesh.TriangleList instanceof Buffer)) { throw new Error('TriangleList is required'); } const indexBuf = Buffer.from(submesh.TriangleList); decoded.triangleList = []; for (let pos = 0; pos < indexBuf.length; pos = pos + 2) { const vertIndex = indexBuf.readUInt16LE(pos); if (vertIndex >= decoded.position.length) { throw new Error('Vertex index out of range: ' + vertIndex) } decoded.triangleList.push(vertIndex); } if (submesh.Weights instanceof Buffer) { const skinBuf = submesh.Weights; decoded.weights = []; let pos = 0; while (pos < skinBuf.length) { const entry: Record = {}; for (let x = 0; x < 4; x++) { const jointNum = skinBuf.readUInt8(pos++); if (jointNum === 0xFF) { break; } const value = skinBuf.readUInt16LE(pos); pos = pos + 2; entry[jointNum] = value; } decoded.weights.push(entry); } if (decoded.weights.length !== decoded.position.length) { throw new Error('Weight list differs in length from position list'); } } list.push(decoded); } } return list; } private static decodeByteDomain3(buf: Buffer, minDomain: Vector3, maxDomain: Vector3): Vector3[] { const result: Vector3[] = []; for (let idx = 0; idx < buf.length; idx = idx + 6) { const posX = Utils.UInt16ToFloat(buf.readUInt16LE(idx), minDomain.x, maxDomain.x, false); const posY = Utils.UInt16ToFloat(buf.readUInt16LE(idx + 2), minDomain.y, maxDomain.y, false); const posZ = Utils.UInt16ToFloat(buf.readUInt16LE(idx + 4), minDomain.z, maxDomain.z, false); result.push(new Vector3([posX, posY, posZ])); } return result; } private static decodeByteDomain2(buf: Buffer, minDomain: Vector2, maxDomain: Vector2): Vector2[] { const result: Vector2[] = []; for (let idx = 0; idx < buf.length; idx = idx + 4) { const posX = Utils.UInt16ToFloat(buf.readUInt16LE(idx), minDomain.x, maxDomain.x, false); const posY = Utils.UInt16ToFloat(buf.readUInt16LE(idx + 2), minDomain.y, maxDomain.y, false); result.push(new Vector2([posX, posY])); } return result; } private static toLLSDReal(num: number[]): LLSDReal[] { const real: LLSDReal[] = []; for(const n of num) { real.push(new LLSDReal(n)); } return real; } private encodeSubMesh(mesh: LLSubMesh): LLSDType { const data = new LLSDMap(); if (mesh.noGeometry === true) { data.add('NoGeometry', true); return data; } if (!mesh.position) { throw new Error('No position data when encoding submesh'); } if (mesh.positionDomain !== undefined) { data.add('Position', this.expandFromDomain(mesh.position, mesh.positionDomain.min, mesh.positionDomain.max)); const min = new Vector3(LLMesh.fixReal(mesh.positionDomain.min.toArray())); const max = new Vector3(LLMesh.fixReal(mesh.positionDomain.max.toArray())); data.add('PositionDomain', new LLSDMap([ ['Min', [new LLSDReal(min.x), new LLSDReal(min.y), new LLSDReal(min.z)]], ['Max', [new LLSDReal(max.x), new LLSDReal(max.y), new LLSDReal(max.z)]] ])); } if (mesh.texCoord0 && mesh.texCoord0Domain !== undefined) { data.add('TexCoord0', this.expandFromDomain(mesh.texCoord0, mesh.texCoord0Domain.min, mesh.texCoord0Domain.max)); const domainMin = new Vector2(LLMesh.fixReal(mesh.texCoord0Domain.min.toArray())); const domainMax = new Vector2(LLMesh.fixReal(mesh.texCoord0Domain.max.toArray())); data.add('TexCoord0Domain', new LLSDMap([ ['Min', [new LLSDReal(domainMin.x), new LLSDReal(domainMin.y)]], ['Max', [new LLSDReal(domainMax.x), new LLSDReal(domainMax.y)]] ])); } if (mesh.normal) { data.add('Normal', this.expandFromDomain(mesh.normal, new Vector3([-1.0, -1.0, -1.0]), new Vector3([1.0, 1.0, 1.0]))); } if (mesh.triangleList) { const triangles = Buffer.allocUnsafe(mesh.triangleList.length * 2); let pos = 0; for(const triangle of mesh.triangleList) { triangles.writeUInt16LE(triangle, pos); pos = pos + 2; } data.add('TriangleList', triangles); } else { throw new Error('Triangle list is required'); } if (mesh.weights) { // Calculate how much space we need let spaceNeeded = 0; for (const weight of mesh.weights) { const keys = Object.keys(weight); spaceNeeded = spaceNeeded + keys.length * 3; if (keys.length < 4) { spaceNeeded = spaceNeeded + 1; } } const weightBuff = Buffer.allocUnsafe(spaceNeeded); let pos = 0; for (const weight of mesh.weights) { const keys = Object.keys(weight); for (const jointID of keys) { weightBuff.writeUInt8(parseInt(jointID, 10), pos++); weightBuff.writeUInt16LE(weight[parseInt(jointID, 10)], pos); pos = pos + 2; } if (keys.length < 4) { weightBuff.writeUInt8(0xFF, pos++); } } data.add('Weights', weightBuff); } return data; } private expandFromDomain(data: Vector3[] | Vector2[], domainMin: Vector3 | Vector2, domainMax: Vector3 | Vector2): Buffer { let length = 4; if (data.length > 0 && data[0] instanceof Vector3) { length = 6; } const buf = Buffer.allocUnsafe(data.length * length); let pos = 0; for (const c of data) { const coord: Vector3 | Vector2 = c; const sizeX = domainMax.x - domainMin.x; const newX = Math.round(((coord.x - domainMin.x) / sizeX) * 65535); const sizeY = domainMax.y - domainMin.y; const newY = Math.round(((coord.y - domainMin.y) / sizeY) * 65535); buf.writeUInt16LE(newX, pos); pos = pos + 2; buf.writeUInt16LE(newY, pos); pos = pos + 2; if (coord instanceof Vector3 && domainMin instanceof Vector3 && domainMax instanceof Vector3) { const sizeZ = domainMax.z - domainMin.z; const newZ = Math.round(((coord.z - domainMin.z) / sizeZ) * 65535); buf.writeUInt16LE(newZ, pos); pos = pos + 2; } } return buf; } private async encodeLODLevel(_: string, submeshes: LLSubMesh[]): Promise { const smList: LLSDType[] = []; for (const sub of submeshes) { smList.push(this.encodeSubMesh(sub)) } return Utils.deflate(LLSD.toBinary(smList)); } private async encodePhysicsHavok(): Promise { if (!this.physicsHavok) { return Buffer.alloc(0); } return Utils.deflate(LLSD.toBinary(new LLSDMap([ ['WeldingData', this.physicsHavok.weldingData], ['HullMassProps', new LLSDMap([ ['CoM', LLMesh.toLLSDReal(this.physicsHavok.hullMassProps.CoM)], ['inertia', LLMesh.toLLSDReal(this.physicsHavok.hullMassProps.inertia)], ['mass', new LLSDReal(this.physicsHavok.hullMassProps.mass)], ['volume', new LLSDReal(this.physicsHavok.hullMassProps.volume)] ])], ['MeshDecompMassProps', new LLSDMap([ ['CoM', LLMesh.toLLSDReal(this.physicsHavok.meshDecompMassProps.CoM)], ['inertia', LLMesh.toLLSDReal(this.physicsHavok.meshDecompMassProps.inertia)], ['mass', new LLSDReal(this.physicsHavok.meshDecompMassProps.mass)], ['volume', new LLSDReal(this.physicsHavok.meshDecompMassProps.volume)] ])] ]))); } private async encodePhysicsConvex(conv: LLPhysicsConvex): Promise { const llsd = new LLSDMap(); llsd.add('Min', LLMesh.fixRealLLSD(conv.domain.min.toArray())); llsd.add('Max', LLMesh.fixRealLLSD(conv.domain.max.toArray())); const sizeX = conv.domain.max.x - conv.domain.min.x; const sizeY = conv.domain.max.y - conv.domain.min.y; const sizeZ = conv.domain.max.z - conv.domain.min.z; if (conv.hullList) { if (!conv.positions) { throw new Error('Positions must be present if hullList is set.') } llsd.add('HullList', Buffer.from(conv.hullList)); const buf = Buffer.allocUnsafe(conv.positions.length * 6); let pos = 0; for (const vec of conv.positions) { buf.writeUInt16LE(Math.round(((vec.x - conv.domain.min.x) / sizeX) * 65535), pos); pos = pos + 2; buf.writeUInt16LE(Math.round(((vec.y - conv.domain.min.y) / sizeY) * 65535), pos); pos = pos + 2; buf.writeUInt16LE(Math.round(((vec.z - conv.domain.min.z) / sizeZ) * 65535), pos); pos = pos + 2; } llsd.add('Positions', buf); } { if(conv.boundingVerts) { const buf = Buffer.allocUnsafe(conv.boundingVerts.length * 6); let pos = 0; for (const vec of conv.boundingVerts) { buf.writeUInt16LE(Math.round(((vec.x - conv.domain.min.x) / sizeX) * 65535), pos); pos = pos + 2; buf.writeUInt16LE(Math.round(((vec.y - conv.domain.min.y) / sizeY) * 65535), pos); pos = pos + 2; buf.writeUInt16LE(Math.round(((vec.z - conv.domain.min.z) / sizeZ) * 65535), pos); pos = pos + 2; } llsd.add('BoundingVerts', buf); } } return Utils.deflate(LLSD.toBinary(llsd)); } private async encodeSkin(skin: LLSkin): Promise { const llsd = new LLSDMap(); llsd.add('joint_names', skin.jointNames); llsd.add('bind_shape_matrix', LLMesh.toLLSDReal(skin.bindShapeMatrix.toArray())); const inverseBindMatrix: LLSDType[] = []; for (const matrix of skin.inverseBindMatrix) { inverseBindMatrix.push(LLMesh.toLLSDReal(matrix.toArray())) } llsd.add('inverse_bind_matrix', inverseBindMatrix); if (skin.altInverseBindMatrix) { const altInverseBindMatrix: LLSDType[] = []; for (const matrix of skin.altInverseBindMatrix) { altInverseBindMatrix.push(LLMesh.toLLSDReal(matrix.toArray())) } llsd.add('alt_inverse_bind_matrix', altInverseBindMatrix); } if (skin.pelvisOffset) { llsd.add('pelvis_offset', LLMesh.toLLSDReal(skin.pelvisOffset.toArray())); } return Utils.deflate(LLSD.toBinary(llsd)); } }