122 lines
5.4 KiB
JavaScript
122 lines
5.4 KiB
JavaScript
const WEIGHT_SIZE = 64 * 64;
|
|
|
|
function assertKnownAlphaLayer(layer, chunk) {
|
|
if (!layer.usesAlphaMap) return;
|
|
if (!layer.alphaMapAvailable || layer.alphaMapDataOffset == null || !layer.alphaMapBytes) {
|
|
throw new Error(`MCNK ${chunk.indexX},${chunk.indexY} layer ${layer.index} has an unsupported or invalid MCAL span.`);
|
|
}
|
|
}
|
|
|
|
function decodeRle8(data, label) {
|
|
const output = new Uint8Array(WEIGHT_SIZE);
|
|
let inputOffset = 0;
|
|
let outputOffset = 0;
|
|
while (outputOffset < output.length) {
|
|
if (inputOffset >= data.length) throw new Error(`${label}: truncated MCAL RLE stream.`);
|
|
const control = data[inputOffset++];
|
|
const count = control & 0x7f;
|
|
if (!count) throw new Error(`${label}: zero-count MCAL RLE command cannot progress.`);
|
|
if (outputOffset + count > output.length) throw new Error(`${label}: MCAL RLE expands beyond 4096 samples.`);
|
|
if (control & 0x80) {
|
|
if (inputOffset >= data.length) throw new Error(`${label}: truncated MCAL fill command.`);
|
|
output.fill(data[inputOffset++], outputOffset, outputOffset + count);
|
|
} else {
|
|
if (inputOffset + count > data.length) throw new Error(`${label}: truncated MCAL copy command.`);
|
|
output.set(data.subarray(inputOffset, inputOffset + count), outputOffset);
|
|
inputOffset += count;
|
|
}
|
|
outputOffset += count;
|
|
}
|
|
if (inputOffset !== data.length) throw new Error(`${label}: ${data.length - inputOffset} trailing MCAL RLE bytes are unaccounted for.`);
|
|
return output;
|
|
}
|
|
|
|
function decodePacked4(data, noEdgeRepair, label) {
|
|
if (data.length !== 2048) throw new Error(`${label}: packed 4-bit MCAL must be exactly 2048 bytes.`);
|
|
const output = new Uint8Array(WEIGHT_SIZE);
|
|
let source = 0;
|
|
for (let row = 0; row < 64; row += 1) {
|
|
for (let column = 0; column < 64; column += 2) {
|
|
const value = data[source++];
|
|
output[row * 64 + column] = (value & 0x0f) * 17;
|
|
output[row * 64 + column + 1] = (value >>> 4) * 17;
|
|
}
|
|
}
|
|
if (!noEdgeRepair) {
|
|
for (let index = 0; index < 64; index += 1) {
|
|
output[index * 64 + 63] = output[index * 64 + 62];
|
|
output[63 * 64 + index] = output[62 * 64 + index];
|
|
}
|
|
output[63 * 64 + 63] = output[62 * 64 + 62];
|
|
}
|
|
return output;
|
|
}
|
|
|
|
export function decodeWowAdtAlphaMap(adtBuffer, chunk, layer) {
|
|
assertKnownAlphaLayer(layer, chunk);
|
|
if (!layer.usesAlphaMap) return new Uint8Array(WEIGHT_SIZE);
|
|
const payload = adtBuffer.subarray(layer.alphaMapDataOffset, layer.alphaMapDataOffset + layer.alphaMapBytes);
|
|
const label = `MCNK ${chunk.indexX},${chunk.indexY} layer ${layer.index}`;
|
|
switch (layer.alphaMapEncoding) {
|
|
case "packed-4":
|
|
return decodePacked4(payload, (chunk.flags & 0x8000) !== 0, label);
|
|
case "uncompressed-8":
|
|
if (payload.length !== WEIGHT_SIZE) throw new Error(`${label}: 8-bit MCAL must be exactly 4096 bytes.`);
|
|
return new Uint8Array(payload);
|
|
case "rle-8":
|
|
return decodeRle8(payload, label);
|
|
default:
|
|
throw new Error(`${label}: unsupported alpha encoding ${layer.alphaMapEncoding ?? "missing"}.`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decode source overlays into the effective RGB weights expected by the
|
|
* wow-overlay-residual shader. Classic 4-bit maps are source opacity maps;
|
|
* their overlay order must be resolved before the shader consumes them.
|
|
*/
|
|
export function composeWowAdtTerrainWeights(adtBuffer, chunk) {
|
|
const overlays = chunk.layers.slice(1);
|
|
if (overlays.length > 3) throw new Error(`MCNK ${chunk.indexX},${chunk.indexY} has more than three overlay layers.`);
|
|
const encodings = [...new Set(overlays.filter((layer) => layer.usesAlphaMap).map((layer) => layer.alphaMapEncoding))];
|
|
if (encodings.length > 1) throw new Error(`MCNK ${chunk.indexX},${chunk.indexY} mixes MCAL alpha encodings.`);
|
|
const sourceEncoding = encodings[0] ?? "none";
|
|
const alphaMaps = overlays.map((layer) => decodeWowAdtAlphaMap(adtBuffer, chunk, layer));
|
|
const packed = sourceEncoding === "packed-4";
|
|
const effective = alphaMaps.map((map) => new Uint8Array(map));
|
|
if (packed) {
|
|
for (let pixel = 0; pixel < WEIGHT_SIZE; pixel += 1) {
|
|
let remaining = 255;
|
|
for (let index = effective.length - 1; index >= 0; index -= 1) {
|
|
const weight = Math.floor((effective[index][pixel] * remaining + 127) / 255);
|
|
effective[index][pixel] = weight;
|
|
remaining -= weight;
|
|
}
|
|
}
|
|
}
|
|
let maxWeightSum = 0;
|
|
for (let pixel = 0; pixel < WEIGHT_SIZE; pixel += 1) {
|
|
const sum = effective.reduce((value, map) => value + map[pixel], 0);
|
|
maxWeightSum = Math.max(maxWeightSum, sum);
|
|
if (sum > 255) throw new Error(`MCNK ${chunk.indexX},${chunk.indexY} effective overlays exceed 255 (${sum} at sample ${pixel}).`);
|
|
}
|
|
const rgba = Buffer.alloc(WEIGHT_SIZE * 4, 255);
|
|
for (let pixel = 0; pixel < WEIGHT_SIZE; pixel += 1) {
|
|
for (let channel = 0; channel < 3; channel += 1) rgba[pixel * 4 + channel] = effective[channel]?.[pixel] ?? 0;
|
|
}
|
|
return {
|
|
rgba,
|
|
sourceEncoding,
|
|
sourcePolicy: packed ? "classic-nibble-to-big-alpha" : "direct-8bit-overlay-weights",
|
|
channelLayerIndices: overlays.map((layer) => layer.index),
|
|
maxWeightSum,
|
|
};
|
|
}
|
|
|
|
export function wowAdtNormalizedUv(row, column) {
|
|
if (!Number.isInteger(row) || row < 0 || row > 16) throw new RangeError("MCVT row must be in [0,16].");
|
|
const columns = row % 2 ? 8 : 9;
|
|
if (!Number.isInteger(column) || column < 0 || column >= columns) throw new RangeError(`MCVT column must be in [0,${columns - 1}].`);
|
|
return [(column + (row % 2 ? 0.5 : 0)) / 8, row / 16];
|
|
}
|