ObjToSchematic/tools/build-atlas.ts
2023-09-03 22:51:29 +01:00

288 lines
10 KiB
TypeScript

import { program } from 'commander';
import fs from 'fs';
import path from 'path';
import sharp from 'sharp';
import { getAverageColour, getStandardDeviation } from './misc';
import { ASSERT } from '../src/util/error_util';
import { RGBAUtil } from '../src/colour';
program
.argument('<textures_directory>', 'The directory to load the blocks texture files from (assets/minecraft/textures/block)')
.argument('<models_directory>', 'The directory to load the blocks model files from (assets/minecraft/models/block)')
.argument('<output_directory>', 'The directory to write the texture atlas files to')
.argument('<ignore_file_path>', 'Ignore file path')
program.parse();
const paths = {
textures: program.args[0],
models: program.args[1],
output: program.args[2],
ignore: program.args[3],
}
type FaceData<T> = {
up: T,
down: T,
north: T,
south: T,
east: T,
west: T,
}
// GO!
const ignoreList = new Set(fs.readFileSync(paths.ignore, 'utf8').split(/\r?\n/));
// Load all models to use
const allModels = fs.readdirSync(paths.models);
const loadedModels = allModels
// Remove ignored models
.filter((modelFileName) => {
return !ignoreList.has(modelFileName);
})
// Get each models content
.map((modelFileName) => {
const modelFilePath = path.join(paths.models, modelFileName);
const fileContents = fs.readFileSync(modelFilePath, 'utf8');
const model = JSON.parse(fileContents);
switch (model.parent) {
case 'minecraft:block/cube_column_horizontal':
return {
modelFileName: modelFileName,
up: model.textures.side,
down: model.textures.side,
north: model.textures.end,
south: model.textures.end,
east: model.textures.side,
west: model.textures.side,
};
case 'minecraft:block/cube_all':
return {
modelFileName: modelFileName,
up: model.textures.all,
down: model.textures.all,
north: model.textures.all,
south: model.textures.all,
east: model.textures.all,
west: model.textures.all,
};
case 'minecraft:block/cube_column':
return {
modelFileName: modelFileName,
up: model.textures.end,
down: model.textures.end,
north: model.textures.side,
south: model.textures.side,
east: model.textures.side,
west: model.textures.side,
};
case 'minecraft:block/cube_bottom_top':
return {
modelFileName: modelFileName,
up: model.textures.top,
down: model.textures.bottom,
north: model.textures.side,
south: model.textures.side,
east: model.textures.side,
west: model.textures.side,
};
case 'minecraft:block/cube':
return {
modelFileName: modelFileName,
up: model.textures.up,
down: model.textures.down,
north: model.textures.north,
south: model.textures.south,
east: model.textures.east,
west: model.textures.west,
};
case 'minecraft:block/template_single_face':
return {
modelFileName: modelFileName,
up: model.textures.texture,
down: model.textures.texture,
north: model.textures.texture,
south: model.textures.texture,
east: model.textures.texture,
west: model.textures.texture,
};
case 'minecraft:block/template_glazed_terracotta':
return {
modelFileName: modelFileName,
up: model.textures.pattern,
down: model.textures.pattern,
north: model.textures.pattern,
south: model.textures.pattern,
east: model.textures.pattern,
west: model.textures.pattern,
};
case 'minecraft:block/leaves':
return {
modelFileName: modelFileName,
up: model.textures.all,
down: model.textures.all,
north: model.textures.all,
south: model.textures.all,
east: model.textures.all,
west: model.textures.all,
};
default:
return null;
}
}, [])
.filter((entry) => {
return entry !== null;
});
const allTextures = new Set<string>();
loadedModels.forEach((model) => {
allTextures.add(model?.up);
allTextures.add(model?.down);
allTextures.add(model?.east);
allTextures.add(model?.west);
allTextures.add(model?.north);
allTextures.add(model?.south);
});
const atlasSize = Math.ceil(Math.sqrt(allTextures.size));
let nextAtlasColumn = 0;
let nextAtlasRow = 0;
const textureData = Array.from(allTextures)
.map(async (texture) => {
const shortName = texture.split('/')[1]; // Eww
const texturePath = path.join(paths.textures, shortName + '.png');
const image = sharp(texturePath);
const imageData = Uint8ClampedArray.from(await image.raw().ensureAlpha(1.0).toBuffer()); // 16 x 16 x 4
return {
textureName: texture,
texturePath: texturePath,
image: image,
imageData: imageData,
}
});
type ArrayElement<ArrayType extends readonly unknown[]> =
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
Promise.all(textureData)
.then(async (res) => {
const tmp = res
.sort((a, b) => {
return a.textureName < b.textureName ? -1 : 1;
})
.map(async (texture) => {
const averageColour = getAverageColour(texture.imageData);
const standardDeviation = getStandardDeviation(texture.imageData, averageColour);
const atlasColumn = nextAtlasColumn;
const atlasRow = nextAtlasRow;
++nextAtlasColumn;
if (nextAtlasColumn >= atlasSize) {
++nextAtlasRow;
nextAtlasColumn = 0;
}
return {
textureName: texture.textureName,
texturePath: texture.texturePath,
atlasColumn: atlasColumn,
atlasRow: atlasRow,
colour: averageColour,
std: standardDeviation,
};
});
Promise.all(tmp)
.then(async (data) => {
const textureMap = new Map<string, Omit<ArrayElement<typeof data>, 'textureName' | 'texturePath'>>();
data.forEach((texture) => {
textureMap.set(texture.textureName, {
atlasColumn: texture.atlasColumn,
atlasRow: texture.atlasRow,
colour: texture.colour,
std: texture.std,
});
});
const baseImage = await sharp({
create: {
width: atlasSize * 16 * 3,
height: atlasSize * 16 * 3,
channels: 4,
background: { r: 255, g: 255, b: 255, alpha: 0.0 },
}
});
const compositeData: sharp.OverlayOptions[] = [];
data.forEach((x) => {
for (let i = 0; i < 3; ++i) {
for (let j = 0; j < 3; ++j) {
compositeData.push({
input: x.texturePath,
blend: 'over',
left: (x.atlasColumn * 16) * 3 + 16 * i,
top: (x.atlasRow * 16) * 3 + 16 * j,
});
}
}
})
baseImage.composite(compositeData)
.toFile(path.join(paths.output, 'atlas.png'))
.then((res) => {
console.log('Done!');
})
.catch((err) => {
console.error(err);
});
const blocks = loadedModels.map((model) => {
ASSERT(model !== null);
const faces = {
up: model.up,
down: model.down,
north: model.north,
south: model.south,
east: model.east,
west: model.west,
};
const faceColours = Object.values(faces).map((texture) => {
const textureData = textureMap.get(texture);
ASSERT(textureData !== undefined);
return textureData.colour;
});
return {
name: 'minecraft:' + model.modelFileName.split('.')[0],
faces: faces,
colour: RGBAUtil.average(...faceColours),
}
});
console.log(textureMap);
const textures: Record<any, any> = {};
textureMap.forEach((value, key) => {
textures[key] = value;
});
const atlasFile = {
formatVersion: 3,
atlasSize: atlasSize,
blocks: blocks,
textures: textures,
}
fs.writeFileSync(path.join(paths.output, 'atlas.atlas'), JSON.stringify(atlasFile, null, 4));
});
});