forked from phenom/healer-man
Release Healer Man 0.1.12
This commit is contained in:
1 parent
13970d03c8
commit
65e24f644e
201 files changed
+5061
-123
No files matched your search
@@ -0,0 +1,297 @@
|
||||
const M2_HEADER_SIZE = 304;
|
||||
const M2_VERTEX_SIZE = 48;
|
||||
const M2_TEXTURE_SIZE = 16;
|
||||
const M2_RENDER_FLAG_SIZE = 4;
|
||||
const SKIN_HEADER_SIZE = 48;
|
||||
const SKIN_SUBMESH_SIZE = 48;
|
||||
const SKIN_TEXTURE_UNIT_SIZE = 24;
|
||||
|
||||
const VALUE_TYPES = {
|
||||
uint8x4: {
|
||||
byteLength: 4,
|
||||
read: (buffer, offset) => Array.from(buffer.subarray(offset, offset + 4)),
|
||||
},
|
||||
uint16: { byteLength: 2, read: (buffer, offset) => buffer.readUInt16LE(offset) },
|
||||
int16: { byteLength: 2, read: (buffer, offset) => buffer.readInt16LE(offset) },
|
||||
};
|
||||
|
||||
function asBuffer(value) {
|
||||
return Buffer.isBuffer(value) ? value : Buffer.from(value);
|
||||
}
|
||||
|
||||
function assertRange(buffer, offset, byteLength, label) {
|
||||
if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(byteLength)
|
||||
|| offset < 0 || byteLength < 0 || offset > buffer.length - byteLength) {
|
||||
throw new Error(`${label} points outside its source buffer.`);
|
||||
}
|
||||
}
|
||||
|
||||
function readArrayHeader(buffer, offset, label) {
|
||||
assertRange(buffer, offset, 8, `${label} descriptor`);
|
||||
return { count: buffer.readUInt32LE(offset), offset: buffer.readUInt32LE(offset + 4) };
|
||||
}
|
||||
|
||||
function decodeArray(buffer, descriptorOffset, valueType, label) {
|
||||
const descriptor = readArrayHeader(buffer, descriptorOffset, label);
|
||||
const byteLength = descriptor.count * valueType.byteLength;
|
||||
if (!Number.isSafeInteger(byteLength)) throw new Error(`${label} is too large to decode.`);
|
||||
if (descriptor.count) assertRange(buffer, descriptor.offset, byteLength, label);
|
||||
return {
|
||||
...descriptor,
|
||||
values: Array.from({ length: descriptor.count }, (_, index) => (
|
||||
valueType.read(buffer, descriptor.offset + index * valueType.byteLength)
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
function decodeString(buffer, descriptorOffset, label) {
|
||||
const descriptor = readArrayHeader(buffer, descriptorOffset, label);
|
||||
if (!descriptor.count) return "";
|
||||
assertRange(buffer, descriptor.offset, descriptor.count, label);
|
||||
const bytes = buffer.subarray(descriptor.offset, descriptor.offset + descriptor.count);
|
||||
const terminator = bytes.indexOf(0);
|
||||
return bytes.toString("utf8", 0, terminator < 0 ? bytes.length : terminator);
|
||||
}
|
||||
|
||||
function vector2(buffer, offset) {
|
||||
return [buffer.readFloatLE(offset), buffer.readFloatLE(offset + 4)];
|
||||
}
|
||||
|
||||
function vector3(buffer, offset) {
|
||||
return [buffer.readFloatLE(offset), buffer.readFloatLE(offset + 4), buffer.readFloatLE(offset + 8)];
|
||||
}
|
||||
|
||||
function decodeVertices(buffer, name) {
|
||||
const count = buffer.readUInt32LE(60);
|
||||
const offset = buffer.readUInt32LE(64);
|
||||
const byteLength = count * M2_VERTEX_SIZE;
|
||||
if (!Number.isSafeInteger(byteLength)) throw new Error(`${name} vertices are too large to decode.`);
|
||||
if (count) assertRange(buffer, offset, byteLength, `${name} vertices`);
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const vertexOffset = offset + index * M2_VERTEX_SIZE;
|
||||
const boneWeights = VALUE_TYPES.uint8x4.read(buffer, vertexOffset + 12);
|
||||
return {
|
||||
position: vector3(buffer, vertexOffset),
|
||||
boneWeights,
|
||||
normalizedBoneWeights: boneWeights.map((weight) => weight / 255),
|
||||
boneIndices: VALUE_TYPES.uint8x4.read(buffer, vertexOffset + 16),
|
||||
normal: vector3(buffer, vertexOffset + 20),
|
||||
uv: vector2(buffer, vertexOffset + 32),
|
||||
uv2: vector2(buffer, vertexOffset + 40),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function decodeTextures(buffer, name) {
|
||||
const count = buffer.readUInt32LE(80);
|
||||
const offset = buffer.readUInt32LE(84);
|
||||
const byteLength = count * M2_TEXTURE_SIZE;
|
||||
if (!Number.isSafeInteger(byteLength)) throw new Error(`${name} textures are too large to decode.`);
|
||||
if (count) assertRange(buffer, offset, byteLength, `${name} texture records`);
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const textureOffset = offset + index * M2_TEXTURE_SIZE;
|
||||
return {
|
||||
type: buffer.readUInt32LE(textureOffset),
|
||||
flags: buffer.readUInt32LE(textureOffset + 4),
|
||||
filename: decodeString(buffer, textureOffset + 8, `${name} texture ${index} filename`),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function decodeMaterials(buffer, name) {
|
||||
const count = buffer.readUInt32LE(112);
|
||||
const offset = buffer.readUInt32LE(116);
|
||||
const byteLength = count * M2_RENDER_FLAG_SIZE;
|
||||
if (!Number.isSafeInteger(byteLength)) throw new Error(`${name} materials are too large to decode.`);
|
||||
if (count) assertRange(buffer, offset, byteLength, `${name} material records`);
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const materialOffset = offset + index * M2_RENDER_FLAG_SIZE;
|
||||
return {
|
||||
flags: buffer.readUInt16LE(materialOffset),
|
||||
blendMode: buffer.readUInt16LE(materialOffset + 2),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function decodeSkinSubmeshes(buffer, count, offset, triangleCount, vertexLookupCount, name) {
|
||||
const byteLength = count * SKIN_SUBMESH_SIZE;
|
||||
if (!Number.isSafeInteger(byteLength)) throw new Error(`${name} submeshes are too large to decode.`);
|
||||
if (count) assertRange(buffer, offset, byteLength, `${name} submesh records`);
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const submeshOffset = offset + index * SKIN_SUBMESH_SIZE;
|
||||
const vertexStart = buffer.readUInt16LE(submeshOffset + 4);
|
||||
const vertexCount = buffer.readUInt16LE(submeshOffset + 6);
|
||||
const indexStart = buffer.readUInt16LE(submeshOffset + 8);
|
||||
const indexCount = buffer.readUInt16LE(submeshOffset + 10);
|
||||
if (vertexStart + vertexCount > vertexLookupCount) {
|
||||
throw new Error(`${name} submesh ${index} vertex range exceeds the vertex lookup.`);
|
||||
}
|
||||
if (indexStart + indexCount > triangleCount) {
|
||||
throw new Error(`${name} submesh ${index} index range exceeds the triangle lookup.`);
|
||||
}
|
||||
return {
|
||||
id: buffer.readUInt16LE(submeshOffset),
|
||||
level: buffer.readUInt16LE(submeshOffset + 2),
|
||||
vertexStart,
|
||||
vertexCount,
|
||||
indexStart,
|
||||
indexCount,
|
||||
boneCount: buffer.readUInt16LE(submeshOffset + 12),
|
||||
boneComboIndex: buffer.readUInt16LE(submeshOffset + 14),
|
||||
boneInfluences: buffer.readUInt16LE(submeshOffset + 16),
|
||||
centerBoneIndex: buffer.readUInt16LE(submeshOffset + 18),
|
||||
center: vector3(buffer, submeshOffset + 20),
|
||||
sortCenter: vector3(buffer, submeshOffset + 32),
|
||||
sortRadius: buffer.readFloatLE(submeshOffset + 44),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function lookupSlice(lookup, start, count, label) {
|
||||
if (start + count > lookup.length) throw new Error(`${label} exceeds its M2 lookup table.`);
|
||||
return lookup.slice(start, start + count);
|
||||
}
|
||||
|
||||
function decodeSkinTextureUnits(buffer, count, offset, context, name) {
|
||||
const byteLength = count * SKIN_TEXTURE_UNIT_SIZE;
|
||||
if (!Number.isSafeInteger(byteLength)) throw new Error(`${name} texture units are too large to decode.`);
|
||||
if (count) assertRange(buffer, offset, byteLength, `${name} texture unit records`);
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const unitOffset = offset + index * SKIN_TEXTURE_UNIT_SIZE;
|
||||
const submeshIndex = buffer.readUInt16LE(unitOffset + 4);
|
||||
const materialIndex = buffer.readUInt16LE(unitOffset + 10);
|
||||
const textureCount = buffer.readUInt16LE(unitOffset + 14);
|
||||
const textureComboIndex = buffer.readUInt16LE(unitOffset + 16);
|
||||
const textureCoordinateComboIndex = buffer.readUInt16LE(unitOffset + 18);
|
||||
const textureWeightComboIndex = buffer.readUInt16LE(unitOffset + 20);
|
||||
const textureTransformComboIndex = buffer.readUInt16LE(unitOffset + 22);
|
||||
if (submeshIndex >= context.submeshes.length) {
|
||||
throw new Error(`${name} texture unit ${index} references missing submesh ${submeshIndex}.`);
|
||||
}
|
||||
if (materialIndex >= context.materials.length) {
|
||||
throw new Error(`${name} texture unit ${index} references missing material ${materialIndex}.`);
|
||||
}
|
||||
const textureIndices = lookupSlice(
|
||||
context.lookups.textures, textureComboIndex, textureCount, `${name} texture unit ${index} textures`,
|
||||
);
|
||||
for (const textureIndex of textureIndices) {
|
||||
if (textureIndex >= context.textures.length) {
|
||||
throw new Error(`${name} texture unit ${index} references missing texture ${textureIndex}.`);
|
||||
}
|
||||
}
|
||||
const submesh = context.submeshes[submeshIndex];
|
||||
return {
|
||||
flags: buffer.readUInt8(unitOffset),
|
||||
priorityPlane: buffer.readInt8(unitOffset + 1),
|
||||
shaderId: buffer.readUInt16LE(unitOffset + 2),
|
||||
submeshIndex,
|
||||
geosetIndex: buffer.readUInt16LE(unitOffset + 6),
|
||||
colorIndex: buffer.readInt16LE(unitOffset + 8),
|
||||
materialIndex,
|
||||
materialLayer: buffer.readUInt16LE(unitOffset + 12),
|
||||
textureCount,
|
||||
textureComboIndex,
|
||||
textureCoordinateComboIndex,
|
||||
textureWeightComboIndex,
|
||||
textureTransformComboIndex,
|
||||
textureIndices,
|
||||
textureFilenames: textureIndices.map((textureIndex) => context.textures[textureIndex].filename),
|
||||
textureCoordinateIndices: lookupSlice(
|
||||
context.lookups.textureCoordinates,
|
||||
textureCoordinateComboIndex,
|
||||
textureCount,
|
||||
`${name} texture unit ${index} texture coordinates`,
|
||||
),
|
||||
textureWeightIndices: lookupSlice(
|
||||
context.lookups.textureWeights,
|
||||
textureWeightComboIndex,
|
||||
textureCount,
|
||||
`${name} texture unit ${index} texture weights`,
|
||||
),
|
||||
textureTransformIndices: lookupSlice(
|
||||
context.lookups.textureTransforms,
|
||||
textureTransformComboIndex,
|
||||
textureCount,
|
||||
`${name} texture unit ${index} texture transforms`,
|
||||
),
|
||||
drawRange: { indexStart: submesh.indexStart, indexCount: submesh.indexCount },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Decode a browser-neutral static mesh descriptor from WotLK 3.3.5 MD20 and SKIN data. */
|
||||
export function decodeWotlkM2Mesh(m2Source, skinSource, name = "M2 mesh") {
|
||||
const m2 = asBuffer(m2Source);
|
||||
const skin = asBuffer(skinSource);
|
||||
if (m2.length < M2_HEADER_SIZE || m2.toString("ascii", 0, 4) !== "MD20" || m2.readUInt32LE(4) !== 264) {
|
||||
throw new Error(`${name} is not a supported WotLK 3.3.5 MD20.`);
|
||||
}
|
||||
if (skin.length < SKIN_HEADER_SIZE || skin.toString("ascii", 0, 4) !== "SKIN") {
|
||||
throw new Error(`${name} has no supported WotLK SKIN profile.`);
|
||||
}
|
||||
|
||||
const vertices = decodeVertices(m2, name);
|
||||
const textures = decodeTextures(m2, name);
|
||||
const materials = decodeMaterials(m2, name);
|
||||
const lookups = {
|
||||
textures: decodeArray(m2, 128, VALUE_TYPES.uint16, `${name} texture lookup`).values,
|
||||
textureCoordinates: decodeArray(m2, 136, VALUE_TYPES.int16, `${name} texture-coordinate lookup`).values,
|
||||
textureWeights: decodeArray(m2, 144, VALUE_TYPES.uint16, `${name} texture-weight lookup`).values,
|
||||
textureTransforms: decodeArray(m2, 152, VALUE_TYPES.int16, `${name} texture-transform lookup`).values,
|
||||
};
|
||||
const vertexLookup = decodeArray(skin, 4, VALUE_TYPES.uint16, `${name} SKIN vertex lookup`).values;
|
||||
const triangleLookup = decodeArray(skin, 12, VALUE_TYPES.uint16, `${name} SKIN triangle lookup`).values;
|
||||
const boneIndices = decodeArray(skin, 20, VALUE_TYPES.uint8x4, `${name} SKIN bone indices`).values;
|
||||
if (triangleLookup.length % 3 !== 0) throw new Error(`${name} SKIN triangle lookup is not divisible by three.`);
|
||||
if (boneIndices.length !== vertexLookup.length) {
|
||||
throw new Error(`${name} SKIN bone-index count does not match its vertex lookup.`);
|
||||
}
|
||||
const indices = triangleLookup.map((lookupIndex, index) => {
|
||||
if (lookupIndex >= vertexLookup.length) {
|
||||
throw new Error(`${name} SKIN triangle index ${index} exceeds its vertex lookup.`);
|
||||
}
|
||||
const vertexIndex = vertexLookup[lookupIndex];
|
||||
if (vertexIndex >= vertices.length) {
|
||||
throw new Error(`${name} SKIN vertex lookup ${lookupIndex} references missing M2 vertex ${vertexIndex}.`);
|
||||
}
|
||||
return vertexIndex;
|
||||
});
|
||||
const submeshCount = skin.readUInt32LE(28);
|
||||
const submeshOffset = skin.readUInt32LE(32);
|
||||
const submeshes = decodeSkinSubmeshes(
|
||||
skin, submeshCount, submeshOffset, triangleLookup.length, vertexLookup.length, name,
|
||||
).map((submesh) => ({
|
||||
...submesh,
|
||||
indices: indices.slice(submesh.indexStart, submesh.indexStart + submesh.indexCount),
|
||||
}));
|
||||
const textureUnitCount = skin.readUInt32LE(36);
|
||||
const textureUnitOffset = skin.readUInt32LE(40);
|
||||
const textureUnits = decodeSkinTextureUnits(skin, textureUnitCount, textureUnitOffset, {
|
||||
submeshes, materials, textures, lookups,
|
||||
}, name);
|
||||
|
||||
return {
|
||||
magic: "MD20",
|
||||
version: 264,
|
||||
modelName: decodeString(m2, 8, `${name} model name`),
|
||||
bounds: {
|
||||
minimum: vector3(m2, 160),
|
||||
maximum: vector3(m2, 172),
|
||||
radius: m2.readFloatLE(184),
|
||||
},
|
||||
vertices,
|
||||
indices,
|
||||
textures,
|
||||
materials,
|
||||
lookups,
|
||||
skin: {
|
||||
magic: "SKIN",
|
||||
maximumBones: skin.readUInt32LE(44),
|
||||
vertexLookup,
|
||||
triangleLookup,
|
||||
boneIndices,
|
||||
submeshes,
|
||||
textureUnits,
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user