mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 11:39:50 +02:00
feat: pvr parser
This commit is contained in:
1 parent
12ee9d381d
commit
376b3f04e5
5 files changed
+264
-47
No files matched your search
@@ -513,13 +513,14 @@ if (allTargets || targets?.includes('tankModels')) {
|
|||||||
const nationVehicleId = tank.id;
|
const nationVehicleId = tank.id;
|
||||||
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
|
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
|
||||||
|
|
||||||
if (id !== 15697) continue; // chieftain TODO: investigate vertices stream over read
|
// if (id !== 15697) continue; // chieftain TODO: investigate vertices stream over read
|
||||||
// if (id !== 24609) continue; // concept 1b
|
// if (id !== 24609) continue; // concept 1b
|
||||||
// if (id !== 4417) continue; // amx m4 mle
|
// if (id !== 4417) continue; // amx m4 mle
|
||||||
// if (id !== 7297) continue; // 60tp
|
// if (id !== 7297) continue; // 60tp
|
||||||
// if (id !== 1) continue; // t-34
|
// if (id !== 1) continue; // t-34
|
||||||
// if (id !== 6753) continue; // type 71
|
// if (id !== 6753) continue; // type 71
|
||||||
// if (id !== 5137) continue; // tiger ii
|
// if (id !== 5137) continue; // tiger ii
|
||||||
|
if (id !== 6225) continue; // fv215b
|
||||||
|
|
||||||
console.log(`Building model ${id} @ ${nation}/${tankIndex}`);
|
console.log(`Building model ${id} @ ${nation}/${tankIndex}`);
|
||||||
|
|
||||||
|
|||||||
@@ -93,15 +93,19 @@ export async function extractModel(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (textures.baseNormalMap) {
|
if (textures.baseNormalMap ?? textures.normalmap) {
|
||||||
|
const isBase = textures.baseNormalMap !== undefined;
|
||||||
|
|
||||||
material.setNormalTexture(
|
material.setNormalTexture(
|
||||||
document
|
document
|
||||||
.createTexture(node.materialName)
|
.createTexture(node.materialName)
|
||||||
.setMimeType('image/png')
|
.setMimeType('image/png')
|
||||||
.setImage(
|
.setImage(
|
||||||
await readTexture(
|
await readTexture(
|
||||||
`${data}/3d/${dirname(path)}/${textures.baseNormalMap}`,
|
`${data}/3d/${dirname(path)}/${
|
||||||
{ mutation: TextureMutation.Normal },
|
textures.baseNormalMap ?? textures.normalmap
|
||||||
|
}`,
|
||||||
|
isBase ? { mutation: TextureMutation.Normal } : undefined,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -146,6 +150,7 @@ export async function extractModel(
|
|||||||
case 'DecorItemComponent':
|
case 'DecorItemComponent':
|
||||||
case 'NewSlotComponent':
|
case 'NewSlotComponent':
|
||||||
case 'StateSwitcherComponent':
|
case 'StateSwitcherComponent':
|
||||||
|
case 'CustomPropertiesComponent':
|
||||||
case 'SlotComponent':
|
case 'SlotComponent':
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import { existsSync } from 'fs';
|
||||||
import { clamp } from 'lodash';
|
import { clamp } from 'lodash';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { Vector3Tuple } from 'three';
|
import { Vector3Tuple } from 'three';
|
||||||
import { readDVPLFile } from '../blitz/readDVPLFile';
|
import { readDVPLFile } from '../blitz/readDVPLFile';
|
||||||
import { DdsStream } from '../streams/dds';
|
import { DdsStream } from '../streams/dds';
|
||||||
|
import { PvrStream } from '../streams/pvr';
|
||||||
|
|
||||||
export enum TextureMutation {
|
export enum TextureMutation {
|
||||||
Normal,
|
Normal,
|
||||||
@@ -24,28 +26,32 @@ type ReadTextureOptions =
|
|||||||
|
|
||||||
export async function readTexture(path: string, options?: ReadTextureOptions) {
|
export async function readTexture(path: string, options?: ReadTextureOptions) {
|
||||||
const ddsTexturePath = path.replace('.tex', '.dx11.dds.dvpl');
|
const ddsTexturePath = path.replace('.tex', '.dx11.dds.dvpl');
|
||||||
const decompressedDvpl = await readDVPLFile(ddsTexturePath);
|
const isDds = existsSync(ddsTexturePath);
|
||||||
const stream = new DdsStream(decompressedDvpl);
|
const resolvedTexturePath = isDds
|
||||||
const dds = await stream.dds();
|
? ddsTexturePath
|
||||||
const channels = 4 * dds.width * dds.height;
|
: ddsTexturePath.replace('.dds', '.pvr');
|
||||||
|
const decompressedDvpl = await readDVPLFile(resolvedTexturePath);
|
||||||
|
const raw = isDds
|
||||||
|
? await new DdsStream(decompressedDvpl).dds()
|
||||||
|
: new PvrStream(decompressedDvpl).pvr();
|
||||||
|
const channels = 4 * raw.width * raw.height;
|
||||||
|
|
||||||
/**
|
|
||||||
* Red is always 255 and blue is always 0. Only alpha and green contain any
|
|
||||||
* sort of information.
|
|
||||||
*/
|
|
||||||
switch (options?.mutation) {
|
switch (options?.mutation) {
|
||||||
case TextureMutation.Normal: {
|
case TextureMutation.Normal: {
|
||||||
for (let index = 0; index < channels; index += 4) {
|
for (let index = 0; index < channels; index += 4) {
|
||||||
// TODO: this math is pretty sus and looks very goofy when rendered
|
/**
|
||||||
let x = dds.data[index + 3] * (2 / 255) - 1;
|
* Red is always 255 and blue is always 0. Only alpha and green contain any
|
||||||
let y = dds.data[index + 1] * (2 / 255) - 1;
|
* sort of information.
|
||||||
|
*/
|
||||||
|
let x = raw.data[index + 3] * (2 / 255) - 1;
|
||||||
|
let y = raw.data[index + 1] * (2 / 255) - 1;
|
||||||
const underSqrt = 1 - x ** 2 - y ** 2;
|
const underSqrt = 1 - x ** 2 - y ** 2;
|
||||||
let z = underSqrt > 0 ? Math.sqrt(underSqrt) : -Math.sqrt(-underSqrt);
|
let z = underSqrt > 0 ? Math.sqrt(underSqrt) : -Math.sqrt(-underSqrt);
|
||||||
|
|
||||||
dds.data[index] = Math.round((x + 1) * (255 / 2));
|
raw.data[index] = Math.round((x + 1) * (255 / 2));
|
||||||
dds.data[index + 1] = Math.round((y + 1) * (255 / 2));
|
raw.data[index + 1] = Math.round((y + 1) * (255 / 2));
|
||||||
dds.data[index + 2] = Math.round((z + 1) * (255 / 2));
|
raw.data[index + 2] = Math.round((z + 1) * (255 / 2));
|
||||||
dds.data[index + 3] = 255;
|
raw.data[index + 3] = 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -53,13 +59,16 @@ export async function readTexture(path: string, options?: ReadTextureOptions) {
|
|||||||
|
|
||||||
case TextureMutation.RoughnessMetallicness: {
|
case TextureMutation.RoughnessMetallicness: {
|
||||||
for (let index = 0; index < channels; index += 4) {
|
for (let index = 0; index < channels; index += 4) {
|
||||||
const metallicness = dds.data[index + 1];
|
/**
|
||||||
const roughness = dds.data[index + 3];
|
* Same channel situation as normal maps.
|
||||||
|
*/
|
||||||
|
const metallicness = raw.data[index + 1];
|
||||||
|
const roughness = raw.data[index + 3];
|
||||||
|
|
||||||
dds.data[index] = 0;
|
raw.data[index] = 0;
|
||||||
dds.data[index + 1] = roughness;
|
raw.data[index + 1] = roughness;
|
||||||
dds.data[index + 2] = metallicness;
|
raw.data[index + 2] = metallicness;
|
||||||
dds.data[index + 3] = 255;
|
raw.data[index + 3] = 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -69,36 +78,24 @@ export async function readTexture(path: string, options?: ReadTextureOptions) {
|
|||||||
for (let index = 0; index < channels; index += 4) {
|
for (let index = 0; index < channels; index += 4) {
|
||||||
// treating as pre multiplied but I can't fully confirm
|
// treating as pre multiplied but I can't fully confirm
|
||||||
|
|
||||||
const r = dds.data[index] / 255;
|
const r = raw.data[index] / 255;
|
||||||
const g = dds.data[index + 1] / 255;
|
const g = raw.data[index + 1] / 255;
|
||||||
const b = dds.data[index + 2] / 255;
|
const b = raw.data[index + 2] / 255;
|
||||||
const a = dds.data[index + 3] / 255;
|
const a = raw.data[index + 3] / 255;
|
||||||
const invA = 1 - a;
|
const invA = 1 - a;
|
||||||
const rPrime = r + invA * options.baseColor[0];
|
const rPrime = r + invA * options.baseColor[0];
|
||||||
const gPrime = g + invA * options.baseColor[1];
|
const gPrime = g + invA * options.baseColor[1];
|
||||||
const bPrime = b + invA * options.baseColor[2];
|
const bPrime = b + invA * options.baseColor[2];
|
||||||
|
|
||||||
dds.data[index] = Math.round(clamp(rPrime * 255, 0, 255));
|
raw.data[index] = Math.round(clamp(rPrime * 255, 0, 255));
|
||||||
dds.data[index + 1] = Math.round(clamp(gPrime * 255, 0, 255));
|
raw.data[index + 1] = Math.round(clamp(gPrime * 255, 0, 255));
|
||||||
dds.data[index + 2] = Math.round(clamp(bPrime * 255, 0, 255));
|
raw.data[index + 2] = Math.round(clamp(bPrime * 255, 0, 255));
|
||||||
dds.data[index + 3] = 255;
|
raw.data[index + 3] = 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await sharp(dds.data, { raw: dds }).png().toBuffer();
|
return await sharp(raw.data, { raw: raw }).png().toBuffer();
|
||||||
|
|
||||||
// const isDds = existsSync(ddsTexturePath);
|
|
||||||
// const resolvedTexturePath = isDds
|
|
||||||
// ? ddsTexturePath
|
|
||||||
// : ddsTexturePath.replace('.dds', '.pvr');
|
|
||||||
// const decompressedTexture = await readDVPLFile(resolvedTexturePath);
|
|
||||||
// const jpg = await toJpg(decompressedTexture, {
|
|
||||||
// ...options,
|
|
||||||
// format: isDds ? 'dds' : 'pvr',
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return jpg;
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import { clamp, isEqual, times } from 'lodash';
|
||||||
|
import { Vector4Tuple } from 'three';
|
||||||
|
import { BufferStream } from './buffer';
|
||||||
|
|
||||||
|
export enum PvrFlags {
|
||||||
|
NoFlag = 0,
|
||||||
|
PreMultiplied = 0x02,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PvrPixelFormat {
|
||||||
|
PVRTC_2bpp_RGB = 0,
|
||||||
|
PVRTC_2bpp_RGBA = 1,
|
||||||
|
PVRTC_4bpp_RGB = 2,
|
||||||
|
PVRTC_4bpp_RGBA = 3,
|
||||||
|
PVRTC_II_2bpp = 4,
|
||||||
|
PVRTC_II_4bpp = 5,
|
||||||
|
ETC1 = 6,
|
||||||
|
DXT1_BC1 = 7,
|
||||||
|
DXT2 = 8,
|
||||||
|
DXT3_BC2 = 9,
|
||||||
|
DXT4 = 10,
|
||||||
|
DXT5_BC3 = 11,
|
||||||
|
BC4 = 12,
|
||||||
|
BC5 = 13,
|
||||||
|
BC6 = 14,
|
||||||
|
BC7 = 15,
|
||||||
|
UYVY = 16,
|
||||||
|
YUY2 = 17,
|
||||||
|
BW1bpp = 18,
|
||||||
|
R9G9B9E5_SharedExponent = 19,
|
||||||
|
RGBG8888 = 20,
|
||||||
|
GRGB8888 = 21,
|
||||||
|
ETC2_RGB = 22,
|
||||||
|
ETC2_RGBA = 23,
|
||||||
|
ETC2_RGB_A1 = 24,
|
||||||
|
EAC_R11 = 25,
|
||||||
|
EAC_RG11 = 26,
|
||||||
|
ASTC_4x4 = 27,
|
||||||
|
ASTC_5x4 = 28,
|
||||||
|
ASTC_5x5 = 29,
|
||||||
|
ASTC_6x5 = 30,
|
||||||
|
ASTC_6x6 = 31,
|
||||||
|
ASTC_8x5 = 32,
|
||||||
|
ASTC_8x6 = 33,
|
||||||
|
ASTC_8x8 = 34,
|
||||||
|
ASTC_10x5 = 35,
|
||||||
|
ASTC_10x6 = 36,
|
||||||
|
ASTC_10x8 = 37,
|
||||||
|
ASTC_10x10 = 38,
|
||||||
|
ASTC_12x10 = 39,
|
||||||
|
ASTC_12x12 = 40,
|
||||||
|
ASTC_3x3x3 = 41,
|
||||||
|
ASTC_4x3x3 = 42,
|
||||||
|
ASTC_4x4x3 = 43,
|
||||||
|
ASTC_4x4x4 = 44,
|
||||||
|
ASTC_5x4x4 = 45,
|
||||||
|
ASTC_5x5x4 = 46,
|
||||||
|
ASTC_5x5x5 = 47,
|
||||||
|
ASTC_6x5x5 = 48,
|
||||||
|
ASTC_6x6x5 = 49,
|
||||||
|
ASTC_6x6x6 = 50,
|
||||||
|
BASISU_ETC1S = 51,
|
||||||
|
BASISU_UASTC = 52,
|
||||||
|
RGBM = 53,
|
||||||
|
RGBD = 54,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PvrColorSpace {
|
||||||
|
LinearRGB = 0,
|
||||||
|
sRGB = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PvrChannelType {
|
||||||
|
UnsignedByteNormalised = 0,
|
||||||
|
SignedByteNormalised = 1,
|
||||||
|
UnsignedByte = 2,
|
||||||
|
SignedByte = 3,
|
||||||
|
UnsignedShortNormalised = 4,
|
||||||
|
SignedShortNormalised = 5,
|
||||||
|
UnsignedShort = 6,
|
||||||
|
SignedShort = 7,
|
||||||
|
UnsignedIntegerNormalised = 8,
|
||||||
|
SignedIntegerNormalised = 9,
|
||||||
|
UnsignedInteger = 10,
|
||||||
|
SignedInteger = 11,
|
||||||
|
SignedFloat = 12,
|
||||||
|
UnsignedFloat = 13,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ResolvedBitRatePixelFormat {
|
||||||
|
R4G4B4A4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PvrStream extends BufferStream {
|
||||||
|
pvr() {
|
||||||
|
const header = this.header();
|
||||||
|
const metadata = this.metadata();
|
||||||
|
|
||||||
|
console.log(header, metadata);
|
||||||
|
|
||||||
|
if (typeof header.pixelFormat === 'number') {
|
||||||
|
switch (header.pixelFormat) {
|
||||||
|
default:
|
||||||
|
throw new TypeError(
|
||||||
|
`Unhandled pixel format ${PvrPixelFormat[header.pixelFormat]} (${
|
||||||
|
header.pixelFormat
|
||||||
|
})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const resolvedPixelFormat = this.resolveBitRatePixelFormat(
|
||||||
|
header.pixelFormat,
|
||||||
|
);
|
||||||
|
const pixelCount = header.width * header.height;
|
||||||
|
|
||||||
|
switch (resolvedPixelFormat) {
|
||||||
|
case ResolvedBitRatePixelFormat.R4G4B4A4: {
|
||||||
|
const data = Buffer.alloc(pixelCount * 4);
|
||||||
|
let lastPixel: Vector4Tuple;
|
||||||
|
|
||||||
|
times(pixelCount, (index) => {
|
||||||
|
const buffer = this.consume(2);
|
||||||
|
const bufferIndex = index * 4;
|
||||||
|
const r = ((buffer[0] & 0b11110000) >>> 4) / 15;
|
||||||
|
const g = (buffer[0] & 0b1111) / 15;
|
||||||
|
const b = ((buffer[1] & 0b11110000) >>> 4) / 15;
|
||||||
|
const a = (buffer[1] & 0b1111) / 15;
|
||||||
|
|
||||||
|
data[bufferIndex] = Math.round(clamp(r * 255, 0, 255));
|
||||||
|
data[bufferIndex + 1] = Math.round(clamp(g * 255, 0, 255));
|
||||||
|
data[bufferIndex + 2] = Math.round(clamp(b * 255, 0, 255));
|
||||||
|
data[bufferIndex + 3] = Math.round(clamp(a * 255, 0, 255));
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
width: header.width,
|
||||||
|
height: header.height,
|
||||||
|
channels: 4 as const,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new TypeError(
|
||||||
|
`Unhandled resolved pixel format ${resolvedPixelFormat} (${header.pixelFormat})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header() {
|
||||||
|
const header = {
|
||||||
|
version: this.ascii(4),
|
||||||
|
flags: this.uint32() as PvrFlags,
|
||||||
|
pixelFormat: this.pixelFormat(),
|
||||||
|
colorSpace: this.uint32() as PvrColorSpace,
|
||||||
|
channelType: this.uint32() as PvrChannelType,
|
||||||
|
height: this.uint32(),
|
||||||
|
width: this.uint32(),
|
||||||
|
depth: this.uint32(),
|
||||||
|
surfaces: this.uint32(),
|
||||||
|
faces: this.uint32(),
|
||||||
|
mipMaps: this.uint32(),
|
||||||
|
metadataSize: this.uint32(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (header.version !== 'PVR\x03') throw new TypeError('Endian mismatch');
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
pixelFormat() {
|
||||||
|
this.skip(4);
|
||||||
|
const usesPresetFormat = this.uint32() === 0;
|
||||||
|
this.skip(-8);
|
||||||
|
|
||||||
|
return usesPresetFormat
|
||||||
|
? (Number(this.uint64()) as PvrPixelFormat)
|
||||||
|
: {
|
||||||
|
order: this.ascii(4),
|
||||||
|
bitRates: times(4, () => this.uint8()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
resolveBitRatePixelFormat(
|
||||||
|
pixelFormat: Exclude<ReturnType<typeof this.pixelFormat>, number>,
|
||||||
|
) {
|
||||||
|
if (pixelFormat.order === 'rgba') {
|
||||||
|
if (isEqual(pixelFormat.bitRates, [4, 4, 4, 4])) {
|
||||||
|
return ResolvedBitRatePixelFormat.R4G4B4A4;
|
||||||
|
} else
|
||||||
|
throw new TypeError(
|
||||||
|
`Unhandled rgba bit rate ${pixelFormat.bitRates.join(', ')}`,
|
||||||
|
);
|
||||||
|
} else throw new TypeError(`Unhandled pixel order ${pixelFormat.order}`);
|
||||||
|
}
|
||||||
|
metadata() {
|
||||||
|
const fourCC = this.ascii(4);
|
||||||
|
const key = this.uint32();
|
||||||
|
const dataSize = this.uint32();
|
||||||
|
const data = this.consume(dataSize);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fourCC,
|
||||||
|
key,
|
||||||
|
dataSize,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -177,7 +177,13 @@ export type Component =
|
|||||||
'comp.typename': 'StateSwitcherComponent';
|
'comp.typename': 'StateSwitcherComponent';
|
||||||
'ssc.activeState': number;
|
'ssc.activeState': number;
|
||||||
'ssc.statesCount': number;
|
'ssc.statesCount': number;
|
||||||
} & Record<`ssc.state${number}`, string>);
|
} & Record<`ssc.state${number}`, string>)
|
||||||
|
| {
|
||||||
|
'comp.typename': 'CustomPropertiesComponent';
|
||||||
|
'cpc.properties.archive': {
|
||||||
|
'editor.donotremove': number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
interface SceneComponents {
|
interface SceneComponents {
|
||||||
count: number;
|
count: number;
|
||||||
|
|||||||
Reference in new issue
Block a user