Support for non-uniform textured blocks

This commit is contained in:
Lucas Dower 2021-10-22 15:52:03 +01:00
parent 0b47fc04e7
commit f77f60e729
10 changed files with 12868 additions and 1581 deletions

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

After

Width:  |  Height:  |  Size: 414 KiB

View File

@ -129,6 +129,7 @@ export class AppContext {
this._renderer.compile();
} catch (err: any) {
this._showToast(err.message, ToastColour.RED);
console.error(err);
return;
}

View File

@ -5,10 +5,26 @@ import { UV, RGB } from "./util";
import fs from "fs";
import path from "path";
export interface TextureInfo {
name: string
texcoord: UV
}
export interface FaceInfo {
[face: string]: TextureInfo,
up: TextureInfo,
down: TextureInfo,
north: TextureInfo,
south: TextureInfo,
east: TextureInfo,
west: TextureInfo
}
export interface BlockInfo {
name: string;
colour: RGB;
texcoord: UV
faces: FaceInfo
}
// https://minecraft.fandom.com/wiki/Java_Edition_data_values/Pre-flattening/Block_IDs

View File

@ -11,6 +11,7 @@ import { RGB, UV, rgbToArray } from "./util";
import { VoxelManager } from "./voxel_manager";
import { Triangle } from "./triangle";
import { Mesh, FillMaterial, TextureMaterial, MaterialType } from "./mesh";
import { FaceInfo } from "./block_atlas";
export class Renderer {
@ -50,7 +51,7 @@ export class Renderer {
this._debug = false;
this._compiled = false;
console.log(twgl.primitives.createCubeVertices(1.0));
//this._blockTexture = twgl.createTexture(this._gl, { src: "resources/blocks/stone.png", mag: this._gl.NEAREST });
@ -79,7 +80,7 @@ export class Renderer {
this._registerData(data);
}
private _registerVoxel(centre: Vector3, voxelManager: VoxelManager, blockTexcoord: UV) {
private _registerVoxel(centre: Vector3, voxelManager: VoxelManager, blockTexcoord: FaceInfo) {
let occlusions = new Array<Array<number>>(6);
// For each face
for (let f = 0; f < 6; ++f) {
@ -109,9 +110,14 @@ export class Renderer {
data.occlusion[j * 16 + k] = occlusions[j][k % 4];
}
}
const l = data.position.length / 3;
for (let i = 0; i < l; ++i) {
data.blockTexcoord.push(blockTexcoord.u, blockTexcoord.v);
// Assign the textures to each face
const faceOrder = ["north", "south", "up", "down", "east", "west"];
for (const face of faceOrder) {
for (let i = 0; i < 4; ++i) {
const texcoord = blockTexcoord[face].texcoord;
data.blockTexcoord.push(texcoord.u, texcoord.v);
}
}
this._registerVoxels.add(data);

View File

@ -2,7 +2,7 @@ import { CubeAABB } from "./aabb";
import { Vector3 } from "./vector.js";
import { HashMap } from "./hash_map";
import { Texture } from "./texture";
import { BlockAtlas, BlockInfo } from "./block_atlas";
import { BlockAtlas, BlockInfo, TextureInfo, FaceInfo } from "./block_atlas";
import { UV, RGB } from "./util";
import { Triangle } from "./triangle";
import { Mesh, MaterialType } from "./mesh";
@ -14,7 +14,6 @@ interface Block {
block?: string
}
interface TriangleCubeAABBs {
triangle: Triangle;
AABBs: Array<CubeAABB>;
@ -23,7 +22,7 @@ interface TriangleCubeAABBs {
export class VoxelManager {
public voxels: Array<Block>;
public voxelTexcoords: Array<UV>;
public voxelTexcoords: Array<FaceInfo>;
public triangleAABBs: Array<TriangleCubeAABBs>;
public _voxelSize: number;
@ -120,7 +119,7 @@ export class VoxelManager {
averageColour.b /= n;
const block = this.blockAtlas.getBlock(averageColour);
this.voxels[i].block = block.name;
this.voxelTexcoords.push(block.texcoord);
this.voxelTexcoords.push(block.faces);
if (!this.blockPalette.includes(block.name)) {
this.blockPalette.push(block.name);

View File

@ -1,174 +1,188 @@
import fs from "fs";
import path from "path";
import { PNGWithMetadata } from "pngjs";
import images from "images";
import { RGB, UV } from "../src/util";
import { log, LogStyle } from "./logging";
import { PNG } from "pngjs";
import { isDirSetup, assert, getAverageColour } from "./misc";
import chalk from "chalk";
import { logStatus, logSuccess, logInfo, isDirSetup, assert, doesTextureFileExists, getTextureData, logWarning, getAbsoluteTexturePath, getAverageColour, getTextureName } from "./misc";
logStatus("Checking Minecraft assets are provided...");
// Check /blocks and /models is setup correctly
log(LogStyle.None, "Checking Minecraft assets are provided...");
const blocksDirSetup = isDirSetup("./blocks", "assets/minecraft/textures/block");
const modelsDirSetup = isDirSetup("./models", "assets/minecraft/models/block");
assert(blocksDirSetup && modelsDirSetup, "Folders not setup correctly");
logSuccess("Folders setup correctly");
log(LogStyle.Success, "Folders setup correctly\n")
logStatus("Loading ignore list...")
// Load the ignore list
log(LogStyle.None, "Loading ignore list...")
let ignoreList: Array<string> = [];
const ignoreListPath = path.join(__dirname, "./ignore-list.txt");
const defaultIgnoreListPath = path.join(__dirname, "./default-ignore-list.txt");
if (fs.existsSync(ignoreListPath)) {
logSuccess("Found custom ignore list");
log(LogStyle.Success, "Found custom ignore list");
ignoreList = fs.readFileSync(ignoreListPath, "utf-8").replace(/\r/g, "").split("\n");
} else if (fs.existsSync(defaultIgnoreListPath)){
logSuccess("Found default ignore list");
log(LogStyle.Success, "Found default ignore list");
ignoreList = fs.readFileSync(defaultIgnoreListPath, "utf-8").replace(/\r/g, "").split("\n");
} else {
logWarning("No ignore list found, looked for ignore-list.txt and default-ignore-list.txt");
log(LogStyle.Warning, "No ignore list found, looked for ignore-list.txt and default-ignore-list.txt");
}
logInfo(`${ignoreList.length} blocks found in ignore list`);
log(LogStyle.Info, `${ignoreList.length} blocks found in ignore list\n`);
interface BlockData {
filePath: string,
modelData: any
}
interface BlockPNGData {
pngData: {[texture: string]: PNGWithMetadata}
}
logStatus("Loading block models...")
let candidateAtlasBlocks: Array<BlockData> = [];
let ignoredCount = 0;
//
log(LogStyle.None, "Loading block models...")
enum parentModel {
Cube = "minecraft:block/cube",
CubeAll = "minecraft:block/cube_all",
CubeColumn = "minecraft:block/cube_column",
Cube = "minecraft:block/cube"
CubeColumnHorizontal = "minecraft:block/cube_column_horizontal"
}
//const supportedModels = [parentModel.CubeAll, parentModel.CubeColumn, parentModel.Cube];
const supportedModels = [parentModel.CubeAll];
interface Model {
name: string,
colour?: RGB,
faces: {
[face: string]: Texture
}
}
interface Texture {
name: string,
texcoord?: UV,
colour?: RGB
}
const faces = ["north", "south", "up", "down", "east", "west"];
let allModels: Array<Model> = [];
let usedTextures: Set<string> = new Set();
fs.readdirSync(path.join(__dirname, "./models")).forEach(filename => {
if (path.extname(filename) !== ".json") {
return;
};
const filePath = path.join(__dirname, "./models", filename);
const fileData = fs.readFileSync(filePath, "utf8");
const modelData = JSON.parse(fileData);
if (supportedModels.includes(modelData.parent) && !ignoreList.includes(filename)) {
candidateAtlasBlocks.push({filePath: filePath, modelData: modelData});
} else {
++ignoredCount;
const parsedPath = path.parse(filePath);
const modelName = parsedPath.name;
if (ignoreList.includes(filename)) {
return;
}
let faceData: {[face: string]: Texture} = {};
switch (modelData.parent) {
case parentModel.CubeAll:
faceData = {
up: { name: modelData.textures.all },
down: { name: modelData.textures.all },
north: { name: modelData.textures.all },
south: { name: modelData.textures.all },
east: { name: modelData.textures.all },
west: { name: modelData.textures.all }
}
break;
case parentModel.CubeColumn:
faceData = {
up: { name: modelData.textures.end },
down: { name: modelData.textures.end },
north: { name: modelData.textures.side },
south: { name: modelData.textures.side },
east: { name: modelData.textures.side },
west: { name: modelData.textures.side }
}
break;
case parentModel.Cube:
faceData = {
up: { name: modelData.textures.up },
down: { name: modelData.textures.down },
north: { name: modelData.textures.north },
south: { name: modelData.textures.south },
east: { name: modelData.textures.east },
west: { name: modelData.textures.west }
}
break;
default:
return;
}
for (const face of faces) {
usedTextures.add(faceData[face].name);
}
allModels.push({
name: modelName,
faces: faceData
});
});
assert(candidateAtlasBlocks.length > 0, "No blocks supplied are supported");
logSuccess(`Collected ${candidateAtlasBlocks.length} blocks, ignored ${ignoredCount} unsupported blocks`);
log(LogStyle.Success, `${allModels.length} blocks loaded\n`);
logStatus("Loading block textures...");
let textureAbsolutePaths = [];
let atlasBlocks: Array<BlockData & BlockPNGData> = [];
for (const blockData of candidateAtlasBlocks) {
const atlasBlock: (BlockData & BlockPNGData) = {
filePath: blockData.filePath,
modelData: blockData.modelData,
pngData: {}
}
let texturesValid = true;
for (const key in blockData.modelData.textures) {
const texturePath = blockData.modelData.textures[key];
if (!doesTextureFileExists(texturePath)) {
// TODO: Use first frame
logWarning(`Texture for ${texturePath} does not exist in ./models, ignoring...`);
texturesValid = false;
break;
}
const textureData: PNGWithMetadata = getTextureData(texturePath);
if (textureData.width !== textureData.height) {
logWarning(`${texturePath} is an animated/non-square texture which is not yet supported, ignoring...`);
texturesValid = false;
break;
}
if (textureData.width !== 16 || textureData.height !== 16) {
logWarning(`${texturePath} is not a 16x16 texture and is not yet supported, ignoring...`);
texturesValid = false;
break;
}
if (textureData.width !== 16 || textureData.height !== 16) {
logWarning(`${texturePath} is not a 16x16 texture and is not yet supported, ignoring...`);
texturesValid = false;
break;
}
textureAbsolutePaths.push(getAbsoluteTexturePath(texturePath));
atlasBlock.pngData[key] = textureData;
}
if (texturesValid) {
atlasBlocks.push(atlasBlock);
}
}
assert(atlasBlocks.length > 0, "No blocks supplied have supported textures");
logSuccess(`Textures for ${atlasBlocks.length} block loaded`);
logStatus("Stitching textures together...");
const atlasSize = Math.ceil(Math.sqrt(textureAbsolutePaths.length));
const atlasSize = Math.ceil(Math.sqrt(usedTextures.size));
const atlasWidth = atlasSize * 16;
let atlasElements = [];
let x = 0;
let y = 0;
for (let i = 0; i < textureAbsolutePaths.length; ++i) {
atlasElements.push({
src: textureAbsolutePaths[i],
offsetX: 3 * 16 * x++,
offsetY: 3 * 16 * y
})
if (x >= atlasSize) {
++y;
x = 0;
}
}
let offsetX = 0, offsetY = 0;
const outputImage = images(atlasWidth * 3, atlasWidth * 3);
let textureDetails: {[textureName: string]: {texcoord: UV, colour: RGB}} = {};
let outputBlocks = [];
let outputImage = images(atlasWidth * 3, atlasWidth * 3);
for (const element of atlasElements) {
// Tile each texture in a 3x3 grid to avoid UV bleeding
// TODO: Just repeat the outer layer of pixels instead of 3x3
const texture = images(element.src);
log(LogStyle.None, "Building blocks.png...");
usedTextures.forEach(textureName => {
const shortName = textureName.split("/")[1]; // Eww
const absolutePath = path.join(__dirname, "./blocks", shortName + ".png");
const fileData = fs.readFileSync(absolutePath);
const pngData = PNG.sync.read(fileData);
const image = images(absolutePath);
for (let x = 0; x < 3; ++x) {
for (let y = 0; y < 3; ++y) {
outputImage.draw(texture, element.offsetX + 16 * x, element.offsetY + 16 * y);
outputImage.draw(image, 16 * (3 * offsetX + x), 16 * (3 * offsetY + y));
}
}
outputBlocks.push({
colour: getAverageColour(element.src),
textureDetails[textureName] = {
texcoord: {
u: (element.offsetX + 16) / (atlasWidth * 3),
v: (element.offsetY + 16) / (atlasWidth * 3),
u: 16 * (3 * offsetX + 1) / (atlasWidth * 3),
v: 16 * (3 * offsetY + 1) / (atlasWidth * 3)
},
name: path.parse(element.src).name
});
colour: getAverageColour(pngData)
}
++offsetX;
if (offsetX >= atlasSize) {
++offsetY;
offsetX = 0;
}
});
// Build up the output JSON
log(LogStyle.None, "Building blocks.json...\n");
for (const model of allModels) {
let blockColour = {r: 0, g: 0, b: 0};
for (const face of faces) {
const faceTexture = textureDetails[model.faces[face].name];
const faceColour = faceTexture.colour;
blockColour.r += faceColour.r;
blockColour.g += faceColour.g;
blockColour.b += faceColour.b;
model.faces[face].texcoord = faceTexture.texcoord;
}
blockColour.r /= 6;
blockColour.g /= 6;
blockColour.b /= 6;
model.colour = blockColour;
}
logSuccess("Atlas texture created");
logStatus("Exporting...");
log(LogStyle.None, "Exporting...");
outputImage.save(path.join(__dirname, "../resources/blocks.png"));
logSuccess("blocks.png exported to /resources");
let outputJSON = { atlasSize: atlasSize, blocks: outputBlocks };
fs.writeFileSync(path.join(__dirname, "../resources/blocks.json"), JSON.stringify(outputJSON));
logSuccess("blocks.json exported to /resources");
log(LogStyle.Success, "blocks.png exported to /resources");
let outputJSON = { atlasSize: atlasSize, blocks: allModels };
fs.writeFileSync(path.join(__dirname, "../resources/blocks.json"), JSON.stringify(outputJSON, null, 4));
log(LogStyle.Success, "blocks.json exported to /resources\n");
console.log(chalk.cyanBright(chalk.inverse("DONE") + " Now run " + chalk.inverse("npm start") + " and the new blocks will be used"));

View File

@ -16,6 +16,7 @@ green_stained_glass.json
light_gray_stained_glass.json
magenta_stained_glass.json
pink_stained_glass.json
blue_stained_glass.json
purple_stained_glass.json
red_stained_glass.json
white_stained_glass.json

24
tools/logging.ts Normal file
View File

@ -0,0 +1,24 @@
import chalk from "chalk";
export enum LogStyle {
None = "None",
Info = "Info",
Warning = "Warning",
Failure = "Failure",
Success = "Success"
}
const LogStyleDetails: {[style: string]: {style: chalk.Chalk, prefix: string}} = {};
LogStyleDetails[LogStyle.Info] = {style: chalk.white, prefix: chalk.white.inverse("INFO")};
LogStyleDetails[LogStyle.Warning] = {style: chalk.yellow, prefix: chalk.yellow.inverse("WARN")};
LogStyleDetails[LogStyle.Failure] = {style: chalk.red, prefix: chalk.red.inverse("UHOH")};
LogStyleDetails[LogStyle.Success] = {style: chalk.green, prefix: chalk.green.inverse(" OK ")};
export function log(style: LogStyle, message: string) {
if (style === LogStyle.None) {
console.log(chalk.whiteBright(message));
} else {
const details: {style: chalk.Chalk, prefix: string} = LogStyleDetails[style];
console.log(details.prefix + " " + details.style(message));
}
}

View File

@ -1,15 +1,9 @@
import fs from "fs";
import path from "path";
import chalk from "chalk";
import { PNG, PNGWithMetadata } from "pngjs";
import { log, LogStyle } from "./logging";
export const logWarning = (message: string) => { console.log(chalk.yellow.inverse("WARN") + " " + chalk.yellow(message)); }
export const logFailure = (message: string) => { console.log(chalk.red.inverse("UHOH") + " " + chalk.red(message)); }
export const logStatus = (message: string) => { console.log("\n" + chalk.bold(message)); }
export const logInfo = (message: string) => { console.log(chalk.white.inverse("INFO") + " " + chalk.white(message)); }
export const logSuccess = (message: string) => { console.log(chalk.green.inverse(" OK ") + " " + chalk.green(message)); }
export const assert = (condition: boolean, onFailMessage: string) => { if (!condition) { logFailure(onFailMessage); process.exit(0); } }
export const assert = (condition: boolean, onFailMessage: string) => { if (!condition) { log(LogStyle.Failure, onFailMessage); process.exit(0); } }
export function isDirSetup(relativePath: string, jarAssetDir: string) {
const dir = path.join(__dirname, relativePath)
@ -20,35 +14,11 @@ export function isDirSetup(relativePath: string, jarAssetDir: string) {
} else {
fs.mkdirSync(dir);
}
logWarning(`Copy the contents of .minecraft/versions/<version>/<version>.jar/${jarAssetDir} from a Minecraft game files into ${relativePath}`);
log(LogStyle.Warning, `Copy the contents of .minecraft/versions/<version>/<version>.jar/${jarAssetDir} from a Minecraft game files into ${relativePath}`);
return false;
}
export function getTextureName(textureName: string) {
return getShortTextureName(textureName).split(".")[0];
}
export function getShortTextureName(textureName: string) {
return textureName.split("/")[1]; //TODO: Eww
}
export function getAbsoluteTexturePath(textureName: string) {
return path.join(__dirname, "./blocks", getShortTextureName(textureName) + ".png");
}
export function doesTextureFileExists(textureName: string) {
return fs.existsSync(getAbsoluteTexturePath(textureName));
}
export function getTextureData(textureName: string): PNGWithMetadata {
const fileData = fs.readFileSync(getAbsoluteTexturePath(textureName));
return PNG.sync.read(fileData);
}
export function getAverageColour(path: string) {
const data = fs.readFileSync(path);
const image = PNG.sync.read(data);
export function getAverageColour(image: PNG) {
let r = 0, g = 0, b = 0;
for (let x = 0; x < image.width; ++x) {
for (let y = 0; y < image.height; ++y) {