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('', 'The directory to load the blocks texture files from (assets/minecraft/textures/block)') .argument('', 'The directory to load the blocks model files from (assets/minecraft/models/block)') .argument('', 'The directory to write the texture atlas files to') .argument('', '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 = { 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(); 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 (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, '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 = {}; 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)); }); });