diff --git a/prismarine-viewer/package.json b/prismarine-viewer/package.json index 6fb40075..2832224f 100644 --- a/prismarine-viewer/package.json +++ b/prismarine-viewer/package.json @@ -8,7 +8,7 @@ "pretest": "npm run lint", "lint": "standard", "fix": "standard --fix", - "postinstall": "node viewer/generateTextures.js && node buildWorker.mjs" + "postinstall": "tsx viewer/prepare/generateTextures.ts && node buildWorker.mjs" }, "author": "PrismarineJS", "license": "MIT", diff --git a/prismarine-viewer/viewer/generateTextures.js b/prismarine-viewer/viewer/generateTextures.js deleted file mode 100644 index 796c5652..00000000 --- a/prismarine-viewer/viewer/generateTextures.js +++ /dev/null @@ -1,31 +0,0 @@ -const path = require('path') -const { makeTextureAtlas } = require('./lib/atlas') -const { prepareBlocksStates } = require('./lib/modelsBuilder') -const mcAssets = require('minecraft-assets') -const fs = require('fs-extra') - -const texturesPath = path.resolve(__dirname, '../public/textures') -if (fs.existsSync(texturesPath) && !process.argv.includes('-f')) { - console.log('textures folder already exists, skipping...') - process.exit(0) -} -fs.mkdirSync(texturesPath, { recursive: true }) - -const blockStatesPath = path.resolve(__dirname, '../public/blocksStates') -fs.mkdirSync(blockStatesPath, { recursive: true }) - -for (const version of mcAssets.versions) { - const assets = mcAssets(version) - const atlas = makeTextureAtlas(assets) - const out = fs.createWriteStream(path.resolve(texturesPath, version + '.png')) - const stream = atlas.canvas.pngStream() - stream.on('data', (chunk) => out.write(chunk)) - stream.on('end', () => console.log('Generated textures/' + version + '.png')) - - const blocksStates = JSON.stringify(prepareBlocksStates(assets, atlas)) - fs.writeFileSync(path.resolve(blockStatesPath, version + '.json'), blocksStates) - - fs.copySync(assets.directory, path.resolve(texturesPath, version), { overwrite: true }) -} - -fs.writeFileSync(path.resolve(__dirname, '../public/supportedVersions.json'), '[' + mcAssets.versions.map(v => `"${v}"`).toString() + ']') diff --git a/prismarine-viewer/viewer/lib/entities.js b/prismarine-viewer/viewer/lib/entities.js index 9c2c0029..ed9459d9 100644 --- a/prismarine-viewer/viewer/lib/entities.js +++ b/prismarine-viewer/viewer/lib/entities.js @@ -5,26 +5,26 @@ const Entity = require('./entity/Entity') const { dispose3 } = require('./dispose') function getUsernameTexture(username, { fontFamily = 'sans-serif' }) { -const canvas = document.createElement('canvas'); -const ctx = canvas.getContext('2d'); + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') -const fontSize = 50; -const padding = 5 -ctx.font = `${fontSize}px ${fontFamily}`; + const fontSize = 50 + const padding = 5 + ctx.font = `${fontSize}px ${fontFamily}` -const textWidth = ctx.measureText(username).width + padding * 2; + const textWidth = ctx.measureText(username).width + padding * 2 -canvas.width = textWidth; -canvas.height = fontSize + padding * 2; + canvas.width = textWidth + canvas.height = fontSize + padding * 2 -ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; -ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = 'rgba(0, 0, 0, 0.3)' + ctx.fillRect(0, 0, canvas.width, canvas.height) -ctx.font = `${fontSize}px ${fontFamily}`; -ctx.fillStyle = 'white' -ctx.fillText(username, padding, fontSize); + ctx.font = `${fontSize}px ${fontFamily}` + ctx.fillStyle = 'white' + ctx.fillText(username, padding, fontSize) -return canvas; + return canvas } function getEntityMesh (entity, scene, options) { @@ -33,7 +33,7 @@ function getEntityMesh (entity, scene, options) { const e = new Entity('1.16.4', entity.name, scene) if (entity.username !== undefined) { - const canvas = getUsernameTexture(entity.username, options); + const canvas = getUsernameTexture(entity.username, options) const tex = new THREE.Texture(canvas) tex.needsUpdate = true const spriteMat = new THREE.SpriteMaterial({ map: tex }) @@ -51,7 +51,7 @@ function getEntityMesh (entity, scene, options) { const geometry = new THREE.BoxGeometry(entity.width, entity.height, entity.width) geometry.translate(0, entity.height / 2, 0) - const material = new THREE.MeshBasicMaterial({ color: 0xff00ff }) + const material = new THREE.MeshBasicMaterial({ color: 0xff_00_ff }) const cube = new THREE.Mesh(geometry, material) return cube } diff --git a/prismarine-viewer/viewer/lib/models.ts b/prismarine-viewer/viewer/lib/models.ts index bf5841e0..605cf2e9 100644 --- a/prismarine-viewer/viewer/lib/models.ts +++ b/prismarine-viewer/viewer/lib/models.ts @@ -484,7 +484,7 @@ function matchProperties (block, properties) { function getModelVariants (block) { // air, cave_air, void_air and so on... - if (block.name.endsWith('air')) return [] + if (block.name === 'air' || block.name.endsWith('_air')) return [] const state = blockStates[block.name] ?? blockStates.missing_texture if (!state) return [] if (state.variants) { diff --git a/prismarine-viewer/viewer/lib/modelsBuilder.js b/prismarine-viewer/viewer/lib/modelsBuilder.js deleted file mode 100644 index 58aca167..00000000 --- a/prismarine-viewer/viewer/lib/modelsBuilder.js +++ /dev/null @@ -1,139 +0,0 @@ -function cleanupBlockName (name) { - if (name.startsWith('block') || name.startsWith('minecraft:block')) return name.split('/')[1] - return name -} - -function getModel (name, blocksModels) { - name = cleanupBlockName(name) - const data = blocksModels[name] - if (!data) { - return null - } - - let model = { textures: {}, elements: [], ao: true } - - for (const axis in ['x', 'y', 'z']) { - if (axis in data) { - model[axis] = data[axis] - } - } - - if (data.parent) { - model = getModel(data.parent, blocksModels) - } - if (data.textures) { - Object.assign(model.textures, JSON.parse(JSON.stringify(data.textures))) - } - if (data.elements) { - model.elements = JSON.parse(JSON.stringify(data.elements)) - } - if (data.ambientocclusion !== undefined) { - model.ao = data.ambientocclusion - } - return model -} - -function prepareModel (model, texturesJson) { - // resolve texture names eg west: #all -> blocks/stone - for (const tex in model.textures) { - let root = model.textures[tex] - while (root.charAt(0) === '#') { - root = model.textures[root.substr(1)] - } - model.textures[tex] = root - } - for (const tex in model.textures) { - let name = model.textures[tex] - name = cleanupBlockName(name) - model.textures[tex] = texturesJson[name] - } - for (const elem of model.elements) { - for (const sideName of Object.keys(elem.faces)) { - const face = elem.faces[sideName] - - if (face.texture.charAt(0) === '#') { - face.texture = JSON.parse(JSON.stringify(model.textures[face.texture.substr(1)])) - } else { - let name = face.texture - name = cleanupBlockName(name) - face.texture = JSON.parse(JSON.stringify(texturesJson[name])) - } - - let uv = face.uv - if (!uv) { - const _from = elem.from - const _to = elem.to - - // taken from https://github.com/DragonDev1906/Minecraft-Overviewer/ - uv = { - north: [_to[0], 16 - _to[1], _from[0], 16 - _from[1]], - east: [_from[2], 16 - _to[1], _to[2], 16 - _from[1]], - south: [_from[0], 16 - _to[1], _to[0], 16 - _from[1]], - west: [_from[2], 16 - _to[1], _to[2], 16 - _from[1]], - up: [_from[0], _from[2], _to[0], _to[2]], - down: [_to[0], _from[2], _from[0], _to[2]] - }[sideName] - } - - const su = (uv[2] - uv[0]) * face.texture.su / 16 - const sv = (uv[3] - uv[1]) * face.texture.sv / 16 - face.texture.bu = face.texture.u + 0.5 * face.texture.su - face.texture.bv = face.texture.v + 0.5 * face.texture.sv - face.texture.u += uv[0] * face.texture.su / 16 - face.texture.v += uv[1] * face.texture.sv / 16 - face.texture.su = su - face.texture.sv = sv - } - } -} - -function resolveModel (name, blocksModels, texturesJson) { - const model = getModel(name, blocksModels) - prepareModel(model, texturesJson.textures) - return model -} - -function prepareBlocksStates (mcAssets, atlas) { - const blocksStates = mcAssets.blocksStates - mcAssets.blocksStates["missing_texture"] = { - "variants": { - "normal": { - "model": "missing_texture" - } - } - }, - mcAssets.blocksModels["missing_texture"] = { - "parent": "block/cube_all", - "textures": { - "all": "blocks/missing_texture" - } - } - for (const block of Object.values(blocksStates)) { - if (!block) continue - if (block.variants) { - for (const variant of Object.values(block.variants)) { - if (variant instanceof Array) { - for (const v of variant) { - v.model = resolveModel(v.model, mcAssets.blocksModels, atlas.json) - } - } else { - variant.model = resolveModel(variant.model, mcAssets.blocksModels, atlas.json) - } - } - } - if (block.multipart) { - for (const variant of block.multipart) { - if (variant.apply instanceof Array) { - for (const v of variant.apply) { - v.model = resolveModel(v.model, mcAssets.blocksModels, atlas.json) - } - } else { - variant.apply.model = resolveModel(variant.apply.model, mcAssets.blocksModels, atlas.json) - } - } - } - } - return blocksStates -} - -module.exports = { prepareBlocksStates } diff --git a/prismarine-viewer/viewer/lib/atlas.js b/prismarine-viewer/viewer/prepare/atlas.ts similarity index 57% rename from prismarine-viewer/viewer/lib/atlas.js rename to prismarine-viewer/viewer/prepare/atlas.ts index d36ccca3..23a93707 100644 --- a/prismarine-viewer/viewer/lib/atlas.js +++ b/prismarine-viewer/viewer/prepare/atlas.ts @@ -1,6 +1,6 @@ -const fs = require('fs') -const { Canvas, Image } = require('canvas') -const path = require('path') +import fs from 'fs' +import path from 'path' +import { Canvas, Image } from 'canvas' function nextPowerOfTwo (n) { if (n === 0) return 1 @@ -13,44 +13,50 @@ function nextPowerOfTwo (n) { return n + 1 } +const localTextures = ['missing_texture.png'] + function readTexture (basePath, name) { - if (name === 'missing_texture.png') { + if (localTextures.includes(name)) { // grab ./missing_texture.png basePath = __dirname } return fs.readFileSync(path.join(basePath, name), 'base64') } -function makeTextureAtlas (mcAssets) { +export function makeTextureAtlas (mcAssets) { const blocksTexturePath = path.join(mcAssets.directory, '/blocks') const textureFiles = fs.readdirSync(blocksTexturePath).filter(file => file.endsWith('.png')) - textureFiles.unshift('missing_texture.png') + textureFiles.unshift(...localTextures) const texSize = nextPowerOfTwo(Math.ceil(Math.sqrt(textureFiles.length))) const tileSize = 16 const imgSize = texSize * tileSize - const canvas = new Canvas(imgSize, imgSize, 'png') + const canvas = new Canvas(imgSize, imgSize, 'png' as any) const g = canvas.getContext('2d') const texturesIndex = {} + let offset = 0 for (const i in textureFiles) { - const x = (i % texSize) * tileSize - const y = Math.floor(i / texSize) * tileSize + const pos = +i + offset + const x = (pos % texSize) * tileSize + const y = Math.floor(pos / texSize) * tileSize const name = textureFiles[i].split('.')[0] - texturesIndex[name] = { u: x / imgSize, v: y / imgSize, su: tileSize / imgSize, sv: tileSize / imgSize } - const img = new Image() img.src = 'data:image/png;base64,' + readTexture(blocksTexturePath, textureFiles[i]) - g.drawImage(img, 0, 0, 16, 16, x, y, 16, 16) + const needsMoreWidth = img.width > tileSize + if (needsMoreWidth) { + console.log('needs more', name, img.width, img.height) + offset++ + } + const renderWidth = needsMoreWidth ? tileSize * 2 : tileSize + g.drawImage(img, 0, 0, img.width, img.height, x, y, renderWidth, tileSize) + + texturesIndex[name] = { u: x / imgSize, v: y / imgSize, su: renderWidth / imgSize, sv: tileSize / imgSize } } return { image: canvas.toBuffer(), canvas, json: { size: tileSize / imgSize, textures: texturesIndex } } } - -module.exports = { - makeTextureAtlas -} diff --git a/prismarine-viewer/viewer/prepare/generateTextures.ts b/prismarine-viewer/viewer/prepare/generateTextures.ts new file mode 100644 index 00000000..3a0b4388 --- /dev/null +++ b/prismarine-viewer/viewer/prepare/generateTextures.ts @@ -0,0 +1,37 @@ +import path from 'path' +import { makeTextureAtlas } from './atlas' +import { McAssets, prepareBlocksStates } from './modelsBuilder' +import mcAssets from 'minecraft-assets' +import fs from 'fs-extra' + +const publicPath = path.resolve(__dirname, '../../public') + +const texturesPath = path.join(publicPath, 'textures') +if (fs.existsSync(texturesPath) && !process.argv.includes('-f')) { + console.log('textures folder already exists, skipping...') + process.exit(0) +} +fs.mkdirSync(texturesPath, { recursive: true }) + +const blockStatesPath = path.join(publicPath, 'blocksStates') +fs.mkdirSync(blockStatesPath, { recursive: true }) + +Promise.resolve().then(async () => { + for (const version of mcAssets.versions) { + const assets = mcAssets(version) + // #region texture atlas + const atlas = makeTextureAtlas(assets) + const out = fs.createWriteStream(path.resolve(texturesPath, version + '.png')) + const stream = (atlas.canvas as any).pngStream() + stream.on('data', (chunk) => out.write(chunk)) + stream.on('end', () => console.log('Generated textures/' + version + '.png')) + // #endregion + + const blocksStates = JSON.stringify(prepareBlocksStates(assets, atlas)) + fs.writeFileSync(path.resolve(blockStatesPath, version + '.json'), blocksStates) + + fs.copySync(assets.directory, path.resolve(texturesPath, version), { overwrite: true }) + } + + fs.writeFileSync(path.join(publicPath, 'supportedVersions.json'), '[' + mcAssets.versions.map(v => `"${v}"`).toString() + ']') +}) diff --git a/prismarine-viewer/viewer/lib/missing_texture.png b/prismarine-viewer/viewer/prepare/missing_texture.png similarity index 100% rename from prismarine-viewer/viewer/lib/missing_texture.png rename to prismarine-viewer/viewer/prepare/missing_texture.png diff --git a/prismarine-viewer/viewer/prepare/modelsBuilder.ts b/prismarine-viewer/viewer/prepare/modelsBuilder.ts new file mode 100644 index 00000000..00fbdf94 --- /dev/null +++ b/prismarine-viewer/viewer/prepare/modelsBuilder.ts @@ -0,0 +1,251 @@ +type ModelBasic = { + model: string + x?: number + y?: number + uvlock?: boolean +} + +type BlockApplyModel = ModelBasic | (ModelBasic & { weight })[] + +type BlockStateCondition = { + [name: string]: string | number +} + +type BlockState = { + variants?: { + [name: string | ""]: BlockApplyModel + } + multipart?: { + when: { + [name: string]: string | number + } & { + OR?: BlockStateCondition[] + } + apply: BlockApplyModel + }[] +} + +type BlockModel = { + parent?: string + textures?: { + [name: string]: string + } + elements?: { + from: number[] + to: number[] + faces: { + [name: string]: { + texture: string + uv?: number[] + cullface?: string + } + } + }[] + ambientocclusion?: boolean + x?: number + y?: number + z?: number + ao?: boolean +} + +export type McAssets = { + blocksStates: { + [x: string]: BlockState + } + blocksModels: { + [x: string]: BlockModel + } + directory: string + version: string +} + +export type BlockStatesOutput = { + // states: { + [blockName: string]: ResolvedModel + // } + // defaults: { + // su: number + // sv: number + // } +} + +export type ResolvedModel = { + textures: { + [name: string]: { + u: number + v: number + su: number + sv: number + bu: number + bv: number + } + } + elements: { + from: number[] + to: number[] + faces: { + [name: string]: { + texture: { + u: number + v: number + su: number + sv: number + bu: number + bv: number + } + } + } + }[] + ao: boolean + x?: number + y?: number + z?: number +} + +export const addBlockAllModel = (mcAssets: McAssets, name: string, texture = name) => { + mcAssets.blocksStates[name] = { + "variants": { + "": { + "model": name + } + } + } + mcAssets.blocksModels[name] = { + "parent": "block/cube_all", + "textures": { + "all": `blocks/${texture}` + } + } +} + +function cleanupBlockName (name: string) { + if (name.startsWith('block') || name.startsWith('minecraft:block')) return name.split('/')[1] + return name +} + +const objectAssignStrict = >(target: T, source: Partial) => Object.assign(target, source) + +function getFinalModel (name: string, blocksModels: { [x: string]: BlockModel }) { + name = cleanupBlockName(name) + const input = blocksModels[name] + if (!input) { + return null + } + + let out: BlockModel | null = { + textures: {}, + elements: [], + ao: true, + x: input.x, + y: input.y, + z: input.z, + } + + if (input.parent) { + out = getFinalModel(input.parent, blocksModels) + if (!out) return null + } + if (input.textures) { + Object.assign(out.textures!, deepCopy(input.textures)) + } + if (input.elements) out.elements = deepCopy(input.elements) + if (input.ao !== undefined) out.ao = input.ao + return out +} + +const deepCopy = (obj) => JSON.parse(JSON.stringify(obj)) + +function prepareModel (model: BlockModel, texturesJson) { + const newModel = {} + + const getFinalTexture = (originalBlockName) => { + // texture name e.g. blocks/anvil_base + const cleanBlockName = cleanupBlockName(originalBlockName); + return {...texturesJson[cleanBlockName], __debugName: cleanBlockName} + } + + const finalTextures = [] + + // resolve texture names eg west: #all -> blocks/stone + for (const side in model.textures) { + let texture = model.textures[side] + + while (texture.charAt(0) === '#') { + texture = model.textures[texture.slice(1)] + } + + finalTextures[side] = getFinalTexture(texture) + } + + for (const elem of model.elements!) { + for (const sideName of Object.keys(elem.faces)) { + const face = elem.faces[sideName] + + const finalTexture = deepCopy( + face.texture.charAt(0) === '#' + ? finalTextures![face.texture.slice(1)] + : getFinalTexture(face.texture) + ) + + const _from = elem.from + const _to = elem.to + // taken from https://github.com/DragonDev1906/Minecraft-Overviewer/ + const uv = face.uv || { + // default UVs + // format: [u1, v1, u2, v2] (u = x, v = y) + north: [_to[0], 16 - _to[1], _from[0], 16 - _from[1]], + east: [_from[2], 16 - _to[1], _to[2], 16 - _from[1]], + south: [_from[0], 16 - _to[1], _to[0], 16 - _from[1]], + west: [_from[2], 16 - _to[1], _to[2], 16 - _from[1]], + up: [_from[0], _from[2], _to[0], _to[2]], + down: [_to[0], _from[2], _from[0], _to[2]] + }[sideName]! + + const su = (uv[2] - uv[0]) / 16 * finalTexture.su + const sv = (uv[3] - uv[1]) / 16 * finalTexture.sv + finalTexture.u += uv[0] / 16 * finalTexture.su + finalTexture.v += uv[1] / 16 * finalTexture.sv + finalTexture.su = su + finalTexture.sv = sv + face.texture = finalTexture + } + } + return model +} + +function resolveModel (name, blocksModels, texturesJson) { + const model = getFinalModel(name, blocksModels) + return prepareModel(model, texturesJson.textures) +} + +export function prepareBlocksStates (mcAssets: McAssets, atlas: { json: any }) { + addBlockAllModel(mcAssets, 'missing_texture') + + const blocksStates = mcAssets.blocksStates + for (const block of Object.values(blocksStates)) { + if (!block) continue + if (block.variants) { + for (const variant of Object.values(block.variants)) { + if (variant instanceof Array) { + for (const v of variant) { + v.model = resolveModel(v.model, mcAssets.blocksModels, atlas.json) as any + } + } else { + variant.model = resolveModel(variant.model, mcAssets.blocksModels, atlas.json) as any + } + } + } + if (block.multipart) { + for (const variant of block.multipart) { + if (variant.apply instanceof Array) { + for (const v of variant.apply) { + v.model = resolveModel(v.model, mcAssets.blocksModels, atlas.json) as any + } + } else { + variant.apply.model = resolveModel(variant.apply.model, mcAssets.blocksModels, atlas.json) as any + } + } + } + } + return blocksStates +}