mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 18:57:03 +02:00
tank model and collision
This commit is contained in:
1 parent
9968b2c83d
commit
9e4d9c7968
19 files changed
+278
-206
No files matched your search
@@ -1,36 +0,0 @@
|
||||
import { toUniqueId } from "@blitzkit/core";
|
||||
import { NodeIO } from "@gltf-transform/core";
|
||||
import { extractArmor } from "../core/blitz/extractArmor";
|
||||
import { AssetUploader } from "../core/github/assetUploader";
|
||||
import { vfs } from "./constants";
|
||||
import { VehicleDefinitionList } from "./definitions";
|
||||
|
||||
export async function tankArmors() {
|
||||
console.log("Building tank armors...");
|
||||
|
||||
using uploader = new AssetUploader("tank armors");
|
||||
const nodeIO = new NodeIO();
|
||||
const nations = await vfs
|
||||
.dir(`Data/XML/item_defs/vehicles`)
|
||||
.then((files) => files.filter((nation) => nation !== "common"));
|
||||
|
||||
for (const nation of nations) {
|
||||
const tanks = await vfs.xml<{ root: VehicleDefinitionList }>(
|
||||
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
|
||||
);
|
||||
|
||||
for (const tankKey in tanks.root) {
|
||||
const tank = tanks.root[tankKey];
|
||||
|
||||
if (tankKey.includes("tutorial_bot")) continue;
|
||||
|
||||
const id = toUniqueId(nation, tank.id);
|
||||
const model = await extractArmor(vfs, `${nation}-${tankKey}`);
|
||||
const content = await nodeIO.writeBinary(model);
|
||||
|
||||
await uploader.add({ path: `3d/tanks/armor/${id}.glb`, content });
|
||||
}
|
||||
}
|
||||
|
||||
await uploader.flush();
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
import {
|
||||
Hierarchy,
|
||||
Sc2ReadStream,
|
||||
ScgReadStream,
|
||||
VertexAttribute,
|
||||
} from "@blitzkit/core";
|
||||
import { Accessor, Document, Node, Scene } from "@gltf-transform/core";
|
||||
import { times } from "lodash-es";
|
||||
import { AbstractVFS } from "../vfs/abstract";
|
||||
import {
|
||||
vertexAttributeGLTFName,
|
||||
vertexAttributeGltfVectorSizes,
|
||||
} from "./extractModel/constants";
|
||||
|
||||
export async function extractArmor(vfs: AbstractVFS, fileName: string) {
|
||||
const sc2Path = `Data/3d/Tanks/CollisionMeshes/${fileName}.sc2`;
|
||||
const scgPath = `Data/3d/Tanks/CollisionMeshes/${fileName}.scg`;
|
||||
const sc2 = new Sc2ReadStream(
|
||||
(await vfs.file(sc2Path)).buffer as ArrayBuffer,
|
||||
).sc2();
|
||||
const scg = new ScgReadStream(
|
||||
(await vfs.file(scgPath)).buffer as ArrayBuffer,
|
||||
).scg();
|
||||
const document = new Document();
|
||||
const scene = document.createScene();
|
||||
const buffer = document.createBuffer();
|
||||
|
||||
function parseHierarchies(hierarchies: Hierarchy[], parent: Scene | Node) {
|
||||
hierarchies.forEach((hierarchy) => {
|
||||
const components = times(
|
||||
hierarchy.components.count,
|
||||
(index) => hierarchy.components[index.toString().padStart(4, "0")],
|
||||
);
|
||||
|
||||
components.forEach((component) => {
|
||||
switch (component["comp.typename"]) {
|
||||
case "TransformComponent":
|
||||
break;
|
||||
|
||||
case "RenderComponent": {
|
||||
const batch = component["rc.renderObj"]["ro.batches"]["0000"];
|
||||
const polygonGroup = scg.get(batch["rb.datasource"]);
|
||||
|
||||
if (!polygonGroup) {
|
||||
console.warn(
|
||||
`Missing polygon group ${batch["rb.datasource"]} (${hierarchy.name}); skipping...`,
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
const hardJointIndices = new Set<number>();
|
||||
const vertexHardJointIndices = new Map<number, number>();
|
||||
const attributes = new Map<VertexAttribute, number[][]>();
|
||||
|
||||
polygonGroup.vertices.forEach((vertex, index) => {
|
||||
vertex.forEach(({ attribute, value }) => {
|
||||
if (!attributes.has(attribute)) {
|
||||
attributes.set(attribute, []);
|
||||
}
|
||||
|
||||
attributes.get(attribute)!.push(value);
|
||||
|
||||
if (attribute === VertexAttribute.HARD_JOINTINDEX) {
|
||||
hardJointIndices.add(value[0]);
|
||||
vertexHardJointIndices.set(index, value[0]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (attributes.has(VertexAttribute.TANGENT)) {
|
||||
attributes.set(
|
||||
VertexAttribute.TANGENT,
|
||||
attributes
|
||||
.get(VertexAttribute.TANGENT)!
|
||||
.map((tangent) => [...tangent, 1]),
|
||||
);
|
||||
}
|
||||
|
||||
const accessors = new Map<string, Accessor>();
|
||||
|
||||
attributes.forEach((value, attribute) => {
|
||||
const name = vertexAttributeGLTFName[attribute];
|
||||
if (!name || accessors.has(name)) return;
|
||||
const vertexSize = vertexAttributeGltfVectorSizes[attribute];
|
||||
const accessor = document
|
||||
.createAccessor(name)
|
||||
.setType(vertexSize === 1 ? "SCALAR" : `VEC${vertexSize}`)
|
||||
.setArray(new Float32Array(value.flat()))
|
||||
.setBuffer(buffer);
|
||||
|
||||
accessors.set(name, accessor);
|
||||
});
|
||||
|
||||
hardJointIndices.forEach((hardJointIndex) => {
|
||||
const node = document.createNode(
|
||||
`${hierarchy.name}_armor_${hardJointIndex}`,
|
||||
);
|
||||
const mesh = document.createMesh(batch["##name"]);
|
||||
const indicesAccessor = document
|
||||
.createAccessor()
|
||||
.setType("SCALAR")
|
||||
.setArray(
|
||||
new Uint16Array(
|
||||
polygonGroup.indices.filter(
|
||||
(index) =>
|
||||
vertexHardJointIndices.get(index) === hardJointIndex,
|
||||
),
|
||||
),
|
||||
)
|
||||
.setBuffer(buffer);
|
||||
const primitive = document
|
||||
.createPrimitive()
|
||||
.setIndices(indicesAccessor);
|
||||
|
||||
accessors.forEach((accessor, name) => {
|
||||
primitive.setAttribute(name, accessor);
|
||||
});
|
||||
|
||||
mesh.addPrimitive(primitive);
|
||||
node.setMesh(mesh);
|
||||
parent.addChild(node);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new TypeError(
|
||||
`Unhandled component type: ${component["comp.typename"]}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (hierarchy["#hierarchy"]) {
|
||||
parseHierarchies(hierarchy["#hierarchy"], parent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
parseHierarchies(sc2["#hierarchy"], scene);
|
||||
|
||||
return document;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { VertexAttribute, vertexAttributeVectorSizes } from "@blitzkit/core";
|
||||
|
||||
export const vertexAttributeGLTFName: Partial<Record<VertexAttribute, string>> =
|
||||
{
|
||||
[VertexAttribute.VERTEX]: "POSITION",
|
||||
[VertexAttribute.NORMAL]: "NORMAL",
|
||||
[VertexAttribute.TEXCOORD0]: "TEXCOORD_0",
|
||||
[VertexAttribute.TEXCOORD1]: "TEXCOORD_1",
|
||||
[VertexAttribute.TEXCOORD2]: "TEXCOORD_2",
|
||||
[VertexAttribute.TEXCOORD3]: "TEXCOORD_3",
|
||||
};
|
||||
export const vertexAttributeGltfVectorSizes = {
|
||||
...vertexAttributeVectorSizes,
|
||||
[VertexAttribute.TANGENT]: 4,
|
||||
} as const;
|
||||
@@ -1,327 +0,0 @@
|
||||
import {
|
||||
bufferToBigInt,
|
||||
ConfigArchive,
|
||||
Hierarchy,
|
||||
Sc2ReadStream,
|
||||
ScgReadStream,
|
||||
Textures,
|
||||
VertexAttribute,
|
||||
} from "@blitzkit/core";
|
||||
import { Document, Material, Node, Scene } from "@gltf-transform/core";
|
||||
import { dedup, prune } from "@gltf-transform/functions";
|
||||
import { times } from "lodash-es";
|
||||
import { dirname } from "path";
|
||||
import { AbstractVFS } from "../../vfs/abstract";
|
||||
import { readBaseColor } from "../readBaseColor";
|
||||
import { readNormal } from "../readNormal";
|
||||
import { readRoughnessMetallic } from "../readRoughnessMetallic";
|
||||
import {
|
||||
vertexAttributeGLTFName,
|
||||
vertexAttributeGltfVectorSizes,
|
||||
} from "./constants";
|
||||
|
||||
const ERROR_ON_UNKNOWN_COMPONENT = false;
|
||||
|
||||
const omitMeshNames = {
|
||||
start: ["chassis_chassis_", "chassis_track_crash_", "HP_"],
|
||||
end: ["_POINT"],
|
||||
};
|
||||
|
||||
export async function extractModel(vfs: AbstractVFS, path: string) {
|
||||
const sc2Path = `Data/3d/${path}.sc2`;
|
||||
const scgPath = `Data/3d/${path}.scg`;
|
||||
const sc2 = new Sc2ReadStream(
|
||||
(await vfs.file(sc2Path)).buffer as ArrayBuffer,
|
||||
).sc2();
|
||||
const scg = new ScgReadStream(
|
||||
(await vfs.file(scgPath)).buffer as ArrayBuffer,
|
||||
).scg();
|
||||
const document = new Document();
|
||||
const scene = document.createScene();
|
||||
const buffer = document.createBuffer();
|
||||
const materials = new Map<bigint, Material | bigint | undefined>();
|
||||
|
||||
for (const node of sc2["#dataNodes"]) {
|
||||
const id = bufferToBigInt(node["#id"]);
|
||||
|
||||
if (node.parentMaterialKey !== undefined) {
|
||||
/**
|
||||
* material depends on a parent and doesn't seem to have any
|
||||
* properties so we just point it to the parent
|
||||
*/
|
||||
materials.set(id, node.parentMaterialKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
const material = document.createMaterial(node.materialName);
|
||||
let textures: Textures | undefined = undefined;
|
||||
|
||||
if (node.textures) {
|
||||
textures = node.textures;
|
||||
} else if (typeof node.configCount === "number") {
|
||||
textures = node.configArchive_0.textures;
|
||||
}
|
||||
|
||||
if (textures) {
|
||||
const { image: baseColor, hasAlpha } = await readBaseColor(
|
||||
`Data/3d/${dirname(path)}/${textures.baseColorMap ?? textures.albedo}`,
|
||||
textures.miscMap
|
||||
? `Data/3d/${dirname(path)}/${textures.miscMap}`
|
||||
: undefined,
|
||||
);
|
||||
material.setBaseColorTexture(
|
||||
document
|
||||
.createTexture(node.materialName)
|
||||
.setMimeType("image/webp")
|
||||
.setImage(baseColor),
|
||||
);
|
||||
|
||||
let defaultConfigArchive: ConfigArchive | undefined = undefined;
|
||||
|
||||
if (node.configCount) {
|
||||
const names = times(
|
||||
node.configCount,
|
||||
(index) => node[`configArchive_${index}`],
|
||||
);
|
||||
|
||||
defaultConfigArchive = names.find(
|
||||
(archive) => archive.configName === "Default",
|
||||
);
|
||||
defaultConfigArchive ??= node.configArchive_0;
|
||||
}
|
||||
|
||||
const customCullMode =
|
||||
node.customCullMode ?? defaultConfigArchive?.customCullMode;
|
||||
|
||||
if (customCullMode === 0) {
|
||||
material.setDoubleSided(true);
|
||||
} else if (customCullMode !== undefined) {
|
||||
material.setDoubleSided(false);
|
||||
}
|
||||
|
||||
if (
|
||||
node.enabledPresets?.AlphaTest &&
|
||||
node.properties?.alphatestThreshold
|
||||
) {
|
||||
const view = new DataView(node.properties.alphatestThreshold);
|
||||
const alphaCutoff =
|
||||
view.byteLength < 4
|
||||
? 0.5
|
||||
: view.getFloat32(view.byteLength - 4, true);
|
||||
|
||||
material.setAlphaMode("MASK").setAlphaCutoff(alphaCutoff);
|
||||
}
|
||||
|
||||
if (node.configCount) {
|
||||
for (
|
||||
let configIndex = 0;
|
||||
configIndex < node.configCount;
|
||||
configIndex++
|
||||
) {
|
||||
const archive = node[`configArchive_${configIndex}`];
|
||||
|
||||
if (
|
||||
archive.configName !== "Default" ||
|
||||
!archive.enabledPresets?.AlphaTest ||
|
||||
!archive.properties?.alphatestThreshold
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const view = new DataView(archive.properties.alphatestThreshold);
|
||||
const alphaCutoff =
|
||||
view.byteLength < 4
|
||||
? 0.5
|
||||
: view.getFloat32(view.byteLength - 4, true);
|
||||
|
||||
material.setAlphaMode("MASK").setAlphaCutoff(alphaCutoff);
|
||||
}
|
||||
}
|
||||
|
||||
if (textures.baseRMMap) {
|
||||
material.setMetallicRoughnessTexture(
|
||||
document
|
||||
.createTexture(node.materialName)
|
||||
.setMimeType("image/webp")
|
||||
.setImage(
|
||||
await readRoughnessMetallic(
|
||||
`Data/3d/${dirname(path)}/${textures.baseRMMap}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (textures.baseNormalMap ?? textures.normalmap) {
|
||||
const isBase = textures.baseNormalMap !== undefined;
|
||||
|
||||
material.setNormalTexture(
|
||||
document
|
||||
.createTexture(node.materialName)
|
||||
.setMimeType("image/webp")
|
||||
.setImage(
|
||||
await readNormal(
|
||||
`Data/3d/${dirname(path)}/${
|
||||
textures.baseNormalMap ?? textures.normalmap
|
||||
}`,
|
||||
isBase,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
materials.set(id, material);
|
||||
}
|
||||
}
|
||||
|
||||
// replace children materials with parents
|
||||
materials.forEach((material, id) => {
|
||||
if (typeof material !== "bigint") return;
|
||||
|
||||
let resolvedMaterial: Material | bigint | undefined = material;
|
||||
|
||||
while (typeof resolvedMaterial === "bigint") {
|
||||
const linkedParentMaterial = materials.get(resolvedMaterial);
|
||||
|
||||
resolvedMaterial = linkedParentMaterial;
|
||||
}
|
||||
|
||||
materials.set(id, resolvedMaterial);
|
||||
});
|
||||
|
||||
function parseHierarchies(hierarchies: Hierarchy[], parent: Scene | Node) {
|
||||
hierarchies.forEach((hierarchy) => {
|
||||
if (
|
||||
omitMeshNames.start.some((omit) => hierarchy.name.startsWith(omit)) ||
|
||||
omitMeshNames.end.some((omit) => hierarchy.name.endsWith(omit))
|
||||
)
|
||||
return;
|
||||
|
||||
const node = document.createNode(hierarchy.name);
|
||||
const components = times(
|
||||
hierarchy.components.count,
|
||||
(index) => hierarchy.components[index.toString().padStart(4, "0")],
|
||||
);
|
||||
|
||||
components.forEach((component) => {
|
||||
switch (component["comp.typename"]) {
|
||||
case "LodComponent":
|
||||
// found and used later by transform component
|
||||
// TODO: lod component always shows up before transform component; cache it before parsing transform
|
||||
break;
|
||||
|
||||
case "TransformComponent": {
|
||||
const localTranslation = component["tc.localTranslation"];
|
||||
// The game resets top-level node translation to [0, 0, 0] on load.
|
||||
// Child nodes must keep their authored local offsets.
|
||||
node.setTranslation(
|
||||
parent instanceof Scene ? [0, 0, 0] : localTranslation,
|
||||
);
|
||||
node.setRotation(component["tc.localRotation"]);
|
||||
node.setScale(component["tc.localScale"]);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "RenderComponent": {
|
||||
const renderObject = component["rc.renderObj"];
|
||||
|
||||
times(renderObject["ro.batchCount"], (batchIndex): void => {
|
||||
const lodIndex = renderObject[`rb${batchIndex}.lodIndex`];
|
||||
|
||||
if (lodIndex !== 0) return;
|
||||
|
||||
const batchKey = batchIndex.toString().padStart(4, "0");
|
||||
const batch = renderObject["ro.batches"][batchKey];
|
||||
const material = materials.get(batch["rb.nmatname"]);
|
||||
const polygonGroup = scg.get(batch["rb.datasource"]);
|
||||
|
||||
if (!(material instanceof Material)) {
|
||||
// probably shadow material
|
||||
return;
|
||||
}
|
||||
if (polygonGroup === undefined) {
|
||||
throw new Error(
|
||||
`Missing polygon group ${batch["rb.datasource"]}`,
|
||||
);
|
||||
}
|
||||
|
||||
const lodNode = document.createNode(batchKey);
|
||||
const indicesAccessor = document
|
||||
.createAccessor()
|
||||
.setType("SCALAR")
|
||||
.setArray(new Uint16Array(polygonGroup.indices))
|
||||
.setBuffer(buffer);
|
||||
const primitive = document
|
||||
.createPrimitive()
|
||||
.setIndices(indicesAccessor)
|
||||
.setMaterial(material)
|
||||
.setName(batchKey);
|
||||
|
||||
const attributes = new Map<VertexAttribute, number[][]>();
|
||||
|
||||
polygonGroup.vertices.forEach((vertex) => {
|
||||
vertex.forEach(({ attribute, value }) => {
|
||||
if (!attributes.has(attribute)) {
|
||||
attributes.set(attribute, []);
|
||||
}
|
||||
|
||||
attributes.get(attribute)!.push(value);
|
||||
});
|
||||
});
|
||||
|
||||
attributes.forEach((value, attribute) => {
|
||||
const name = vertexAttributeGLTFName[attribute];
|
||||
|
||||
if (
|
||||
name === undefined ||
|
||||
primitive.getAttribute(name) !== null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vertexSize = vertexAttributeGltfVectorSizes[attribute];
|
||||
const attributeAccessor = document
|
||||
.createAccessor(name)
|
||||
.setType(vertexSize === 1 ? "SCALAR" : `VEC${vertexSize}`)
|
||||
.setArray(new Float32Array(value.flat()))
|
||||
.setBuffer(buffer);
|
||||
|
||||
primitive.setAttribute(name, attributeAccessor);
|
||||
});
|
||||
|
||||
const mesh = document
|
||||
.createMesh(batch["##name"])
|
||||
.addPrimitive(primitive);
|
||||
lodNode.setMesh(mesh);
|
||||
node.addChild(lodNode);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
if (ERROR_ON_UNKNOWN_COMPONENT) {
|
||||
throw new Error(
|
||||
`Unhandled component type: ${component["comp.typename"]}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (hierarchy["#hierarchy"]) {
|
||||
parseHierarchies(hierarchy["#hierarchy"], node);
|
||||
}
|
||||
|
||||
parent.addChild(node);
|
||||
});
|
||||
}
|
||||
|
||||
parseHierarchies(sc2["#hierarchy"], scene);
|
||||
|
||||
scene.addChild(document.createNode("test"));
|
||||
|
||||
await document.transform(prune({ keepAttributes: true }), dedup());
|
||||
|
||||
return document;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import sharp from "sharp";
|
||||
import { readTexture } from "./readTexture";
|
||||
|
||||
export async function readBaseColor(path: string, occlusionPath?: string) {
|
||||
let hasAlpha = false;
|
||||
const baseRaw = await readTexture(path);
|
||||
const occlusionRaw = occlusionPath
|
||||
? await readTexture(occlusionPath)
|
||||
: undefined;
|
||||
const base = await sharp(baseRaw.data, { raw: baseRaw }).raw().toBuffer();
|
||||
const occlusion = occlusionRaw
|
||||
? await sharp(occlusionRaw.data, { raw: occlusionRaw })
|
||||
.extractChannel(3)
|
||||
.raw()
|
||||
.toBuffer()
|
||||
: undefined;
|
||||
|
||||
const combined = Buffer.alloc(baseRaw.width * baseRaw.height * 4);
|
||||
|
||||
for (let i = 0; i < baseRaw.width * baseRaw.height; i++) {
|
||||
let c = 1;
|
||||
|
||||
if (occlusionRaw) {
|
||||
const x = i % baseRaw.width;
|
||||
const y = Math.floor(i / baseRaw.width);
|
||||
const u = Math.floor(x * (occlusionRaw.width / baseRaw.width));
|
||||
const v = Math.floor(y * (occlusionRaw.height / baseRaw.height));
|
||||
|
||||
const occlusionI = u + v * occlusionRaw.width;
|
||||
|
||||
c = occlusion![occlusionI] / 255;
|
||||
}
|
||||
|
||||
const alpha = base[i * 4 + 3];
|
||||
hasAlpha ||= alpha < 255;
|
||||
|
||||
combined[i * 4 + 0] = Math.round(base[i * 4 + 0] * c);
|
||||
combined[i * 4 + 1] = Math.round(base[i * 4 + 1] * c);
|
||||
combined[i * 4 + 2] = Math.round(base[i * 4 + 2] * c);
|
||||
combined[i * 4 + 3] = alpha;
|
||||
}
|
||||
|
||||
const image = await sharp(combined, {
|
||||
raw: { width: baseRaw.width, height: baseRaw.height, channels: 4 },
|
||||
})
|
||||
.webp()
|
||||
.toBuffer();
|
||||
|
||||
return { hasAlpha, image };
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import sharp from "sharp";
|
||||
import { readTexture } from "./readTexture";
|
||||
|
||||
export async function readNormal(path: string, isBase: boolean) {
|
||||
const raw = await readTexture(path);
|
||||
|
||||
if (!isBase) {
|
||||
return await sharp(raw.data, { raw }).removeAlpha().jpeg().toBuffer();
|
||||
}
|
||||
|
||||
const bytes = 4 * raw.width * raw.height;
|
||||
|
||||
for (let index = 0; index < bytes; index += 4) {
|
||||
/**
|
||||
* Red is always 255 and blue is always 0. Only alpha and green contain any
|
||||
* sort of information.
|
||||
*/
|
||||
let x = raw.data[index + 3] * (2 / 255) - 1;
|
||||
let y = raw.data[index + 1] * (2 / 255) - 1;
|
||||
let z = Math.sqrt(Math.max(0, 1 - x ** 2 - y ** 2));
|
||||
|
||||
raw.data[index] = Math.round((x + 1) * (255 / 2));
|
||||
raw.data[index + 1] = Math.round((y + 1) * (255 / 2));
|
||||
raw.data[index + 2] = Math.round((z + 1) * (255 / 2));
|
||||
raw.data[index + 3] = 255;
|
||||
}
|
||||
|
||||
return await sharp(raw.data, { raw }).webp().toBuffer();
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import sharp from "sharp";
|
||||
import { readTexture } from "./readTexture";
|
||||
|
||||
export async function readRoughnessMetallic(path: string) {
|
||||
const raw = await readTexture(path);
|
||||
const blitz = sharp(raw.data, { raw: raw });
|
||||
const metallicness = await blitz?.extractChannel(1).raw().toBuffer();
|
||||
const roughness = await blitz?.extractChannel(3).raw().toBuffer();
|
||||
|
||||
const combined = Buffer.alloc(raw.width * raw.height * 3);
|
||||
|
||||
for (let i = 0; i < raw.width * raw.height; i++) {
|
||||
combined[i * 3 + 1] = roughness[i];
|
||||
combined[i * 3 + 2] = metallicness[i];
|
||||
}
|
||||
|
||||
const image = await sharp(combined, {
|
||||
raw: { width: raw.width, height: raw.height, channels: 3 },
|
||||
})
|
||||
.webp()
|
||||
.toBuffer();
|
||||
|
||||
return image;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export enum TextureMutation {
|
||||
Normal,
|
||||
RoughnessMetallicness,
|
||||
Albedo,
|
||||
Miscellaneous,
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { DdsReadStream, PvrReadStream } from "@blitzkit/core";
|
||||
import { vfs } from "../../../buildAssets/constants";
|
||||
|
||||
export async function readTexture(path: string) {
|
||||
const ddsTexturePath = path.replace(".tex", ".dx11.dds");
|
||||
const isDds = await vfs.resolve(ddsTexturePath);
|
||||
const resolvedTexturePath = isDds
|
||||
? ddsTexturePath
|
||||
: ddsTexturePath.replace(".dds", ".pvr");
|
||||
const file = await vfs.file(resolvedTexturePath);
|
||||
|
||||
const raw = isDds
|
||||
? await new DdsReadStream(file.buffer as ArrayBuffer).dds()
|
||||
: new PvrReadStream(file.buffer as ArrayBuffer).pvr();
|
||||
|
||||
return raw;
|
||||
}
|
||||
Reference in new issue
Block a user