From 8774cb229162bc080be4af617d0d11e39cd9af5e Mon Sep 17 00:00:00 2001 From: Jochen Jacobs Date: Tue, 3 Dec 2024 22:00:04 +0100 Subject: [PATCH] Item renderer update (#41) * implement default components and component removal * start update item renderer * implement range dispatch * refactor Color to its own namespace, add fromJson * implement item tints * fix imports * update demo and fixes * fix circular dependencies * add item component string parser * don't error on missing item_model component & map color fix * fix circular dependencies * start special renderer * update from 24w46a changed default component handling again * fix imports * store id in itemstack * implement most special models * improve item registry * implement bundle/selected_item * implement bundle/fullness * remove local_time, fix chest special renderer * minor fixes * add tests * fix defaults of properties and tints * add more tests and a few fixes * undo unnecessary formatting changes * update item_definition url * add changes from 1.21.4-pre1 --- demo/index.html | 1 + demo/main.ts | 39 +- src/core/Effects.ts | 9 +- src/core/Item.ts | 31 + src/core/ItemStack.ts | 77 +- src/core/index.ts | 2 + src/nbt/tags/Util.ts | 23 + src/nbt/tags/index.ts | 2 + src/render/BlockColors.ts | 15 +- src/render/BlockModel.ts | 2 +- src/render/ItemColors.ts | 210 ----- src/render/ItemModel.ts | 392 +++++++++ src/render/ItemRenderer.ts | 90 +- src/render/ItemTint.ts | 179 ++++ src/render/SpecialModel.ts | 230 +++++ src/render/SpecialRenderer.ts | 1504 ++++++++++++++++----------------- src/render/index.ts | 2 +- src/util/Color.ts | 29 + src/util/Util.ts | 10 +- src/util/index.ts | 2 + test/render/ItemModel.test.ts | 301 +++++++ test/render/ItemTint.test.ts | 80 ++ 22 files changed, 2185 insertions(+), 1045 deletions(-) create mode 100644 src/core/Item.ts create mode 100644 src/nbt/tags/Util.ts delete mode 100644 src/render/ItemColors.ts create mode 100644 src/render/ItemModel.ts create mode 100644 src/render/ItemTint.ts create mode 100644 src/render/SpecialModel.ts create mode 100644 src/util/Color.ts create mode 100644 test/render/ItemModel.test.ts create mode 100644 test/render/ItemTint.test.ts diff --git a/demo/index.html b/demo/index.html index a4df886..83acbc4 100644 --- a/demo/index.html +++ b/demo/index.html @@ -20,6 +20,7 @@ input { padding: 4px; margin-left: 8px; + width: 500px; } .invalid { color: #cb0000; diff --git a/demo/main.ts b/demo/main.ts index c7c244f..00b2a7e 100644 --- a/demo/main.ts +++ b/demo/main.ts @@ -1,6 +1,8 @@ import { mat4 } from 'gl-matrix' -import type { Resources, Voxel } from '../src/index.js' -import { BlockDefinition, BlockModel, Identifier, ItemRenderer, ItemStack, NormalNoise, Structure, StructureRenderer, TextureAtlas, upperPowerOfTwo, VoxelRenderer, XoroshiroRandom } from '../src/index.js' +import type { ItemRendererResources, ItemRenderingContext, NbtTag, Resources, Voxel } from '../src/index.js' +import { BlockDefinition, BlockModel, Identifier, Item, ItemRenderer, ItemStack, NormalNoise, Structure, StructureRenderer, TextureAtlas, VoxelRenderer, XoroshiroRandom, jsonToNbt, upperPowerOfTwo } from '../src/index.js' +import { } from '../src/nbt/Util.js' +import { ItemModel } from '../src/render/ItemModel.js' class InteractiveCanvas { @@ -66,6 +68,8 @@ Promise.all([ fetch(`${MCMETA}registries/item/data.min.json`).then(r => r.json()), fetch(`${MCMETA}summary/assets/block_definition/data.min.json`).then(r => r.json()), fetch(`${MCMETA}summary/assets/model/data.min.json`).then(r => r.json()), + fetch(`${MCMETA}summary/assets/item_definition/data.min.json`).then(r => r.json()), + fetch(`${MCMETA}summary/item_components/data.min.json`).then(r => r.json()), fetch(`${MCMETA}atlas/all/data.min.json`).then(r => r.json()), new Promise(res => { const image = new Image() @@ -73,7 +77,7 @@ Promise.all([ image.crossOrigin = 'Anonymous' image.src = `${MCMETA}atlas/all/atlas.png` }), -]).then(([items, blockstates, models, uvMap, atlas]) => { +]).then(([items, blockstates, models, item_models, item_components, uvMap, atlas]) => { // === Prepare assets for item and structure rendering === @@ -97,6 +101,21 @@ Promise.all([ }) Object.values(blockModels).forEach((m: any) => m.flatten({ getBlockModel: id => blockModels[id] })) + + const itemModels: Record = {} + Object.keys(item_models).forEach(id => { + itemModels['minecraft:' + id] = ItemModel.fromJson(item_models[id].model) + }) + + + Object.keys(item_components).forEach(id => { + const components = new Map() + Object.keys(item_components[id]).forEach(c_id => { + components.set(c_id, jsonToNbt(item_components[id][c_id])) + }) + Item.REGISTRY.register(Identifier.create(id), new Item(components)) + }) + const atlasCanvas = document.createElement('canvas') const atlasSize = upperPowerOfTwo(Math.max(atlas.width, atlas.height)) atlasCanvas.width = atlasSize @@ -112,7 +131,7 @@ Promise.all([ }) const textureAtlas = new TextureAtlas(atlasData, idMap) - const resources: Resources = { + const resources: Resources & ItemRendererResources = { getBlockDefinition(id) { return blockDefinitions[id.toString()] }, getBlockModel(id) { return blockModels[id.toString()] }, getTextureUV(id) { return textureAtlas.getTextureUV(id) }, @@ -120,20 +139,28 @@ Promise.all([ getBlockFlags(id) { return { opaque: false } }, getBlockProperties(id) { return null }, getDefaultBlockProperties(id) { return null }, + getItemModel(id) { return itemModels[id.toString()] }, } // === Item rendering === + const context: ItemRenderingContext = { + "bundle/selected_item": 0 + } + const itemCanvas = document.getElementById('item-display') as HTMLCanvasElement const itemGl = itemCanvas.getContext('webgl')! const itemInput = document.getElementById('item-input') as HTMLInputElement itemInput.value = localStorage.getItem('deepslate_demo_item') ?? 'stone' - const itemRenderer = new ItemRenderer(itemGl, Identifier.parse(itemInput.value), resources) + const itemStack = ItemStack.fromString(itemInput.value) + const itemRenderer = new ItemRenderer(itemGl, itemStack, resources, context) itemInput.addEventListener('keyup', () => { try { const id = itemInput.value - itemRenderer.setItem(new ItemStack(Identifier.parse(id), 1)) + const itemStack = ItemStack.fromString(itemInput.value) + itemGl.clear(itemGl.DEPTH_BUFFER_BIT | itemGl.COLOR_BUFFER_BIT); + itemRenderer.setItem(itemStack, context) itemRenderer.drawItem() itemInput.classList.remove('invalid') localStorage.setItem('deepslate_demo_item', id) diff --git a/src/core/Effects.ts b/src/core/Effects.ts index 7e968c2..9445804 100644 --- a/src/core/Effects.ts +++ b/src/core/Effects.ts @@ -1,7 +1,6 @@ import type { NbtCompound, NbtTag } from '../nbt/index.js' import { NbtType } from '../nbt/index.js' -import type { Color } from '../util/index.js' -import { intToRgb } from '../util/index.js' +import { Color } from '../util/index.js' import { Identifier } from './Identifier.js' export const EFFECT_COLORS = new Map([ @@ -139,7 +138,7 @@ export namespace PotionContents { export function getColor(contents: PotionContents): Color { if (contents.customColor) { - return intToRgb(contents.customColor) + return Color.intToRgb(contents.customColor) } const effects = getAllEffects(contents) return mixEffectColors(effects) @@ -162,7 +161,7 @@ export namespace PotionContents { for (const effect of effects) { const color = EFFECT_COLORS.get(effect.effect.toString()) if (color === undefined) continue - const rgb = intToRgb(color) + const rgb = Color.intToRgb(color) const amplifier = effect.amplifier + 1 r += amplifier * rgb[0] g += amplifier * rgb[1] @@ -170,7 +169,7 @@ export namespace PotionContents { total += amplifier } if (total === 0) { - return intToRgb(-13083194) + return Color.intToRgb(-13083194) } r = r / total g = g / total diff --git a/src/core/Item.ts b/src/core/Item.ts new file mode 100644 index 0000000..22d5f1f --- /dev/null +++ b/src/core/Item.ts @@ -0,0 +1,31 @@ +import { NbtTag } from "../nbt/index.js" +import { Identifier } from "./index.js" +import { Registry } from "./Registry.js" + + +export class Item { + public static REGISTRY = Registry.createAndRegister('item') + + constructor( + public components: Map = new Map(), + ) { + } + + public getComponent(key: string | Identifier, reader: (tag: NbtTag) => T) { + if (typeof key === 'string') { + key = Identifier.parse(key) + } + const value = this.components.get(key.toString()) + if (value) { + return reader(value) + } + return undefined + } + + public hasComponent(key: string | Identifier) { + if (typeof key === 'string') { + key = Identifier.parse(key) + } + return this.components.has(key.toString()) + } +} \ No newline at end of file diff --git a/src/core/ItemStack.ts b/src/core/ItemStack.ts index 2c99b36..8bd829c 100644 --- a/src/core/ItemStack.ts +++ b/src/core/ItemStack.ts @@ -1,30 +1,46 @@ +import { NbtParser } from '../nbt/NbtParser.js' import type { NbtTag } from '../nbt/index.js' import { NbtCompound, NbtInt, NbtString } from '../nbt/index.js' +import { StringReader } from '../util/index.js' +import { Holder } from './Holder.js' import { Identifier } from './Identifier.js' +import { Item } from './Item.js' export class ItemStack { + private readonly item: Holder + constructor( - public id: Identifier, + public readonly id: Identifier, public count: number, - public components: Map = new Map(), - ) {} + public readonly components: Map = new Map(), + ) { + this.item = Holder.reference(Item.REGISTRY, id, false) + } - public getComponent(key: string | Identifier, reader: (tag: NbtTag) => T) { + public getComponent(key: string | Identifier, reader: (tag: NbtTag) => T, includeDefaultComponents: boolean = true): T | undefined { if (typeof key === 'string') { key = Identifier.parse(key) } + + if (this.components.has('!' + key.toString())){ + return undefined + } const value = this.components.get(key.toString()) if (value) { return reader(value) } - return undefined + return includeDefaultComponents ? this.item.value()?.getComponent(key, reader) : undefined } - public hasComponent(key: string | Identifier) { + public hasComponent(key: string | Identifier, includeDefaultComponents: boolean = true): boolean { if (typeof key === 'string') { key = Identifier.parse(key) } - return this.components.has(key.toString()) + if (this.components.has('!' + key.toString())){ + return false + } + + return this.components.has(key.toString()) || (includeDefaultComponents && (this.item.value()?.hasComponent(key) ?? false)) } public clone(): ItemStack { @@ -77,6 +93,53 @@ export class ItemStack { return result } + public static fromString(string: string) { + const reader = new StringReader(string) + + while (reader.canRead() && reader.peek() !== '[') { + reader.skip() + } + const itemId = Identifier.parse(reader.getRead()) + if (!reader.canRead()){ + return new ItemStack(itemId, 1) + } + + const components = new Map() + reader.skip() + if (reader.peek() === ']'){ + return new ItemStack(itemId, 1, components) + } + do{ + if (reader.peek() === '!'){ + reader.skip() + const start = reader.cursor + while (reader.canRead() && reader.peek() !== ']' && reader.peek() !== ',') { + reader.skip() + } + components.set('!' + Identifier.parse(reader.getRead(start)).toString(), new NbtCompound()) + } else { + const start = reader.cursor + while (reader.canRead() && reader.peek() !== '=') { + reader.skip() + } + const component = Identifier.parse(reader.getRead(start)).toString() + if (!reader.canRead()) break; + reader.skip() + const tag = NbtParser.readTag(reader) + components.set(component, tag) + } + if (!reader.canRead()) break; + if (reader.peek() === ']'){ + return new ItemStack(itemId, 1, components) + } + if (reader.peek() !== ','){ + throw new Error('Expected , or ]') + } + reader.skip() + } while (reader.canRead()) + throw new Error('Missing closing ]') + } + public toNbt() { const result = new NbtCompound() .set('id', new NbtString(this.id.toString())) diff --git a/src/core/index.ts b/src/core/index.ts index bdb8c1f..fba2592 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -8,9 +8,11 @@ export * from './Effects.js' export * from './Holder.js' export * from './HolderSet.js' export * from './Identifier.js' +export * from './Item.js' export * from './ItemStack.js' export * from './PalettedContainer.js' export * from './Registry.js' export * from './Rotation.js' export * from './Structure.js' export * from './StructureProvider.js' + diff --git a/src/nbt/tags/Util.ts b/src/nbt/tags/Util.ts new file mode 100644 index 0000000..e5cf0ef --- /dev/null +++ b/src/nbt/tags/Util.ts @@ -0,0 +1,23 @@ +import { NbtByte, NbtCompound, NbtDouble, NbtInt, NbtList, NbtString, NbtTag } from "./index.js" + +export function jsonToNbt(value: unknown): NbtTag { + if (typeof value === 'string') { + return new NbtString(value) + } + if (typeof value === 'number') { + return Number.isInteger(value) ? new NbtInt(value) : new NbtDouble(value) + } + if (typeof value === 'boolean') { + return new NbtByte(value) + } + if (Array.isArray(value)) { + return new NbtList(value.map(jsonToNbt)) + } + if (typeof value === 'object' && value !== null) { + return new NbtCompound( + new Map(Object.entries(value ?? {}) + .map(([k, v]) => [k, jsonToNbt(v)])) + ) + } + return new NbtByte(0) +} diff --git a/src/nbt/tags/index.ts b/src/nbt/tags/index.ts index a0ed412..5a09ad1 100644 --- a/src/nbt/tags/index.ts +++ b/src/nbt/tags/index.ts @@ -14,3 +14,5 @@ export * from './NbtShort.js' export * from './NbtString.js' export * from './NbtTag.js' export * from './NbtType.js' +export * from './Util.js' + diff --git a/src/render/BlockColors.ts b/src/render/BlockColors.ts index 7787dd6..2290cd3 100644 --- a/src/render/BlockColors.ts +++ b/src/render/BlockColors.ts @@ -1,14 +1,13 @@ import { clamp } from '../math/index.js' -import type { Color } from '../util/index.js' -import { intToRgb } from '../util/index.js' +import { Color } from '../util/index.js' const grass: Color = [124 / 255, 189 / 255, 107 / 255] -const spruce = intToRgb(6396257) -const birch = intToRgb(8431445) -const foliage = intToRgb(4764952) -const water = intToRgb(4159204) -const attached_stem = intToRgb(8431445) -const lily_pad = intToRgb(2129968) +const spruce = Color.intToRgb(6396257) +const birch = Color.intToRgb(8431445) +const foliage = Color.intToRgb(4764952) +const water = Color.intToRgb(4159204) +const attached_stem = Color.intToRgb(8431445) +const lily_pad = Color.intToRgb(2129968) const redstone = (power: number): Color => { const a = power / 15 diff --git a/src/render/BlockModel.ts b/src/render/BlockModel.ts index 8ae5f50..672badc 100644 --- a/src/render/BlockModel.ts +++ b/src/render/BlockModel.ts @@ -11,7 +11,7 @@ import type { TextureAtlasProvider, UV } from './TextureAtlas.js' type Axis = 'x' | 'y' | 'z' -type Display = 'thirdperson_righthand' | 'thirdperson_lefthand' | 'firstperson_righthand' | 'firstperson_lefthand' | 'gui' | 'head' | 'ground' | 'fixed' +export type Display = 'thirdperson_righthand' | 'thirdperson_lefthand' | 'firstperson_righthand' | 'firstperson_lefthand' | 'gui' | 'head' | 'ground' | 'fixed' | 'none' type BlockModelFace = { texture: string, diff --git a/src/render/ItemColors.ts b/src/render/ItemColors.ts deleted file mode 100644 index fee65e7..0000000 --- a/src/render/ItemColors.ts +++ /dev/null @@ -1,210 +0,0 @@ -import type { ItemStack } from '../core/index.js' -import { Identifier, PotionContents } from '../core/index.js' -import { NbtIntArray } from '../index.js' -import type { Color } from '../util/index.js' -import { intToRgb } from '../util/index.js' -import { BlockColors } from './BlockColors.js' - -type Tint = Color | ((index: number) => Color) -const ItemColors = new Map Tint>() - -export function getItemColor(item: ItemStack): Tint { - if (item.id.namespace !== Identifier.DEFAULT_NAMESPACE) return [1, 1, 1] - const tint = ItemColors.get(item.id.path) - return tint ? tint(item) : [1, 1, 1] -} - -function register(items: string[], fn: (item: ItemStack) => Tint) { - for (const item of items) { - ItemColors.set(item, fn) - } -} - -function getDyedColor(item: ItemStack, fallback: number) { - const dyedColor = item.getComponent('dyed_color', tag => { - return tag.isCompound() ? tag.getNumber('rgb') : tag.getAsNumber() - }) - return intToRgb(dyedColor ?? fallback) -} - -register([ - 'leather_helmet', - 'leather_chestplate', - 'leather_leggings', - 'leather_boots', - 'leather_horse_armor', -], item => { - const color = getDyedColor(item, -6265536) - return (index: number) => index > 0 ? [1, 1, 1] : color -}) - -register([ - 'wolf_armor', -], item => { - const color = getDyedColor(item, 0) - return (index: number) => index !== 1 ? [1, 1, 1] : color -}) - -register([ - 'tall_grass', - 'large_fern', -], () => [124 / 255, 189 / 255, 107 / 255]) - -register([ - 'firework_star', -], item => { - const colors = item.getComponent('firework_explosion', tag => { - return tag.isCompound() ? tag.getIntArray('colors') : new NbtIntArray() - }) - const color: Color = (() => { - if (!colors || colors.length === 0) { - return intToRgb(9079434) - } - if (colors.length === 1) { - return intToRgb(colors.get(0)!.getAsNumber()) - } - let [r, g, b] = [0, 0, 0] - for (const color of colors.getItems()) { - r += (color.getAsNumber() & 0xFF0000) >> 16 - g += (color.getAsNumber() & 0xFF00) >> 8 - b += (color.getAsNumber() & 0xFF) >> 0 - } - r /= colors.length - g /= colors.length - b /= colors.length - return [r, g, b] - })() - return (index: number) => index !== 1 ? [1, 1, 1] : color -}) - -register([ - 'potion', - 'splash_potion', - 'lingering_potion', - 'tipped_arrow', -], item => { - const data = item.getComponent('potion_contents', PotionContents.fromNbt) - if (!data) { - return () => [1, 1, 1] - } - const color = PotionContents.getColor(data) - return (index: number) => index > 0 ? [1, 1, 1] : color -}) - -const SpawnEggs: [string, number, number][] = [ - ['allay', 56063, 44543], - ['armadillo', 11366765, 8538184], - ['axolotl', 16499171, 10890612], - ['bat', 4996656, 986895], - ['bee', 15582019, 4400155], - ['blaze', 16167425, 16775294], - ['bogged', 9084018, 3231003], - ['breeze', 11506911, 9529055], - ['cat', 15714446, 9794134], - ['camel', 16565097, 13341495], - ['cave_spider', 803406, 11013646], - ['chicken', 10592673, 16711680], - ['cod', 12691306, 15058059], - ['cow', 4470310, 10592673], - ['creeper', 894731, 0], - ['dolphin', 2243405, 16382457], - ['donkey', 5457209, 8811878], - ['drowned', 9433559, 7969893], - ['elder_guardian', 13552826, 7632531], - ['ender_dragon', 1842204, 14711290], - ['enderman', 1447446, 0], - ['endermite', 1447446, 7237230], - ['evoker', 9804699, 1973274], - ['fox', 14005919, 13396256], - ['frog', 13661252, 16762748], - ['ghast', 16382457, 12369084], - ['glow_squid', 611926, 8778172], - ['goat', 10851452, 5589310], - ['guardian', 5931634, 15826224], - ['hoglin', 13004373, 6251620], - ['horse', 12623485, 15656192], - ['husk', 7958625, 15125652], - ['iron_golem', 14405058, 7643954], - ['llama', 12623485, 10051392], - ['magma_cube', 3407872, 16579584], - ['mooshroom', 10489616, 12040119], - ['mule', 1769984, 5321501], - ['ocelot', 15720061, 5653556], - ['panda', 15198183, 1776418], - ['parrot', 894731, 16711680], - ['phantom', 4411786, 8978176], - ['pig', 15771042, 14377823], - ['piglin', 10051392, 16380836], - ['piglin_brute', 5843472, 16380836], - ['pillager', 5451574, 9804699], - ['polar_bear', 15658718, 14014157], - ['pufferfish', 16167425, 3654642], - ['rabbit', 10051392, 7555121], - ['ravager', 7697520, 5984329], - ['salmon', 10489616, 951412], - ['sheep', 15198183, 16758197], - ['shulker', 9725844, 5060690], - ['silverfish', 7237230, 3158064], - ['skeleton', 12698049, 4802889], - ['skeleton_horse', 6842447, 15066584], - ['slime', 5349438, 8306542], - ['sniffer', 8855049, 2468720], - ['snow_golem', 14283506, 8496292], - ['spider', 3419431, 11013646], - ['squid', 2243405, 7375001], - ['stray', 6387319, 14543594], - ['strider', 10236982, 5065037], - ['tadpole', 7164733, 1444352], - ['trader_llama', 15377456, 4547222], - ['tropical_fish', 15690005, 16775663], - ['turtle', 15198183, 44975], - ['vex', 8032420, 15265265], - ['villager', 5651507, 12422002], - ['vindicator', 9804699, 2580065], - ['wandering_trader', 4547222, 15377456], - ['warden', 1001033, 3790560], - ['witch', 3407872, 5349438], - ['wither', 1315860, 5075616], - ['wither_skeleton', 1315860, 4672845], - ['wolf', 14144467, 13545366], - ['zoglin', 13004373, 15132390], - ['zombie', 44975, 7969893], - ['zombie_horse', 3232308, 9945732], - ['zombie_villager', 5651507, 7969893], - ['zombified_piglin', 15373203, 5009705], -] - -for (const egg of SpawnEggs) { - register([`${egg[0]}_spawn_egg`], () => { - return (index: number) => intToRgb(index === 0 ? egg[1] : egg[2]) - }) -} - -for (const id of [ - 'grass_block', - 'short_grass', - 'fern', - 'vine', - 'oak_leaves', - 'spruce_leaves', - 'birch_leaves', - 'jungle_leaves', - 'acacia_leaves', - 'dark_oak_leaves', - 'lily_pad', -]) { - const color = BlockColors[id]({}) - register([id], () => color) -} - -register([ - 'mangrove_leaves', -], () => intToRgb(9619016)) - -register([ - 'filled_map', -], item => { - const mapColor = item.getComponent('map_color', tag => tag.getAsNumber()) - const color = intToRgb(mapColor ?? 4603950) - return (index: number) => index === 0 ? [1, 1, 1] : color -}) diff --git a/src/render/ItemModel.ts b/src/render/ItemModel.ts new file mode 100644 index 0000000..7dde809 --- /dev/null +++ b/src/render/ItemModel.ts @@ -0,0 +1,392 @@ +import { Identifier, ItemStack, } from "../core/index.js" +import { clamp } from "../math/index.js" +import { Color, Json } from "../util/index.js" +import { ItemTint } from "./ItemTint.js" +import { SpecialModel } from "./SpecialModel.js" +import { Cull, ItemRenderer, ItemRendererResources, ItemRenderingContext, Mesh } from "./index.js" + +export interface ItemModelProvider { + getItemModel(id: Identifier): ItemModel | null +} + +export abstract class ItemModel { + + public abstract getMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext): Mesh + +} + +const MISSING_MESH: Mesh = new Mesh() ///TODO + +export namespace ItemModel { + export function fromJson(obj: unknown): ItemModel { + const root = Json.readObject(obj) ?? {} + const type = Json.readString(root.type)?.replace(/^minecraft:/, '') + switch (type) { + case 'empty': return new Empty() + case 'model': return new Model( + Identifier.parse(Json.readString(root.model) ?? ''), + Json.readArray(root.tints, ItemTint.fromJson) ?? [] + ) + case 'composite': return new Composite( + Json.readArray(root.models, ItemModel.fromJson) ?? [] + ) + case 'condition': return new Condition( + Condition.propertyFromJson(root), + ItemModel.fromJson(root.on_true), + ItemModel.fromJson(root.on_false) + ) + case 'select': return new Select( + Select.propertyFromJson(root), + new Map(Json.readArray(root.cases, caseObj => { + const caseRoot = Json.readObject(caseObj) ?? {} + return [Json.readString(caseRoot.when) ?? '', ItemModel.fromJson(caseRoot.model)] + })), + root.fallback ? ItemModel.fromJson(root.fallback) : undefined + ) + case 'range_dispatch': return new RangeDispatch( + RangeDispatch.propertyFromJson(root), + Json.readNumber(root.scale) ?? 1, + Json.readArray(root.entries, entryObj => { + const entryRoot = Json.readObject(entryObj) ?? {} + return {threshold: Json.readNumber(entryRoot.threshold) ?? 0, model: ItemModel.fromJson(entryRoot.model)} + }) ?? [], + root.fallback ? ItemModel.fromJson(root.fallback) : undefined + ) + case 'special': return new Special( + SpecialModel.fromJson(root.model), + Identifier.parse(Json.readString(root.base) ?? '') + ) + case 'bundle/selected_item': return new BundleSelectedItem() + default: + throw new Error(`Invalid item model type ${type}`) + } + } + + export class Empty extends ItemModel { + public getMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext): Mesh{ + return new Mesh() + } + } + + export class Model extends ItemModel { + constructor( + private modelId: Identifier, + private tints: ItemTint[] + ) { + super() + } + + public getMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext): Mesh{ + const model = resources.getBlockModel(this.modelId) + if (!model) { + throw new Error(`Model ${this.modelId} does not exist (trying to render ${item.toString()})`) + } + + let tint = (i: number): Color => { + if (i < this.tints.length) { + return this.tints[i].getTint(item, context) + } else { + return [1, 1, 1] + } + } + + const mesh = model.getMesh(resources, Cull.none(), tint) + mesh.transform(model.getDisplayTransform(context.display_context ?? 'gui')) + return mesh + } + + } + + export class Composite extends ItemModel { + constructor( + private models: ItemModel[] + ) { + super() + } + + public getMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext): Mesh { + const mesh = new Mesh() + this.models.forEach(model => mesh.merge(model.getMesh(item, resources, context))) + return mesh + } + } + + export class Condition extends ItemModel { + constructor( + private property: (item: ItemStack, context: ItemRenderingContext) => boolean, + private onTrue: ItemModel, + private onFalse: ItemModel + ) { + super() + } + + public getMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext): Mesh { + return (this.property(item, context) ? this.onTrue : this.onFalse).getMesh(item, resources, context) + } + + static propertyFromJson(root: {[x: string]: unknown}): (item: ItemStack, context: ItemRenderingContext) => boolean{ + const property = Json.readString(root.property)?.replace(/^minecraft:/, '') + + switch (property){ + case 'fishing_rod/cast': + case 'selected': + case 'carried': + case 'extended_view': + return (item, context) => context[property] ?? false + case 'view_entity': + return (item, context) => context.context_entity_is_view_entity ?? false + case 'using_item': + return (item, context) => (context.use_duration ?? -1) >= 0 + case 'bundle/has_selected_item': + return (item, context) => (context['bundle/selected_item'] ?? -1) >= 0 + case 'broken': return (item, context) => { + const damage = item.getComponent('damage', tag => tag.getAsNumber()) + const max_damage = item.getComponent('max_damage', tag => tag.getAsNumber()) + return (damage !== undefined && max_damage !== undefined && damage >= max_damage - 1) + } + case 'damaged': return (item, context) => { + const damage = item.getComponent('damage', tag => tag.getAsNumber()) + const max_damage = item.getComponent('max_damage', tag => tag.getAsNumber()) + return (damage !== undefined && max_damage !== undefined && damage >= 1) + } + case 'has_component': + const componentId = Identifier.parse(Json.readString(root.component) ?? '') + const ignore_default = Json.readBoolean(root.ignore_default) ?? false + return (item, context) => item.hasComponent(componentId, !ignore_default) + case 'keybind_down': + const keybind = Json.readString(root.keybind) ?? '' + return (item, context) => context.keybind_down?.includes(keybind) ?? false + case 'custom_model_data': + const index = Json.readInt(root.index) ?? 0 + return (item, context) => item.getComponent('custom_model_data', tag => { + if (!tag.isCompound()) return false + const flag = tag.getList('flags').getNumber(index) + return flag !== undefined && flag !== 0 + }) ?? false + default: + throw new Error(`Invalid condition property ${property}`) + } + } + } + + export class Select extends ItemModel { + constructor( + private property: (item: ItemStack, context: ItemRenderingContext) => string | null, + private cases: Map, + private fallback?: ItemModel + ) { + super() + } + + public getMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext): Mesh { + const value = this.property(item, context) + return ((value !== null ? this.cases.get(value) : undefined) ?? this.fallback)?.getMesh(item, resources, context) ?? MISSING_MESH + } + + static propertyFromJson(root: {[x: string]: unknown}): (item: ItemStack, context: ItemRenderingContext) => string | null{ + const property = Json.readString(root.property)?.replace(/^minecraft:/, '') + + switch (property){ + case 'main_hand': + return (item, context) => context.main_hand ?? 'right' + case 'display_context': + return (item, context) => context.display_context ?? 'gui' + case 'context_dimension': + return (item, context) => context.context_dimension?.toString() ?? null + case 'charge_type': + const FIREWORK = Identifier.create('firework_rocket') + return (item, context) => item.getComponent('charged_projectiles', tag => { + if (!tag.isList() || tag.length === 0) { + return 'none' + } + return tag.filter(tag => { + if (!tag.isCompound()) { + return false + } + return Identifier.parse(tag.getString('id')).equals(FIREWORK) + }).length > 0 ? 'rocket' : 'arrow' + }) ?? 'none' + case 'trim_material': + return (item, context) => item.getComponent('trim', tag => { + if (!tag.isCompound()) { + return undefined + } + return Identifier.parse(tag.getString('material')).toString() + }) ?? null + case 'block_state': + const block_state_property = Json.readString(root.block_state_property) ?? '' + return (item, context) => item.getComponent('block_state', tag => { + if (!tag.isCompound()) { + return undefined + } + return tag.getString(block_state_property) + }) ?? null + case 'local_time': return (item, context) => 'NOT IMPLEMENTED' + case 'context_entity_type': + return (item, context) => context.context_entity_type?.toString() ?? null + case 'custom_model_data': + const index = Json.readInt(root.index) ?? 0 + return (item, context) => item.getComponent('custom_model_data', tag => { + if (!tag.isCompound()) return undefined + const list = tag.getList('strings') + if (list.length <= index) return undefined + return list.getString(index) + }) ?? null + default: + throw new Error(`Invalid select property ${property}`) + + } + } + } + + export class RangeDispatch extends ItemModel { + private entries: {threshold: number, model: ItemModel}[] + + constructor( + private property: (item: ItemStack, context: ItemRenderingContext) => number, + private scale: number, + entries: {threshold: number, model: ItemModel}[], + private fallback?: ItemModel + ) { + super() + this.entries = entries.sort((a, b) => a.threshold - b.threshold) + } + + public getMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext): Mesh { + const value = this.property(item, context) * this.scale + let model = this.fallback + for (const entry of this.entries) { + if (entry.threshold <= value) { + model = entry.model + } else { + break + } + } + return model?.getMesh(item, resources, context) ?? MISSING_MESH + } + + static propertyFromJson(root: {[x: string]: unknown}): (item: ItemStack, context: ItemRenderingContext) => number{ + const property = Json.readString(root.property)?.replace(/^minecraft:/, '') + + switch (property){ + case 'bundle/fullness': + function calculateBundleWeight(item: ItemStack): number{ + const bundle_contents = item.getComponent('bundle_contents', tag => { + if (!tag.isListOrArray()) return undefined + return tag.map(t => t.isCompound() ? ItemStack.fromNbt(t) : undefined) + }) + if (bundle_contents === undefined) return 0 + return bundle_contents.reduce((weight, item) => { + if (item === undefined) return weight + if (item.hasComponent('bundle_contents')) return weight + calculateBundleWeight(item) + 1/16 + if (item.getComponent('bees', tag => tag.isListOrArray() && tag.length > 0)) return weight + 1 + return weight + item.count / (item.getComponent('max_stack_size', tag => tag.getAsNumber()) ?? 1) + }, 0) + } + + return (item, context) => calculateBundleWeight(item) + case 'damage': { + const normalize = Json.readBoolean(root.normalize) ?? true + return (item, context) => { + const max_damage = item.getComponent('max_damage', tag => tag.getAsNumber()) ?? 0 + const damage = clamp(item.getComponent('damage', tag => tag.getAsNumber()) ?? 0, 0, max_damage) + if (normalize) return clamp(damage / max_damage, 0, 1) + return clamp(damage, 0, max_damage) + } + } + case 'count': { + const normalize = Json.readBoolean(root.normalize) ?? true + return (item, context) => { + const max_stack_size = item.getComponent('max_stack_size', tag => tag.getAsNumber()) ?? 1 + if (normalize) return clamp(item.count / max_stack_size, 0, 1) + return clamp(item.count, 0, max_stack_size) + } + } + case 'cooldown': return (item, context) => { + const cooldownGroup = item.getComponent('use_cooldown', tag => { + if (!tag.isCompound()) return undefined + return Identifier.parse(tag.getString('cooldown_group')) + }) ?? item.id + + return context.cooldown_percentage?.[cooldownGroup.toString()] ?? 0 + } + case 'time': + const source = Json.readString(root.source) ?? 'daytime' + switch (source) { + case 'daytime': return (item, context) => { + const gameTime = context.game_time ?? 0 + const linearTime = ((gameTime / 24000.0) % 1) - 0.25; + const cosTime = 0.5 - Math.cos(linearTime * Math.PI) / 2.0; + return (linearTime * 2.0 + cosTime) / 3; + } + case 'moon_phase': return (item, context) => ((context.game_time ?? 0) / 24000 % 8) / 8 + case 'random': return (item, context) => Math.random() + } + case 'compass': return (item, context) => context.compass_angle ?? 0 // TODO: calculate properly? + case 'crossbow/pull': return (item, context) => context['crossbow/pull'] ?? 0 + case 'use_duration': + const remaining = Json.readBoolean(root.remaining) ?? true + return (item, context) => { + if (context.use_duration === undefined || context.use_duration < 0) return 0 + if (remaining) return Math.max((context.max_use_duration ?? 0) - (context.use_duration), 0) + return context.use_duration + } + case 'use_cycle': + const period = Json.readNumber(root.period) ?? 1 + return (item, context) => { + if (context.use_duration === undefined || context.use_duration < 0) return 0 + return Math.max((context.max_use_duration ?? 0) - (context.use_duration ?? 0), 0) % period + } + case 'custom_model_data': + const index = Json.readInt(root.index) ?? 0 + return (item, context) => item.getComponent('custom_model_data', tag => { + if (!tag.isCompound()) return undefined + return tag.getList('floats').getNumber(index) + }) ?? 0 + default: + throw new Error(`Invalid select property ${property}`) + } + } + } + + export class Special extends ItemModel { + constructor( + private specialModel: SpecialModel, + private base: Identifier + ) { + super() + } + + public getMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext): Mesh { + const mesh = this.specialModel.getMesh(item, resources) + const model = resources.getBlockModel(this.base) + if (!model) { + throw new Error(`Base model ${this.base} does not exist (trying to render ${item.toString()})`) + } + mesh.transform(model.getDisplayTransform(context.display_context ?? 'gui')) + return mesh + } + } + + export class BundleSelectedItem extends ItemModel { + public getMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext): Mesh { + const selectedItemIndex = context['bundle/selected_item'] + if (selectedItemIndex === undefined || selectedItemIndex < 0) return new Mesh() + const selectedItem = item.getComponent('bundle_contents', tag => { + if (!tag.isListOrArray()) return undefined + const selectedItemTag = tag.get(selectedItemIndex) + if (selectedItemTag === undefined || !selectedItemTag.isCompound()) return undefined + return ItemStack.fromNbt(selectedItemTag) + }) + + return selectedItem !== undefined ? ItemRenderer.getItemMesh(selectedItem, resources, { + ...context, + 'bundle/selected_item': -1, + selected: false, + carried: false, + use_duration: -1 + }) : new Mesh() + } + } +} + diff --git a/src/render/ItemRenderer.ts b/src/render/ItemRenderer.ts index ade44c5..00594af 100644 --- a/src/render/ItemRenderer.ts +++ b/src/render/ItemRenderer.ts @@ -1,60 +1,82 @@ import { mat4 } from 'gl-matrix' -import { Identifier } from '../core/index.js' import { ItemStack } from '../core/ItemStack.js' -import { Cull, SpecialRenderers, type Color } from '../index.js' -import type { BlockModelProvider } from './BlockModel.js' -import { getItemColor } from './ItemColors.js' -import type { Mesh } from './Mesh.js' +import { Identifier } from '../core/index.js' +import { Color } from '../index.js' +import type { BlockModelProvider, Display } from './BlockModel.js' +import { ItemModelProvider } from './ItemModel.js' +import { Mesh } from './Mesh.js' import { Renderer } from './Renderer.js' import type { TextureAtlasProvider } from './TextureAtlas.js' -interface ModelRendererOptions { - /** Force the tint index of the item */ - tint?: Color, -} +export interface ItemRendererResources extends BlockModelProvider, TextureAtlasProvider, ItemModelProvider {} + +export type ItemRenderingContext = { + display_context?: Display, -interface ItemRendererResources extends BlockModelProvider, TextureAtlasProvider {} + 'fishing_rod/cast'?: boolean, + 'bundle/selected_item'?: number, + selected?: boolean, + carried?: boolean, + extended_view?: boolean, + context_entity_is_view_entity?: boolean + + keybind_down?: string[], + + main_hand?: 'left' | 'right', + context_entity_type?: Identifier, + context_entity_team_color?: Color + context_dimension?: Identifier + + cooldown_percentage?: {[key: string]: number}, + game_time?: number, + compass_angle?: number, + use_duration?: number, + max_use_duration?: number, + 'crossbow/pull'?: number +} export class ItemRenderer extends Renderer { - private item: ItemStack - private mesh: Mesh - private readonly tint: Color | ((index: number) => Color) | undefined + private mesh!: Mesh private readonly atlasTexture: WebGLTexture + constructor( gl: WebGLRenderingContext, - item: Identifier | ItemStack, + private item: ItemStack, private readonly resources: ItemRendererResources, - options?: ModelRendererOptions, + context: ItemRenderingContext = {}, ) { super(gl) - this.item = item instanceof ItemStack ? item : new ItemStack(item, 1) - this.mesh = this.getItemMesh() - this.tint = options?.tint + this.updateMesh(context) this.atlasTexture = this.createAtlasTexture(this.resources.getTextureAtlas()) } - public setItem(item: Identifier | ItemStack) { - this.item = item instanceof ItemStack ? item : new ItemStack(item, 1) - this.mesh = this.getItemMesh() + public setItem(item: ItemStack, context: ItemRenderingContext = {}) { + this.item = item + this.updateMesh(context) + } + + public updateMesh(context: ItemRenderingContext = {}) { + this.mesh = ItemRenderer.getItemMesh(this.item, this.resources, context) + this.mesh.computeNormals() + this.mesh.rebuild(this.gl, { pos: true, color: true, texture: true, normal: true }) } - private getItemMesh() { - const model = this.resources.getBlockModel(this.item.id.withPrefix('item/')) - if (!model) { - throw new Error(`Item model for ${this.item.toString()} does not exist`) + public static getItemMesh(item: ItemStack, resources: ItemRendererResources, context: ItemRenderingContext) { + const itemModelId = item.getComponent('item_model', tag => tag.getAsString()) + if (itemModelId === undefined){ + return new Mesh() } - let tint = this.tint - if (!tint && this.item.id.namespace === Identifier.DEFAULT_NAMESPACE) { - tint = getItemColor(this.item) + + const itemModel = resources.getItemModel(Identifier.parse(itemModelId)) + if (!itemModel) { + throw new Error(`Item model ${itemModelId} does not exist (defined by item ${item.toString()})`) } - const mesh = model.getMesh(this.resources, Cull.none(), tint) - const specialMesh = SpecialRenderers.getItemMesh(this.item, this.resources) - mesh.merge(specialMesh) - mesh.transform(model.getDisplayTransform('gui')) - mesh.computeNormals() - mesh.rebuild(this.gl, { pos: true, color: true, texture: true, normal: true }) + + const mesh = itemModel.getMesh(item, resources, context) + return mesh + } protected override getPerspective() { diff --git a/src/render/ItemTint.ts b/src/render/ItemTint.ts new file mode 100644 index 0000000..41c5504 --- /dev/null +++ b/src/render/ItemTint.ts @@ -0,0 +1,179 @@ +import { Color, ItemRenderingContext, ItemStack, Json, NbtIntArray, PotionContents } from "../index.js" + +export abstract class ItemTint { + public abstract getTint(item: ItemStack, context: ItemRenderingContext): Color +} + +const INVALID_COLOR: Color = [0, 0, 0] + +export namespace ItemTint { + export function fromJson(obj: unknown): ItemTint { + const root = Json.readObject(obj) ?? {} + const type = Json.readString(root.type)?.replace(/^minecraft:/, '') + switch (type) { + case 'constant': return new Constant( + Color.fromJson(root.value) ?? INVALID_COLOR + ) + case 'dye': return new Dye( + Color.fromJson(root.default) ?? INVALID_COLOR + ) + case 'grass': return new Grass( + Json.readNumber(root.temperature) ?? 0, + Json.readNumber(root.downfall) ?? 0 + ) + case 'firework': return new Firework( + Color.fromJson(root.default) ?? INVALID_COLOR + ) + case 'potion': return new Potion( + Color.fromJson(root.default) ?? INVALID_COLOR + ) + case 'map_color': return new MapColor( + Color.fromJson(root.default) ?? INVALID_COLOR + ) + case 'custom_model_data': return new CustomModelData( + Json.readInt(root.index) ?? 0, + Color.fromJson(root.default) ?? INVALID_COLOR + ) + case 'team': return new Team( + Color.fromJson(root.default) ?? INVALID_COLOR + ) + default: + throw new Error(`Invalid item tint type ${type}`) + } + } + + export class Constant extends ItemTint{ + constructor( + public value: Color + ) { + super() + } + + public getTint(item: ItemStack): Color { + return this.value + } + } + + export class Dye extends ItemTint{ + constructor( + public default_color: Color + ) { + super() + } + + public getTint(item: ItemStack): Color { + const dyedColor = item.getComponent('dyed_color', tag => { + return tag.isCompound() ? tag.getNumber('rgb') : tag.getAsNumber() + }) + if (dyedColor === undefined) return this.default_color + return Color.intToRgb(dyedColor) + } + } + + export class Grass extends ItemTint{ + constructor( + public temperature: number, + public downfall: number + ) { + super() + } + + public getTint(item: ItemStack): Color { + return [124 / 255, 189 / 255, 107 / 255] // TODO: this is hardcoded to the same value as for blocks + } + } + + export class Firework extends ItemTint{ + constructor( + public default_color: Color + ) { + super() + } + + public getTint(item: ItemStack): Color { + const colors = item.getComponent('firework_explosion', tag => { + if (!tag.isCompound()) return new NbtIntArray() + const colorsTag = tag.get('colors') + if (colorsTag && colorsTag.isListOrArray()) return colorsTag + return new NbtIntArray() + }) + const color: Color = (() => { + if (!colors || colors.length === 0) { + return this.default_color + } + if (colors.length === 1) { + return Color.intToRgb(colors.get(0)!.getAsNumber()) + } + let [r, g, b] = [0, 0, 0] + for (const color of colors.getItems()) { + r += (color.getAsNumber() & 0xFF0000) >> 16 + g += (color.getAsNumber() & 0xFF00) >> 8 + b += (color.getAsNumber() & 0xFF) >> 0 + } + r /= colors.length + g /= colors.length + b /= colors.length + return [r / 255, g / 255, b / 255] + })() + return color + } + } + + export class Potion extends ItemTint { + constructor( + public default_color: Color + ) { + super() + } + + public getTint(item: ItemStack): Color { + const potion_contents = item.getComponent('potion_contents', PotionContents.fromNbt ) + if (!potion_contents) return this.default_color + return PotionContents.getColor(potion_contents) + } + } + + export class MapColor extends ItemTint { + constructor( + public default_color: Color + ) { + super() + } + + public getTint(item: ItemStack): Color { + const mapColor = item.getComponent('map_color', tag => tag.getAsNumber()) + if (mapColor === undefined) return this.default_color + return Color.intToRgb(mapColor) + } + } + + export class CustomModelData extends ItemTint { + constructor( + public index: number, + public default_color: Color + ) { + super() + } + + public getTint(item: ItemStack): Color { + return item.getComponent('custom_model_data', tag => { + if (!tag.isCompound()) return undefined + const colorTag = tag.getList('colors').get(this.index) + if (colorTag === undefined) return undefined + return Color.fromNbt(colorTag) + }) ?? this.default_color + } + } + + export class Team extends ItemTint { + constructor( + public default_color: Color + ) { + super() + } + + public getTint(item: ItemStack, context: ItemRenderingContext): Color { + return context.context_entity_team_color ?? this.default_color + } + } +} \ No newline at end of file diff --git a/src/render/SpecialModel.ts b/src/render/SpecialModel.ts new file mode 100644 index 0000000..123fed1 --- /dev/null +++ b/src/render/SpecialModel.ts @@ -0,0 +1,230 @@ +import { mat4 } from "gl-matrix" +import { Direction, Identifier, ItemStack, Json, SpecialRenderers, TextureAtlasProvider } from "../index.js" +import { Mesh } from "./Mesh.js" + + + +export abstract class SpecialModel { + public abstract getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh +} + +export namespace SpecialModel { + export function fromJson(obj: unknown) { + const root = Json.readObject(obj) ?? {} + const type = Json.readString(root.type)?.replace(/^minecraft:/, '') + switch (type) { + case 'bed': return new Bed( + Identifier.parse(Json.readString(root.texture) ?? '') + ) + case 'banner': return new Banner( + Json.readString(root.color) ?? '' + ) + case 'conduit': return new Conduit() + case 'chest': return new Chest( + Identifier.parse(Json.readString(root.texture) ?? ''), + Json.readNumber(root.openness) ?? 0 + ) + case 'head': return new Head( + Json.readString(root.kind) ?? '', + typeof root.texture === 'string' ? Identifier.parse(root.texture) : undefined, + Json.readNumber(root.animation) ?? 0 + ) + case 'shulker_box': return new ShulkerBox( + Identifier.parse(Json.readString(root.texture) ?? ''), + Json.readNumber(root.openness) ?? 0, + (Json.readString(root.orientation) ?? 'up') as Direction + ) + case 'shield': return new Shield() + case 'trident': return new Trident() + case 'decorated_pot': return new DecoratedPot() + case 'standing_sign': return new StandingSign( + Json.readString(root.wood_type) ?? '', + typeof root.texture === 'string' ? Identifier.parse(root.texture) : undefined + ) + case 'hanging_sign': return new HangingSign( + Json.readString(root.wood_type) ?? '', + typeof root.texture === 'string' ? Identifier.parse(root.texture) : undefined + ) + default: + throw new Error(`Invalid item model type ${type}`) + } + } + + class Bed extends SpecialModel { + private readonly renderer + + constructor( + texture: Identifier + ) { + super() + this.renderer = SpecialRenderers.bedRenderer(texture) + } + + public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh { + const headMesh = this.renderer("head", resources) + const footMesh = this.renderer("foot", resources) + const t = mat4.create() + mat4.translate(t, t, [0, 0, -16]) + return headMesh.merge(footMesh.transform(t)) + } + } + + class Banner extends SpecialModel { + private readonly renderer + + constructor( + color: string + ) { + super() + this.renderer = SpecialRenderers.bannerRenderer(color) + } + + public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh { + return this.renderer(resources) + } + } + + class Conduit extends SpecialModel { + constructor() { + super() + } + + public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh { + return SpecialRenderers.conduitRenderer(resources) + } + } + + class Chest extends SpecialModel { + private readonly renderer + + constructor( + texture: Identifier, + openness: number + ) { + super() + this.renderer = SpecialRenderers.chestRenderer(texture) + } + + public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh { + const t = mat4.create() + mat4.translate(t, t, [8, 8, 8]) + mat4.rotateY(t, t, Math.PI) + mat4.translate(t, t, [-8, -8, -8]) + return this.renderer(resources).transform(t) + } + } + + class Head extends SpecialModel { + private readonly renderer + + constructor( + kind: string, + texture: Identifier | undefined, + animation: number + ) { + super() + + this.renderer = ({ + 'skeleton': () => SpecialRenderers.headRenderer(texture ?? Identifier.create('skeleton/skeleton'), 2), + 'wither_skeleton': () => SpecialRenderers.headRenderer(texture ?? Identifier.create('skeleton/wither_skeleton'), 2), + 'zombie': () => SpecialRenderers.headRenderer(texture ?? Identifier.create('zombie/zombie'), 1), + 'creeper': () => SpecialRenderers.headRenderer(texture ?? Identifier.create('creeper/creeper'), 2), + 'dragon': () => SpecialRenderers.dragonHeadRenderer(texture), + 'piglin': () => SpecialRenderers.piglinHeadRenderer(texture), + 'player': () => SpecialRenderers.headRenderer(texture ?? Identifier.create('player/wide/steve'), 1), // TODO: fix texture + }[kind] ?? (() => () => new Mesh()))() + } + + public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh { + return this.renderer(resources) + } + } + + class ShulkerBox extends SpecialModel { + private readonly renderer + + constructor( + texture: Identifier, + openness: number, + orientation: Direction + ) { + super() + + this.renderer = SpecialRenderers.shulkerBoxRenderer(texture) + } + + public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh { + return this.renderer(resources) + } + } + + class Shield extends SpecialModel { + constructor() { + super() + } + + public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh { + const shieldMesh = SpecialRenderers.shieldRenderer(resources) + const t = mat4.create() + mat4.translate(t, t, [-3, 1, 0]) + mat4.rotateX(t, t, -10 * Math.PI/180) + mat4.rotateY(t, t, -10 * Math.PI/180) + mat4.rotateZ(t, t, -5 * Math.PI/180) + return shieldMesh.transform(t) + } + } + + class Trident extends SpecialModel { + constructor() { + super() + } + + public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh { + return new Mesh() // TODO + } + } + + class DecoratedPot extends SpecialModel { + constructor() { + super() + } + + public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh { + return SpecialRenderers.decoratedPotRenderer(resources) + } + } + + class StandingSign extends SpecialModel { + private readonly renderer + + constructor( + wood_type: string, + texture?: Identifier + ) { + super() + + this.renderer = SpecialRenderers.signRenderer(texture ?? Identifier.create(wood_type)) + } + + public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh { + return this.renderer(resources) + } + } + + class HangingSign extends SpecialModel { + private readonly renderer + + constructor( + wood_type: string, + texture?: Identifier + ) { + super() + + this.renderer = SpecialRenderers.hangingSignRenderer(texture ?? Identifier.create(wood_type)) + } + + public getMesh(item: ItemStack, resources: TextureAtlasProvider): Mesh { + return this.renderer(false, resources) + } + } +} diff --git a/src/render/SpecialRenderer.ts b/src/render/SpecialRenderer.ts index 8723161..3ae9c33 100644 --- a/src/render/SpecialRenderer.ts +++ b/src/render/SpecialRenderer.ts @@ -1,6 +1,5 @@ import { mat4 } from 'gl-matrix' -import type { ItemStack } from '../core/index.js' -import { BlockState, Direction } from '../core/index.js' +import { BlockState, Direction, Identifier } from '../core/index.js' import { BlockColors } from './BlockColors.js' import { BlockModel } from './BlockModel.js' import { Cull } from './Cull.js' @@ -26,371 +25,402 @@ function liquidRenderer(type: string, level: number, atlas: TextureAtlasProvider }]).getMesh(atlas, cull, BlockColors[type]?.({})) } -function chestRenderer(type: string) { - return (atlas: TextureAtlasProvider) => { + +export namespace SpecialRenderers { + export function chestRenderer(texture: Identifier) { + return (atlas: TextureAtlasProvider) => { + return new BlockModel(undefined, { + 0: texture.withPrefix('entity/chest/').toString(), + }, [ + { + from: [1, 0, 1], + to: [15, 10, 15], + faces: { + north: {uv: [10.5, 8.25, 14, 10.75], rotation: 180, texture: '#0'}, + east: {uv: [7, 8.25, 10.5, 10.75], rotation: 180, texture: '#0'}, + south: {uv: [3.5, 8.25, 7, 10.75], rotation: 180, texture: '#0'}, + west: {uv: [0, 8.25, 3.5, 10.75], rotation: 180, texture: '#0'}, + up: {uv: [7, 4.75, 10.5, 8.25], texture: '#0'}, + down: {uv: [3.5, 4.75, 7, 8.25], texture: '#0'}, + }, + }, + { + from: [1, 10, 1], + to: [15, 14, 15], + faces: { + north: {uv: [10.5, 3.75, 14, 4.75], rotation: 180, texture: '#0'}, + east: {uv: [7, 3.75, 10.5, 4.75], rotation: 180, texture: '#0'}, + south: {uv: [3.5, 3.75, 7, 4.75], rotation: 180, texture: '#0'}, + west: {uv: [0, 3.75, 3.5, 4.75], rotation: 180, texture: '#0'}, + up: {uv: [7, 0, 10.5, 3.5], texture: '#0'}, + down: {uv: [3.5, 0, 7, 3.5], texture: '#0'}, + }, + }, + { + from: [7, 7, 0], + to: [9, 11, 2], + faces: { + north: {uv: [0.25, 0.25, 0.75, 1.25], rotation: 180, texture: '#0'}, + east: {uv: [0, 0.25, 0.25, 1.25], rotation: 180, texture: '#0'}, + south: {uv: [1, 0.25, 1.5, 1.25], rotation: 180, texture: '#0'}, + west: {uv: [0.75, 0.25, 1, 1.25], rotation: 180, texture: '#0'}, + up: {uv: [0.25, 0, 0.75, 0.25], rotation: 180, texture: '#0'}, + down: {uv: [0.75, 0, 1.25, 0.25], rotation: 180, texture: '#0'}, + }, + }, + ]).getMesh(atlas, Cull.none()) + } + } + + export function decoratedPotRenderer(atlas: TextureAtlasProvider) { return new BlockModel(undefined, { - 0: `entity/chest/${type}`, + 0: 'entity/decorated_pot/decorated_pot_side', + 1: 'entity/decorated_pot/decorated_pot_base', }, [ { from: [1, 0, 1], - to: [15, 10, 15], + to: [15, 16, 15], faces: { - north: {uv: [10.5, 8.25, 14, 10.75], rotation: 180, texture: '#0'}, - east: {uv: [7, 8.25, 10.5, 10.75], rotation: 180, texture: '#0'}, - south: {uv: [3.5, 8.25, 7, 10.75], rotation: 180, texture: '#0'}, - west: {uv: [0, 8.25, 3.5, 10.75], rotation: 180, texture: '#0'}, - up: {uv: [7, 4.75, 10.5, 8.25], texture: '#0'}, - down: {uv: [3.5, 4.75, 7, 8.25], texture: '#0'}, + north: {uv: [1, 0, 15, 16], texture: '#0'}, + east: {uv: [1, 0, 15, 16], texture: '#0'}, + south: {uv: [1, 0, 15, 16], texture: '#0'}, + west: {uv: [1, 0, 15, 16], texture: '#0'}, + up: {uv: [0, 6.5, 7, 13.5], texture: '#1'}, + down: {uv: [7, 6.5, 14, 13.5], texture: '#1'}, }, }, { - from: [1, 10, 1], - to: [15, 14, 15], + from: [5, 16, 5], + to: [11, 17, 11], faces: { - north: {uv: [10.5, 3.75, 14, 4.75], rotation: 180, texture: '#0'}, - east: {uv: [7, 3.75, 10.5, 4.75], rotation: 180, texture: '#0'}, - south: {uv: [3.5, 3.75, 7, 4.75], rotation: 180, texture: '#0'}, - west: {uv: [0, 3.75, 3.5, 4.75], rotation: 180, texture: '#0'}, - up: {uv: [7, 0, 10.5, 3.5], texture: '#0'}, - down: {uv: [3.5, 0, 7, 3.5], texture: '#0'}, + north: {uv: [0, 5.5, 3, 6], texture: '#1'}, + east: {uv: [3, 5.5, 6, 6], texture: '#1'}, + south: {uv: [6, 5.5, 9, 6], texture: '#1'}, + west: {uv: [9, 5.5, 12, 6], texture: '#1'}, }, }, { - from: [7, 7, 0], - to: [9, 11, 2], + from: [4, 17, 4], + to: [12, 20, 12], faces: { - north: {uv: [0.25, 0.25, 0.75, 1.25], rotation: 180, texture: '#0'}, - east: {uv: [0, 0.25, 0.25, 1.25], rotation: 180, texture: '#0'}, - south: {uv: [1, 0.25, 1.5, 1.25], rotation: 180, texture: '#0'}, - west: {uv: [0.75, 0.25, 1, 1.25], rotation: 180, texture: '#0'}, - up: {uv: [0.25, 0, 0.75, 0.25], rotation: 180, texture: '#0'}, - down: {uv: [0.75, 0, 1.25, 0.25], rotation: 180, texture: '#0'}, + north: {uv: [0, 4, 4, 5.5], texture: '#1'}, + east: {uv: [4, 4, 8, 5.5], texture: '#1'}, + south: {uv: [8, 4, 12, 5.5], texture: '#1'}, + west: {uv: [12, 4, 16, 5.5], texture: '#1'}, + up: {uv: [4, 0, 8, 4], texture: '#1'}, + down: {uv: [8, 0, 12, 4], texture: '#1'}, }, }, ]).getMesh(atlas, Cull.none()) } -} - -function decoratedPotRenderer(atlas: TextureAtlasProvider){ - return new BlockModel(undefined, { - 0: 'entity/decorated_pot/decorated_pot_side', - 1: 'entity/decorated_pot/decorated_pot_base', - }, [ - { - from: [1, 0, 1], - to: [15, 16, 15], - faces: { - north: {uv: [1, 0, 15, 16], texture: '#0'}, - east: {uv: [1, 0, 15, 16], texture: '#0'}, - south: {uv: [1, 0, 15, 16], texture: '#0'}, - west: {uv: [1, 0, 15, 16], texture: '#0'}, - up: {uv: [0, 6.5, 7, 13.5], texture: '#1'}, - down: {uv: [7, 6.5, 14, 13.5], texture: '#1'}, - }, - }, - { - from: [5, 16, 5], - to: [11, 17, 11], - faces: { - north: {uv: [0, 5.5, 3, 6], texture: '#1'}, - east: {uv: [3, 5.5, 6, 6], texture: '#1'}, - south: {uv: [6, 5.5, 9, 6], texture: '#1'}, - west: {uv: [9, 5.5, 12, 6], texture: '#1'}, - }, - }, - { - from: [4, 17, 4], - to: [12, 20, 12], - faces: { - north: {uv: [0, 4, 4, 5.5], texture: '#1'}, - east: {uv: [4, 4, 8, 5.5], texture: '#1'}, - south: {uv: [8, 4, 12, 5.5], texture: '#1'}, - west: {uv: [12, 4, 16, 5.5], texture: '#1'}, - up: {uv: [4, 0, 8, 4], texture: '#1'}, - down: {uv: [8, 0, 12, 4], texture: '#1'}, - }, - }, - ]).getMesh(atlas, Cull.none()) -} - -function shieldRenderer(atlas: TextureAtlasProvider) { - return new BlockModel(undefined, { - 0: 'entity/shield_base_nopattern', - }, [ - { - from: [-6, -11, -2], - to: [6, 11, -1], - faces: { - north: {uv: [3.5, 0.25, 6.5, 5.75], texture: '#0'}, - east: {uv: [3.25, 0.25, 3.5, 5.75], texture: '#0'}, - south: {uv: [0.25, 0.25, 3.25, 5.75], texture: '#0'}, - west: {uv: [0, 0.25, 0.25, 5.75], texture: '#0'}, - up: {uv: [0.25, 0, 3.25, 0.25], texture: '#0'}, - down: {uv: [3.25, 0, 6.25, 0.25], texture: '#0'}, - }, - }, - ]).getMesh(atlas, Cull.none()) -} -function skullRenderer(texture: string, n: number) { - return (atlas: TextureAtlasProvider) => { + export function shieldRenderer(atlas: TextureAtlasProvider) { return new BlockModel(undefined, { - 0: `entity/${texture}`, + 0: 'entity/shield_base_nopattern', }, [ { - from: [4, 0, 4], - to: [12, 8, 12], + from: [-6, -11, -2], + to: [6, 11, -1], faces: { - north: {uv: [6, 2*n, 8, 4*n], texture: '#0'}, - east: {uv: [2, 2*n, 0, 4*n], texture: '#0'}, - south: {uv: [2, 2*n, 4, 4*n], texture: '#0'}, - west: {uv: [6, 2*n, 4, 4*n], texture: '#0'}, - up: {uv: [2, 0*n, 4, 2*n], texture: '#0'}, - down: {uv: [4, 0*n, 6, 2*n], texture: '#0'}, + north: {uv: [3.5, 0.25, 6.5, 5.75], texture: '#0'}, + east: {uv: [3.25, 0.25, 3.5, 5.75], texture: '#0'}, + south: {uv: [0.25, 0.25, 3.25, 5.75], texture: '#0'}, + west: {uv: [0, 0.25, 0.25, 5.75], texture: '#0'}, + up: {uv: [0.25, 0, 3.25, 0.25], texture: '#0'}, + down: {uv: [3.25, 0, 6.25, 0.25], texture: '#0'}, }, }, - ]) - .getMesh(atlas, Cull.none()) + ]).getMesh(atlas, Cull.none()) } -} -function dragonHeadRenderer(atlas: TextureAtlasProvider) { - const transformation = mat4.create() - mat4.translate(transformation, transformation, [8, 8, 8]) - mat4.scale(transformation, transformation, [0.75, 0.75, 0.75]) - mat4.rotateY(transformation, transformation, Math.PI) - mat4.translate(transformation, transformation, [-8, -11.2, -8]) - return new BlockModel(undefined, { - 0: 'entity/enderdragon/dragon', - }, [ - { - from: [2, 4, -16], - to: [14, 9, 0], - faces: { - north: {uv: [12, 3.75, 12.75, 4.0625], texture: '#0'}, - east: {uv: [11, 3.75, 12, 4.0625], texture: '#0'}, - south: {uv: [13.75, 3.75, 14.5, 4.0625], texture: '#0'}, - west: {uv: [12.75, 3.75, 13.75, 4.0625], texture: '#0'}, - up: {uv: [12.75, 3.75, 12, 2.75], texture: '#0'}, - down: {uv: [13.5, 2.75, 12.75, 3.75], texture: '#0'}, - }, - }, - { - from: [0, 0, -2], - to: [16, 16, 14], - faces: { - north: {uv: [8, 2.875, 9, 3.875], texture: '#0'}, - east: {uv: [7, 2.875, 8, 3.875], texture: '#0'}, - south: {uv: [10, 2.875, 11, 3.875], texture: '#0'}, - west: {uv: [9, 2.875, 10, 3.875], texture: '#0'}, - up: {uv: [9, 2.875, 8, 1.875], texture: '#0'}, - down: {uv: [10, 1.875, 9, 2.875], texture: '#0'}, - }, - }, - { - from: [2, 0, -16], - to: [14, 4, 0], - rotation: {angle: -0.2 * 180 / Math.PI, axis: 'x', origin: [8, 4, -2]}, - faces: { - north: {uv: [12, 5.0625, 12.75, 5.3125], texture: '#0'}, - east: {uv: [11, 5.0625, 12, 5.3125], texture: '#0'}, - south: {uv: [13.75, 5.0625, 14.5, 5.3125], texture: '#0'}, - west: {uv: [12.75, 5.0625, 13.75, 5.3125], texture: '#0'}, - up: {uv: [12.75, 5.0625, 12, 4.0625], texture: '#0'}, - down: {uv: [13.5, 4.0625, 12.75, 5.0625], texture: '#0'}, - }, - }, - { - from: [3, 16, 4], - to: [5, 20, 10], - faces: { - north: {uv: [0.375, 0.375, 0.5, 0.625], texture: '#0'}, - east: {uv: [0, 0.375, 0.375, 0.625], texture: '#0'}, - south: {uv: [0.875, 0.375, 1, 0.625], texture: '#0'}, - west: {uv: [0.5, 0.375, 0.875, 0.625], texture: '#0'}, - up: {uv: [0.5, 0.375, 0.375, 0], texture: '#0'}, - down: {uv: [0.625, 0, 0.5, 0.375], texture: '#0'}, - }, - }, - { - from: [11, 16, 4], - to: [13, 20, 10], - faces: { - north: {uv: [0.375, 0.375, 0.5, 0.625], texture: '#0'}, - east: {uv: [0, 0.375, 0.375, 0.625], texture: '#0'}, - south: {uv: [0.875, 0.375, 1, 0.625], texture: '#0'}, - west: {uv: [0.5, 0.375, 0.875, 0.625], texture: '#0'}, - up: {uv: [0.5, 0.375, 0.375, 0], texture: '#0'}, - down: {uv: [0.625, 0, 0.5, 0.375], texture: '#0'}, - }, - }, - { - from: [3, 9, -14], - to: [5, 11, -10], - faces: { - north: {uv: [7.25, 0.25, 7.375, 0.375], texture: '#0'}, - east: {uv: [7, 0.25, 7.25, 0.375], texture: '#0'}, - south: {uv: [7.625, 0.25, 7.75, 0.375], texture: '#0'}, - west: {uv: [7.375, 0.25, 7.625, 0.375], texture: '#0'}, - up: {uv: [7.375, 0.25, 7.25, 0], texture: '#0'}, - down: {uv: [7.5, 0, 7.375, 0.25], texture: '#0'}, - }, - }, - { - from: [11, 9, -14], - to: [13, 11, -10], - faces: { - north: {uv: [7.25, 0.25, 7.375, 0.375], texture: '#0'}, - east: {uv: [7, 0.25, 7.25, 0.375], texture: '#0'}, - south: {uv: [7.625, 0.25, 7.75, 0.375], texture: '#0'}, - west: {uv: [7.375, 0.25, 7.625, 0.375], texture: '#0'}, - up: {uv: [7.375, 0.25, 7.25, 0], texture: '#0'}, - down: {uv: [7.5, 0, 7.375, 0.25], texture: '#0'}, - }, - }, - ]).withUvEpsilon(1/256).getMesh(atlas, Cull.none()).transform(transformation) -} + export function headRenderer(texture: Identifier, n: number) { + return (atlas: TextureAtlasProvider) => { + return new BlockModel(undefined, { + 0: texture.withPrefix('entity/').toString(), + }, [ + { + from: [4, 0, 4], + to: [12, 8, 12], + faces: { + north: {uv: [6, 2*n, 8, 4*n], texture: '#0'}, + east: {uv: [2, 2*n, 0, 4*n], texture: '#0'}, + south: {uv: [2, 2*n, 4, 4*n], texture: '#0'}, + west: {uv: [6, 2*n, 4, 4*n], texture: '#0'}, + up: {uv: [2, 0*n, 4, 2*n], texture: '#0'}, + down: {uv: [4, 0*n, 6, 2*n], texture: '#0'}, + }, + }, + ]) + .getMesh(atlas, Cull.none()) + } + } -function piglinHeadRenderer(atlas: TextureAtlasProvider) { - return new BlockModel(undefined, { - 0: 'entity/piglin/piglin', - }, [ - { - from: [3, 0, 4], - to: [13, 8, 12], - faces: { - north: {uv: [6.5, 2, 9, 4], texture: '#0'}, - east: {uv: [2, 2, 0, 4], texture: '#0'}, - south: {uv: [2, 2, 4.5, 4], texture: '#0'}, - west: {uv: [6.5, 2, 4.5, 4], texture: '#0'}, - up: {uv: [2, 0, 4.5, 2], texture: '#0'}, - down: {uv: [4.5, 0, 7, 2], texture: '#0'}, - }, - }, - { - from: [6, 0, 12], - to: [10, 4, 13], - faces: { - north: {uv: [9.25, 0.5, 10.25, 1.5], texture: '#0'}, - east: {uv: [7.75, 0.5, 8, 1.5], texture: '#0'}, - south: {uv: [8, 0.5, 9, 1.5], texture: '#0'}, - west: {uv: [9, 0.5, 9.25, 1.5], texture: '#0'}, - up: {uv: [8, 0.25, 9, 0.5], texture: '#0'}, - down: {uv: [9, 0.25, 10, 0.5], texture: '#0'}, - }, - }, - { - from: [5, 0, 12], - to: [6, 2, 13], - faces: { - north: {uv: [1.25, 0.25, 1.5, 0.75], texture: '#0'}, - east: {uv: [0.5, 0.25, 0.75, 0.75], texture: '#0'}, - south: {uv: [0.75, 0.25, 1, 0.75], texture: '#0'}, - west: {uv: [1, 0.25, 1.25, 0.75], texture: '#0'}, - up: {uv: [0.75, 0, 1, 0.25], texture: '#0'}, - down: {uv: [1, 0, 1.25, 0.25], texture: '#0'}, - }, - }, - { - from: [10, 0, 12], - to: [11, 2, 13], - faces: { - north: {uv: [1.25, 1.25, 1.5, 1.75], texture: '#0'}, - east: {uv: [0.5, 1.25, 0.75, 1.75], texture: '#0'}, - south: {uv: [0.75, 1.25, 1, 1.75], texture: '#0'}, - west: {uv: [1, 1.25, 1.25, 1.75], texture: '#0'}, - up: {uv: [0.75, 1, 1, 1.25], texture: '#0'}, - down: {uv: [1, 1, 1.25, 1.25], texture: '#0'}, - }, - }, - { - from: [2.5, 1.5, 6], - to: [3.5, 6.5, 10], - rotation: {angle: -30, axis: 'z', origin: [3, 7, 8]}, - faces: { - north: {uv: [12, 2.5, 12.25, 3.75], texture: '#0'}, - east: {uv: [9.75, 2.5, 10.75, 3.75], texture: '#0'}, - south: {uv: [10.75, 2.5, 11, 3.75], texture: '#0'}, - west: {uv: [11, 2.5, 12, 3.75], texture: '#0'}, - up: {uv: [10.75, 1.5, 11, 2.5], texture: '#0'}, - down: {uv: [11, 1.5, 11.25, 2.5], texture: '#0'}, - }, - }, - { - from: [12.5, 1.5, 6], - to: [13.5, 6.5, 10], - rotation: {angle: 30, axis: 'z', origin: [13, 7, 8]}, - faces: { - north: {uv: [15.25, 2.5, 15, 3.75], texture: '#0'}, - east: {uv: [15, 2.5, 14, 3.75], texture: '#0'}, - south: {uv: [14, 2.5, 13.75, 3.75], texture: '#0'}, - west: {uv: [13.75, 2.5, 12.75, 3.75], texture: '#0'}, - up: {uv: [14, 1.5, 13.75, 2.5], texture: '#0'}, - down: {uv: [14.25, 1.5, 14, 2.5], texture: '#0'}, - }, - }, - ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none()) -} + export function dragonHeadRenderer(texture: Identifier = Identifier.create('enderdragon/dragon')) { + return (atlas: TextureAtlasProvider) => { + const transformation = mat4.create() + mat4.translate(transformation, transformation, [8, 8, 8]) + mat4.scale(transformation, transformation, [0.75, 0.75, 0.75]) + mat4.rotateY(transformation, transformation, Math.PI) + mat4.translate(transformation, transformation, [-8, -11.2, -8]) + return new BlockModel(undefined, { + 0: texture.withPrefix('entity/').toString(), + }, [ + { + from: [2, 4, -16], + to: [14, 9, 0], + faces: { + north: {uv: [12, 3.75, 12.75, 4.0625], texture: '#0'}, + east: {uv: [11, 3.75, 12, 4.0625], texture: '#0'}, + south: {uv: [13.75, 3.75, 14.5, 4.0625], texture: '#0'}, + west: {uv: [12.75, 3.75, 13.75, 4.0625], texture: '#0'}, + up: {uv: [12.75, 3.75, 12, 2.75], texture: '#0'}, + down: {uv: [13.5, 2.75, 12.75, 3.75], texture: '#0'}, + }, + }, + { + from: [0, 0, -2], + to: [16, 16, 14], + faces: { + north: {uv: [8, 2.875, 9, 3.875], texture: '#0'}, + east: {uv: [7, 2.875, 8, 3.875], texture: '#0'}, + south: {uv: [10, 2.875, 11, 3.875], texture: '#0'}, + west: {uv: [9, 2.875, 10, 3.875], texture: '#0'}, + up: {uv: [9, 2.875, 8, 1.875], texture: '#0'}, + down: {uv: [10, 1.875, 9, 2.875], texture: '#0'}, + }, + }, + { + from: [2, 0, -16], + to: [14, 4, 0], + rotation: {angle: -0.2 * 180 / Math.PI, axis: 'x', origin: [8, 4, -2]}, + faces: { + north: {uv: [12, 5.0625, 12.75, 5.3125], texture: '#0'}, + east: {uv: [11, 5.0625, 12, 5.3125], texture: '#0'}, + south: {uv: [13.75, 5.0625, 14.5, 5.3125], texture: '#0'}, + west: {uv: [12.75, 5.0625, 13.75, 5.3125], texture: '#0'}, + up: {uv: [12.75, 5.0625, 12, 4.0625], texture: '#0'}, + down: {uv: [13.5, 4.0625, 12.75, 5.0625], texture: '#0'}, + }, + }, + { + from: [3, 16, 4], + to: [5, 20, 10], + faces: { + north: {uv: [0.375, 0.375, 0.5, 0.625], texture: '#0'}, + east: {uv: [0, 0.375, 0.375, 0.625], texture: '#0'}, + south: {uv: [0.875, 0.375, 1, 0.625], texture: '#0'}, + west: {uv: [0.5, 0.375, 0.875, 0.625], texture: '#0'}, + up: {uv: [0.5, 0.375, 0.375, 0], texture: '#0'}, + down: {uv: [0.625, 0, 0.5, 0.375], texture: '#0'}, + }, + }, + { + from: [11, 16, 4], + to: [13, 20, 10], + faces: { + north: {uv: [0.375, 0.375, 0.5, 0.625], texture: '#0'}, + east: {uv: [0, 0.375, 0.375, 0.625], texture: '#0'}, + south: {uv: [0.875, 0.375, 1, 0.625], texture: '#0'}, + west: {uv: [0.5, 0.375, 0.875, 0.625], texture: '#0'}, + up: {uv: [0.5, 0.375, 0.375, 0], texture: '#0'}, + down: {uv: [0.625, 0, 0.5, 0.375], texture: '#0'}, + }, + }, + { + from: [3, 9, -14], + to: [5, 11, -10], + faces: { + north: {uv: [7.25, 0.25, 7.375, 0.375], texture: '#0'}, + east: {uv: [7, 0.25, 7.25, 0.375], texture: '#0'}, + south: {uv: [7.625, 0.25, 7.75, 0.375], texture: '#0'}, + west: {uv: [7.375, 0.25, 7.625, 0.375], texture: '#0'}, + up: {uv: [7.375, 0.25, 7.25, 0], texture: '#0'}, + down: {uv: [7.5, 0, 7.375, 0.25], texture: '#0'}, + }, + }, + { + from: [11, 9, -14], + to: [13, 11, -10], + faces: { + north: {uv: [7.25, 0.25, 7.375, 0.375], texture: '#0'}, + east: {uv: [7, 0.25, 7.25, 0.375], texture: '#0'}, + south: {uv: [7.625, 0.25, 7.75, 0.375], texture: '#0'}, + west: {uv: [7.375, 0.25, 7.625, 0.375], texture: '#0'}, + up: {uv: [7.375, 0.25, 7.25, 0], texture: '#0'}, + down: {uv: [7.5, 0, 7.375, 0.25], texture: '#0'}, + }, + }, + ]).withUvEpsilon(1/256).getMesh(atlas, Cull.none()).transform(transformation) + } + } -function signRenderer(woodType: string) { - return (atlas: TextureAtlasProvider) => { - return new BlockModel(undefined, { - 0: `entity/signs/${woodType}`, - }, [ - { - from: [-4, 8, 7], - to: [20, 20, 9], - faces: { - north: {uv: [0.5, 1, 6.5, 7], texture: '#0'}, - east: {uv: [0, 1, 0.5, 7], texture: '#0'}, - south: {uv: [7, 1, 13, 7], texture: '#0'}, - west: {uv: [6.5, 1, 7, 7], texture: '#0'}, - up: {uv: [6.5, 1, 0.5, 0], texture: '#0'}, - down: {uv: [12.5, 0, 6.5, 1], texture: '#0'}, + export function piglinHeadRenderer(texture: Identifier = Identifier.create('piglin/piglin')) { + return (atlas: TextureAtlasProvider) => { + return new BlockModel(undefined, { + 0: texture.withPrefix('entity/').toString(), + }, [ + { + from: [3, 0, 4], + to: [13, 8, 12], + faces: { + north: {uv: [6.5, 2, 9, 4], texture: '#0'}, + east: {uv: [2, 2, 0, 4], texture: '#0'}, + south: {uv: [2, 2, 4.5, 4], texture: '#0'}, + west: {uv: [6.5, 2, 4.5, 4], texture: '#0'}, + up: {uv: [2, 0, 4.5, 2], texture: '#0'}, + down: {uv: [4.5, 0, 7, 2], texture: '#0'}, + }, }, - }, - { - from: [7, -6, 7], - to: [9, 8, 9], - faces: { - north: {uv: [0.5, 8, 1, 15], texture: '#0'}, - east: {uv: [0, 8, 0.5, 15], texture: '#0'}, - south: {uv: [1.5, 8, 2, 15], texture: '#0'}, - west: {uv: [1, 8, 1.5, 15], texture: '#0'}, - up: {uv: [1, 8, 0.5, 7], texture: '#0'}, - down: {uv: [1.5, 7, 1, 8], texture: '#0'}, + { + from: [6, 0, 12], + to: [10, 4, 13], + faces: { + north: {uv: [9.25, 0.5, 10.25, 1.5], texture: '#0'}, + east: {uv: [7.75, 0.5, 8, 1.5], texture: '#0'}, + south: {uv: [8, 0.5, 9, 1.5], texture: '#0'}, + west: {uv: [9, 0.5, 9.25, 1.5], texture: '#0'}, + up: {uv: [8, 0.25, 9, 0.5], texture: '#0'}, + down: {uv: [9, 0.25, 10, 0.5], texture: '#0'}, + }, }, - }, - ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none()) + { + from: [5, 0, 12], + to: [6, 2, 13], + faces: { + north: {uv: [1.25, 0.25, 1.5, 0.75], texture: '#0'}, + east: {uv: [0.5, 0.25, 0.75, 0.75], texture: '#0'}, + south: {uv: [0.75, 0.25, 1, 0.75], texture: '#0'}, + west: {uv: [1, 0.25, 1.25, 0.75], texture: '#0'}, + up: {uv: [0.75, 0, 1, 0.25], texture: '#0'}, + down: {uv: [1, 0, 1.25, 0.25], texture: '#0'}, + }, + }, + { + from: [10, 0, 12], + to: [11, 2, 13], + faces: { + north: {uv: [1.25, 1.25, 1.5, 1.75], texture: '#0'}, + east: {uv: [0.5, 1.25, 0.75, 1.75], texture: '#0'}, + south: {uv: [0.75, 1.25, 1, 1.75], texture: '#0'}, + west: {uv: [1, 1.25, 1.25, 1.75], texture: '#0'}, + up: {uv: [0.75, 1, 1, 1.25], texture: '#0'}, + down: {uv: [1, 1, 1.25, 1.25], texture: '#0'}, + }, + }, + { + from: [2.5, 1.5, 6], + to: [3.5, 6.5, 10], + rotation: {angle: -30, axis: 'z', origin: [3, 7, 8]}, + faces: { + north: {uv: [12, 2.5, 12.25, 3.75], texture: '#0'}, + east: {uv: [9.75, 2.5, 10.75, 3.75], texture: '#0'}, + south: {uv: [10.75, 2.5, 11, 3.75], texture: '#0'}, + west: {uv: [11, 2.5, 12, 3.75], texture: '#0'}, + up: {uv: [10.75, 1.5, 11, 2.5], texture: '#0'}, + down: {uv: [11, 1.5, 11.25, 2.5], texture: '#0'}, + }, + }, + { + from: [12.5, 1.5, 6], + to: [13.5, 6.5, 10], + rotation: {angle: 30, axis: 'z', origin: [13, 7, 8]}, + faces: { + north: {uv: [15.25, 2.5, 15, 3.75], texture: '#0'}, + east: {uv: [15, 2.5, 14, 3.75], texture: '#0'}, + south: {uv: [14, 2.5, 13.75, 3.75], texture: '#0'}, + west: {uv: [13.75, 2.5, 12.75, 3.75], texture: '#0'}, + up: {uv: [14, 1.5, 13.75, 2.5], texture: '#0'}, + down: {uv: [14.25, 1.5, 14, 2.5], texture: '#0'}, + }, + }, + ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none()) + } } -} -function wallSignRenderer(woodType: string) { - return (atlas: TextureAtlasProvider) => { - return new BlockModel(undefined, { - 0: `entity/signs/${woodType}`, - }, [ - { - from: [-4, 4, 17], - to: [20, 16, 19], - faces: { - north: {uv: [0.5, 1, 6.5, 7], texture: '#0'}, - east: {uv: [0, 1, 0.5, 7], texture: '#0'}, - south: {uv: [7, 1, 13, 7], texture: '#0'}, - west: {uv: [6.5, 1, 7, 7], texture: '#0'}, - up: {uv: [6.5, 1, 0.5, 0], texture: '#0'}, - down: {uv: [12.5, 0, 6.5, 1], texture: '#0'}, + export function signRenderer(texture: Identifier) { + return (atlas: TextureAtlasProvider) => { + return new BlockModel(undefined, { + 0: texture.withPrefix('entity/signs/').toString(), + }, [ + { + from: [-4, 8, 7], + to: [20, 20, 9], + faces: { + north: {uv: [0.5, 1, 6.5, 7], texture: '#0'}, + east: {uv: [0, 1, 0.5, 7], texture: '#0'}, + south: {uv: [7, 1, 13, 7], texture: '#0'}, + west: {uv: [6.5, 1, 7, 7], texture: '#0'}, + up: {uv: [6.5, 1, 0.5, 0], texture: '#0'}, + down: {uv: [12.5, 0, 6.5, 1], texture: '#0'}, + }, }, - }, - ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none()) + { + from: [7, -6, 7], + to: [9, 8, 9], + faces: { + north: {uv: [0.5, 8, 1, 15], texture: '#0'}, + east: {uv: [0, 8, 0.5, 15], texture: '#0'}, + south: {uv: [1.5, 8, 2, 15], texture: '#0'}, + west: {uv: [1, 8, 1.5, 15], texture: '#0'}, + up: {uv: [1, 8, 0.5, 7], texture: '#0'}, + down: {uv: [1.5, 7, 1, 8], texture: '#0'}, + }, + }, + ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none()) + } } -} -function hangingSignRenderer(woodType: string) { - return (attached: boolean, atlas: TextureAtlasProvider) => { - if (attached) { + export function wallSignRenderer(texture: Identifier) { + return (atlas: TextureAtlasProvider) => { return new BlockModel(undefined, { - 0: `entity/signs/hanging/${woodType}`, + 0: texture.withPrefix('entity/signs/').toString(), + }, [ + { + from: [-4, 4, 17], + to: [20, 16, 19], + faces: { + north: {uv: [0.5, 1, 6.5, 7], texture: '#0'}, + east: {uv: [0, 1, 0.5, 7], texture: '#0'}, + south: {uv: [7, 1, 13, 7], texture: '#0'}, + west: {uv: [6.5, 1, 7, 7], texture: '#0'}, + up: {uv: [6.5, 1, 0.5, 0], texture: '#0'}, + down: {uv: [12.5, 0, 6.5, 1], texture: '#0'}, + }, + }, + ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none()) + } + } + + export function hangingSignRenderer(texture: Identifier) { + return (attached: boolean, atlas: TextureAtlasProvider) => { + if (attached) { + return new BlockModel(undefined, { + 0: texture.withPrefix('entity/signs/hanging/').toString(), + }, [ + { + from: [1, 0, 7], + to: [15, 10, 9], + faces: { + north: {uv: [0.5, 7, 4, 12], texture: '#0'}, + east: {uv: [0, 7, 0.5, 12], texture: '#0'}, + south: {uv: [4.5, 7, 8, 12], texture: '#0'}, + west: {uv: [4, 7, 4.5, 12], texture: '#0'}, + up: {uv: [4, 7, 0.5, 6], texture: '#0'}, + down: {uv: [7.5, 6, 4, 7], texture: '#0'}, + }, + }, + { + from: [2, 10, 8], + to: [14, 16, 8], + faces: { + north: {uv: [3.5, 3, 6.5, 6], texture: '#0'}, + south: {uv: [3.5, 3, 6.5, 6], texture: '#0'}, + }, + }, + ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none()) + } + return new BlockModel(undefined, { + 0: texture.withPrefix('entity/signs/hanging/').toString(), }, [ { from: [1, 0, 7], @@ -405,476 +435,451 @@ function hangingSignRenderer(woodType: string) { }, }, { - from: [2, 10, 8], - to: [14, 16, 8], + from: [1.5, 10, 8], + to: [4.5, 16, 8], + rotation: {angle: 45, axis: 'y', origin: [3, 12, 8]}, faces: { - north: {uv: [3.5, 3, 6.5, 6], texture: '#0'}, - south: {uv: [3.5, 3, 6.5, 6], texture: '#0'}, + north: {uv: [0, 3, 0.75, 6], texture: '#0'}, + south: {uv: [0, 3, 0.75, 6], texture: '#0'}, }, }, - ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none()) - } - return new BlockModel(undefined, { - 0: `entity/signs/hanging/${woodType}`, - }, [ - { - from: [1, 0, 7], - to: [15, 10, 9], - faces: { - north: {uv: [0.5, 7, 4, 12], texture: '#0'}, - east: {uv: [0, 7, 0.5, 12], texture: '#0'}, - south: {uv: [4.5, 7, 8, 12], texture: '#0'}, - west: {uv: [4, 7, 4.5, 12], texture: '#0'}, - up: {uv: [4, 7, 0.5, 6], texture: '#0'}, - down: {uv: [7.5, 6, 4, 7], texture: '#0'}, - }, - }, - { - from: [1.5, 10, 8], - to: [4.5, 16, 8], - rotation: {angle: 45, axis: 'y', origin: [3, 12, 8]}, - faces: { - north: {uv: [0, 3, 0.75, 6], texture: '#0'}, - south: {uv: [0, 3, 0.75, 6], texture: '#0'}, - }, - }, - { - from: [3, 10, 6.5], - to: [3, 16, 9.5], - rotation: {angle: 45, axis: 'y', origin: [3, 12, 8]}, - faces: { - east: {uv: [1.5, 3, 2.25, 6], texture: '#0'}, - west: {uv: [1.5, 3, 2.25, 6], texture: '#0'}, + { + from: [3, 10, 6.5], + to: [3, 16, 9.5], + rotation: {angle: 45, axis: 'y', origin: [3, 12, 8]}, + faces: { + east: {uv: [1.5, 3, 2.25, 6], texture: '#0'}, + west: {uv: [1.5, 3, 2.25, 6], texture: '#0'}, + }, }, - }, - { - from: [11.5, 10, 8], - to: [14.5, 16, 8], - rotation: {angle: 45, axis: 'y', origin: [13, 12, 8]}, - faces: { - north: {uv: [0, 3, 0.75, 6], texture: '#0'}, - south: {uv: [0, 3, 0.75, 6], texture: '#0'}, + { + from: [11.5, 10, 8], + to: [14.5, 16, 8], + rotation: {angle: 45, axis: 'y', origin: [13, 12, 8]}, + faces: { + north: {uv: [0, 3, 0.75, 6], texture: '#0'}, + south: {uv: [0, 3, 0.75, 6], texture: '#0'}, + }, }, - }, - { - from: [13, 10, 6.5], - to: [13, 16, 9.5], - rotation: {angle: 45, axis: 'y', origin: [13, 12, 8]}, - faces: { - east: {uv: [1.5, 3, 2.25, 6], texture: '#0'}, - west: {uv: [1.5, 3, 2.25, 6], texture: '#0'}, + { + from: [13, 10, 6.5], + to: [13, 16, 9.5], + rotation: {angle: 45, axis: 'y', origin: [13, 12, 8]}, + faces: { + east: {uv: [1.5, 3, 2.25, 6], texture: '#0'}, + west: {uv: [1.5, 3, 2.25, 6], texture: '#0'}, + }, }, - }, - ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none()) + ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none()) + } } -} -function wallHangingSignRenderer(woodType: string) { - return (atlas: TextureAtlasProvider) => { - return new BlockModel(undefined, { - 0: `entity/signs/hanging/${woodType}`, - }, [ - { - from: [1, 0, 7], - to: [15, 10, 9], - faces: { - north: {uv: [0.5, 7, 4, 12], texture: '#0'}, - east: {uv: [0, 7, 0.5, 12], texture: '#0'}, - south: {uv: [4.5, 7, 8, 12], texture: '#0'}, - west: {uv: [4, 7, 4.5, 12], texture: '#0'}, - up: {uv: [4, 7, 0.5, 6], texture: '#0'}, - down: {uv: [7.5, 6, 4, 7], texture: '#0'}, + export function wallHangingSignRenderer(woodType: string) { + return (atlas: TextureAtlasProvider) => { + return new BlockModel(undefined, { + 0: `entity/signs/hanging/${woodType}`, + }, [ + { + from: [1, 0, 7], + to: [15, 10, 9], + faces: { + north: {uv: [0.5, 7, 4, 12], texture: '#0'}, + east: {uv: [0, 7, 0.5, 12], texture: '#0'}, + south: {uv: [4.5, 7, 8, 12], texture: '#0'}, + west: {uv: [4, 7, 4.5, 12], texture: '#0'}, + up: {uv: [4, 7, 0.5, 6], texture: '#0'}, + down: {uv: [7.5, 6, 4, 7], texture: '#0'}, + }, }, - }, - { - from: [0, 14, 6], - to: [16, 16, 10], - faces: { - north: {uv: [1, 2, 5, 3], texture: '#0'}, - east: {uv: [0, 2, 1, 3], texture: '#0'}, - south: {uv: [6, 2, 10, 3], texture: '#0'}, - west: {uv: [5, 2, 6, 3], texture: '#0'}, - up: {uv: [5, 2, 1, 0], texture: '#0'}, - down: {uv: [9, 0, 5, 2], texture: '#0'}, + { + from: [0, 14, 6], + to: [16, 16, 10], + faces: { + north: {uv: [1, 2, 5, 3], texture: '#0'}, + east: {uv: [0, 2, 1, 3], texture: '#0'}, + south: {uv: [6, 2, 10, 3], texture: '#0'}, + west: {uv: [5, 2, 6, 3], texture: '#0'}, + up: {uv: [5, 2, 1, 0], texture: '#0'}, + down: {uv: [9, 0, 5, 2], texture: '#0'}, + }, }, - }, - { - from: [1.5, 10, 8], - to: [4.5, 16, 8], - rotation: {angle: 45, axis: 'y', origin: [3, 12, 8]}, - faces: { - north: {uv: [0, 3, 0.75, 6], texture: '#0'}, - south: {uv: [0, 3, 0.75, 6], texture: '#0'}, + { + from: [1.5, 10, 8], + to: [4.5, 16, 8], + rotation: {angle: 45, axis: 'y', origin: [3, 12, 8]}, + faces: { + north: {uv: [0, 3, 0.75, 6], texture: '#0'}, + south: {uv: [0, 3, 0.75, 6], texture: '#0'}, + }, }, - }, - { - from: [3, 10, 6.5], - to: [3, 16, 9.5], - rotation: {angle: 45, axis: 'y', origin: [3, 12, 8]}, - faces: { - east: {uv: [1.5, 3, 2.25, 6], texture: '#0'}, - west: {uv: [1.5, 3, 2.25, 6], texture: '#0'}, + { + from: [3, 10, 6.5], + to: [3, 16, 9.5], + rotation: {angle: 45, axis: 'y', origin: [3, 12, 8]}, + faces: { + east: {uv: [1.5, 3, 2.25, 6], texture: '#0'}, + west: {uv: [1.5, 3, 2.25, 6], texture: '#0'}, + }, }, - }, - { - from: [11.5, 10, 8], - to: [14.5, 16, 8], - rotation: {angle: 45, axis: 'y', origin: [13, 12, 8]}, - faces: { - north: {uv: [0, 3, 0.75, 6], texture: '#0'}, - south: {uv: [0, 3, 0.75, 6], texture: '#0'}, + { + from: [11.5, 10, 8], + to: [14.5, 16, 8], + rotation: {angle: 45, axis: 'y', origin: [13, 12, 8]}, + faces: { + north: {uv: [0, 3, 0.75, 6], texture: '#0'}, + south: {uv: [0, 3, 0.75, 6], texture: '#0'}, + }, }, - }, - { - from: [13, 10, 6.5], - to: [13, 16, 9.5], - rotation: {angle: 45, axis: 'y', origin: [13, 12, 8]}, - faces: { - east: {uv: [1.5, 3, 2.25, 6], texture: '#0'}, - west: {uv: [1.5, 3, 2.25, 6], texture: '#0'}, + { + from: [13, 10, 6.5], + to: [13, 16, 9.5], + rotation: {angle: 45, axis: 'y', origin: [13, 12, 8]}, + faces: { + east: {uv: [1.5, 3, 2.25, 6], texture: '#0'}, + west: {uv: [1.5, 3, 2.25, 6], texture: '#0'}, + }, }, - }, - ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none()) + ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none()) + } } -} -function conduitRenderer(atlas: TextureAtlasProvider) { - return new BlockModel(undefined, { - 0: 'entity/conduit/base', - }, [ - { - from: [5, 5, 5], - to: [11, 11, 11], - faces: { - north: {uv: [3, 6, 6, 12], texture: '#0'}, - east: {uv: [0, 6, 3, 12], texture: '#0'}, - south: {uv: [9, 6, 12, 12], texture: '#0'}, - west: {uv: [6, 6, 9, 12], texture: '#0'}, - up: {uv: [6, 6, 3, 0], texture: '#0'}, - down: {uv: [9, 0, 6, 6], texture: '#0'}, - }, - }, - ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none()) -} - -function shulkerBoxRenderer(color: string) { - return (atlas: TextureAtlasProvider) => { + export function conduitRenderer(atlas: TextureAtlasProvider) { return new BlockModel(undefined, { - 0: `entity/shulker/shulker_${color}`, + 0: 'entity/conduit/base', }, [ { - from: [0, 0, 0], - to: [16, 8, 16], - faces: { - north: {uv: [4, 11, 8, 13], texture: '#0'}, - east: {uv: [0, 11, 4, 13], texture: '#0'}, - south: {uv: [12, 11, 16, 13], texture: '#0'}, - west: {uv: [8, 11, 12, 13], texture: '#0'}, - up: {uv: [8, 11, 4, 7], texture: '#0'}, - down: {uv: [12, 7, 8, 11], texture: '#0'}, - }, - }, - { - from: [0, 4, 0], - to: [16, 16, 16], + from: [5, 5, 5], + to: [11, 11, 11], faces: { - north: {uv: [4, 4, 8, 7], texture: '#0'}, - east: {uv: [0, 4, 4, 7], texture: '#0'}, - south: {uv: [12, 4, 16, 7], texture: '#0'}, - west: {uv: [8, 4, 12, 7], texture: '#0'}, - up: {uv: [8, 4, 4, 0], texture: '#0'}, - down: {uv: [12, 0, 8, 4], texture: '#0'}, + north: {uv: [3, 6, 6, 12], texture: '#0'}, + east: {uv: [0, 6, 3, 12], texture: '#0'}, + south: {uv: [9, 6, 12, 12], texture: '#0'}, + west: {uv: [6, 6, 9, 12], texture: '#0'}, + up: {uv: [6, 6, 3, 0], texture: '#0'}, + down: {uv: [9, 0, 6, 6], texture: '#0'}, }, }, ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none()) } -} -function bannerRenderer(color: string) { - return (atlas: TextureAtlasProvider) => { - return new BlockModel(undefined, { - 0: 'entity/banner_base', - }, [ - { - from: [-2, -8, 9], - to: [18, 32, 10], - faces: { - north: {uv: [0.25, 0.25, 5.25, 10.25], texture: '#0'}, - east: {uv: [0, 0.25, 0.25, 10.25], texture: '#0'}, - south: {uv: [5.5, 0.25, 10.5, 10.25], texture: '#0'}, - west: {uv: [5.25, 0.25, 5.5, 10.25], texture: '#0'}, - up: {uv: [5.25, 0.25, 0.25, 0], texture: '#0'}, - down: {uv: [10.25, 0, 5.25, 0.25], texture: '#0'}, + export function shulkerBoxRenderer(texture: Identifier) { + return (atlas: TextureAtlasProvider) => { + return new BlockModel(undefined, { + 0: texture.withPrefix('entity/shulker/').toString(), + }, [ + { + from: [0, 0, 0], + to: [16, 8, 16], + faces: { + north: {uv: [4, 11, 8, 13], texture: '#0'}, + east: {uv: [0, 11, 4, 13], texture: '#0'}, + south: {uv: [12, 11, 16, 13], texture: '#0'}, + west: {uv: [8, 11, 12, 13], texture: '#0'}, + up: {uv: [8, 11, 4, 7], texture: '#0'}, + down: {uv: [12, 7, 8, 11], texture: '#0'}, + }, }, - }, - { - from: [7, -12, 7], - to: [9, 30, 9], - faces: { - north: {uv: [11.5, 0.5, 12, 11], texture: '#0'}, - east: {uv: [11, 0.5, 11.5, 11], texture: '#0'}, - south: {uv: [12.5, 0.5, 13, 11], texture: '#0'}, - west: {uv: [12, 0.5, 12.5, 11], texture: '#0'}, - up: {uv: [12, 0.5, 11.5, 0], texture: '#0'}, - down: {uv: [12.5, 0, 12, 0.5], texture: '#0'}, + { + from: [0, 4, 0], + to: [16, 16, 16], + faces: { + north: {uv: [4, 4, 8, 7], texture: '#0'}, + east: {uv: [0, 4, 4, 7], texture: '#0'}, + south: {uv: [12, 4, 16, 7], texture: '#0'}, + west: {uv: [8, 4, 12, 7], texture: '#0'}, + up: {uv: [8, 4, 4, 0], texture: '#0'}, + down: {uv: [12, 0, 8, 4], texture: '#0'}, + }, }, - }, - { - from: [-2, 30, 7], - to: [18, 32, 9], - faces: { - north: {uv: [0.5, 11, 5.5, 11.5], texture: '#0'}, - east: {uv: [0, 11, 0.5, 11.5], texture: '#0'}, - south: {uv: [6, 11, 11, 11.5], texture: '#0'}, - west: {uv: [5.5, 11, 6, 11.5], texture: '#0'}, - up: {uv: [5.5, 11, 0.5, 10.5], texture: '#0'}, - down: {uv: [10.5, 10.5, 5.5, 11], texture: '#0'}, + ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none()) + } + } + + export function bannerRenderer(color: string) { + return (atlas: TextureAtlasProvider) => { + return new BlockModel(undefined, { + 0: 'entity/banner_base', + }, [ + { + from: [-2, -8, 9], + to: [18, 32, 10], + faces: { + north: {uv: [0.25, 0.25, 5.25, 10.25], texture: '#0'}, + east: {uv: [0, 0.25, 0.25, 10.25], texture: '#0'}, + south: {uv: [5.5, 0.25, 10.5, 10.25], texture: '#0'}, + west: {uv: [5.25, 0.25, 5.5, 10.25], texture: '#0'}, + up: {uv: [5.25, 0.25, 0.25, 0], texture: '#0'}, + down: {uv: [10.25, 0, 5.25, 0.25], texture: '#0'}, + }, }, - }, - ]).getMesh(atlas, Cull.none()) + { + from: [7, -12, 7], + to: [9, 30, 9], + faces: { + north: {uv: [11.5, 0.5, 12, 11], texture: '#0'}, + east: {uv: [11, 0.5, 11.5, 11], texture: '#0'}, + south: {uv: [12.5, 0.5, 13, 11], texture: '#0'}, + west: {uv: [12, 0.5, 12.5, 11], texture: '#0'}, + up: {uv: [12, 0.5, 11.5, 0], texture: '#0'}, + down: {uv: [12.5, 0, 12, 0.5], texture: '#0'}, + }, + }, + { + from: [-2, 30, 7], + to: [18, 32, 9], + faces: { + north: {uv: [0.5, 11, 5.5, 11.5], texture: '#0'}, + east: {uv: [0, 11, 0.5, 11.5], texture: '#0'}, + south: {uv: [6, 11, 11, 11.5], texture: '#0'}, + west: {uv: [5.5, 11, 6, 11.5], texture: '#0'}, + up: {uv: [5.5, 11, 0.5, 10.5], texture: '#0'}, + down: {uv: [10.5, 10.5, 5.5, 11], texture: '#0'}, + }, + }, + ]).getMesh(atlas, Cull.none()) + } + } + + export function wallBannerRenderer(color: string) { + return (atlas: TextureAtlasProvider) => { + return new BlockModel(undefined, { + 0: 'entity/banner_base', + }, [ + { + from: [-2, -8, -1.5], + to: [18, 32, -0.5], + faces: { + north: {uv: [0.25, 0.25, 5.25, 10.25], texture: '#0'}, + east: {uv: [0, 0.25, 0.25, 10.25], texture: '#0'}, + south: {uv: [5.5, 0.25, 10.5, 10.25], texture: '#0'}, + west: {uv: [5.25, 0.25, 5.5, 10.25], texture: '#0'}, + up: {uv: [5.25, 0.25, 0.25, 0], texture: '#0'}, + down: {uv: [10.25, 0, 5.25, 0.25], texture: '#0'}, + }, + }, + { + from: [-2, 30, -3.5], + to: [18, 32, -1.5], + faces: { + north: {uv: [0.5, 11, 5.5, 11.5], texture: '#0'}, + east: {uv: [0, 11, 0.5, 11.5], texture: '#0'}, + south: {uv: [6, 11, 11, 11.5], texture: '#0'}, + west: {uv: [5.5, 11, 6, 11.5], texture: '#0'}, + up: {uv: [5.5, 11, 0.5, 10.5], texture: '#0'}, + down: {uv: [10.5, 10.5, 5.5, 11], texture: '#0'}, + }, + }, + ]).getMesh(atlas, Cull.none()) + } } -} -function wallBannerRenderer(color: string) { - return (atlas: TextureAtlasProvider) => { + export function bellRenderer(atlas: TextureAtlasProvider) { return new BlockModel(undefined, { - 0: 'entity/banner_base', + 0: 'entity/bell/bell_body', }, [ { - from: [-2, -8, -1.5], - to: [18, 32, -0.5], + from: [5, 3, 5], + to: [11, 10, 11], faces: { - north: {uv: [0.25, 0.25, 5.25, 10.25], texture: '#0'}, - east: {uv: [0, 0.25, 0.25, 10.25], texture: '#0'}, - south: {uv: [5.5, 0.25, 10.5, 10.25], texture: '#0'}, - west: {uv: [5.25, 0.25, 5.5, 10.25], texture: '#0'}, - up: {uv: [5.25, 0.25, 0.25, 0], texture: '#0'}, - down: {uv: [10.25, 0, 5.25, 0.25], texture: '#0'}, + north: {uv: [3, 3, 6, 6.5], texture: '#0'}, + east: {uv: [0, 3, 3, 6.5], texture: '#0'}, + south: {uv: [9, 3, 12, 6.5], texture: '#0'}, + west: {uv: [6, 3, 9, 6.5], texture: '#0'}, + up: {uv: [6, 3, 3, 0], texture: '#0'}, + down: {uv: [9, 0, 6, 3], texture: '#0'}, }, }, { - from: [-2, 30, -3.5], - to: [18, 32, -1.5], + from: [4, 10, 4], + to: [12, 12, 12], faces: { - north: {uv: [0.5, 11, 5.5, 11.5], texture: '#0'}, - east: {uv: [0, 11, 0.5, 11.5], texture: '#0'}, - south: {uv: [6, 11, 11, 11.5], texture: '#0'}, - west: {uv: [5.5, 11, 6, 11.5], texture: '#0'}, - up: {uv: [5.5, 11, 0.5, 10.5], texture: '#0'}, - down: {uv: [10.5, 10.5, 5.5, 11], texture: '#0'}, + north: {uv: [4, 10.5, 8, 11.5], texture: '#0'}, + east: {uv: [0, 10.5, 4, 11.5], texture: '#0'}, + south: {uv: [12, 10.5, 16, 11.5], texture: '#0'}, + west: {uv: [8, 10.5, 12, 11.5], texture: '#0'}, + up: {uv: [8, 10.5, 4, 6.5], texture: '#0'}, + down: {uv: [12, 6.5, 8, 10.5], texture: '#0'}, }, }, - ]).getMesh(atlas, Cull.none()) + ]).withUvEpsilon(1 / 64).getMesh(atlas, Cull.none()) } -} - -function bellRenderer(atlas: TextureAtlasProvider) { - return new BlockModel(undefined, { - 0: 'entity/bell/bell_body', - }, [ - { - from: [5, 3, 5], - to: [11, 10, 11], - faces: { - north: {uv: [3, 3, 6, 6.5], texture: '#0'}, - east: {uv: [0, 3, 3, 6.5], texture: '#0'}, - south: {uv: [9, 3, 12, 6.5], texture: '#0'}, - west: {uv: [6, 3, 9, 6.5], texture: '#0'}, - up: {uv: [6, 3, 3, 0], texture: '#0'}, - down: {uv: [9, 0, 6, 3], texture: '#0'}, - }, - }, - { - from: [4, 10, 4], - to: [12, 12, 12], - faces: { - north: {uv: [4, 10.5, 8, 11.5], texture: '#0'}, - east: {uv: [0, 10.5, 4, 11.5], texture: '#0'}, - south: {uv: [12, 10.5, 16, 11.5], texture: '#0'}, - west: {uv: [8, 10.5, 12, 11.5], texture: '#0'}, - up: {uv: [8, 10.5, 4, 6.5], texture: '#0'}, - down: {uv: [12, 6.5, 8, 10.5], texture: '#0'}, - }, - }, - ]).withUvEpsilon(1/64).getMesh(atlas, Cull.none()) -} -function bedRenderer(color: string) { - return (part: string, atlas: TextureAtlasProvider) => { - if (part === 'foot') { + export function bedRenderer(texture: Identifier) { + return (part: string, atlas: TextureAtlasProvider) => { + if (part === 'foot') { + return new BlockModel(undefined, { + 0: texture.withPrefix('entity/bed/').toString(), + }, [ + { + from: [0, 3, 0], + to: [16, 9, 16], + faces: { + north: {uv: [5.5, 5.5, 9.5, 7], rotation: 180, texture: '#0'}, + east: {uv: [0, 7, 1.5, 11], rotation: 270, texture: '#0'}, + west: {uv: [5.5, 7, 7, 11], rotation: 90, texture: '#0'}, + up: {uv: [5.5, 11, 1.5, 7], texture: '#0'}, + down: {uv: [11, 7, 7, 11], texture: '#0'}, + }, + }, + { + from: [0, 0, 0], + to: [3, 3, 3], + faces: { + north: {uv: [12.5, 5.25, 13.25, 6], texture: '#0'}, + east: {uv: [14.75, 5.25, 15.5, 6], texture: '#0'}, + south: {uv: [14, 5.25, 14.75, 6], texture: '#0'}, + west: {uv: [13.25, 5.25, 14, 6], texture: '#0'}, + up: {uv: [13.25, 4.5, 14, 5.25], texture: '#0'}, + down: {uv: [14, 4.5, 14.75, 5.25], texture: '#0'}, + }, + }, + { + from: [13, 0, 0], + to: [16, 3, 3], + faces: { + north: {uv: [13.25, 3.75, 14, 4.5], texture: '#0'}, + east: {uv: [12.5, 3.75, 13.25, 4.5], texture: '#0'}, + south: {uv: [14.75, 3.75, 15.5, 4.5], texture: '#0'}, + west: {uv: [14, 3.75, 14.75, 4.5], texture: '#0'}, + up: {uv: [13.25, 3, 14, 3.75], texture: '#0'}, + down: {uv: [14, 3, 14.75, 3.75], texture: '#0'}, + }, + }, + ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none()) + } return new BlockModel(undefined, { - 0: `entity/bed/${color}`, + 0: texture.withPrefix('entity/bed/').toString(), }, [ { from: [0, 3, 0], to: [16, 9, 16], faces: { - north: {uv: [5.5, 5.5, 9.5, 7], rotation: 180, texture: '#0'}, - east: {uv: [0, 7, 1.5, 11], rotation: 270, texture: '#0'}, - west: {uv: [5.5, 7, 7, 11], rotation: 90, texture: '#0'}, - up: {uv: [5.5, 11, 1.5, 7], texture: '#0'}, - down: {uv: [11, 7, 7, 11], texture: '#0'}, + east: {uv: [0, 1.5, 1.5, 5.5], rotation: 270, texture: '#0'}, + south: {uv: [1.5, 0, 5.5, 1.5], rotation: 180, texture: '#0'}, + west: {uv: [5.5, 1.5, 7, 5.5], rotation: 90, texture: '#0'}, + up: {uv: [5.5, 5.5, 1.5, 1.5], texture: '#0'}, + down: {uv: [11, 1.5, 7, 5.5], texture: '#0'}, }, }, { - from: [0, 0, 0], - to: [3, 3, 3], + from: [0, 0, 13], + to: [3, 3, 16], faces: { - north: {uv: [12.5, 5.25, 13.25, 6], texture: '#0'}, - east: {uv: [14.75, 5.25, 15.5, 6], texture: '#0'}, - south: {uv: [14, 5.25, 14.75, 6], texture: '#0'}, - west: {uv: [13.25, 5.25, 14, 6], texture: '#0'}, - up: {uv: [13.25, 4.5, 14, 5.25], texture: '#0'}, - down: {uv: [14, 4.5, 14.75, 5.25], texture: '#0'}, + north: {uv: [14.75, 0.75, 15.5, 1.5], texture: '#0'}, + east: {uv: [14, 0.75, 14.75, 1.5], texture: '#0'}, + south: {uv: [13.25, 0.75, 14, 1.5], texture: '#0'}, + west: {uv: [12.5, 0.75, 13.25, 1.5], texture: '#0'}, + up: {uv: [13.25, 0, 14, 0.75], texture: '#0'}, + down: {uv: [14, 0, 14.75, 0.75], texture: '#0'}, }, }, { - from: [13, 0, 0], - to: [16, 3, 3], + from: [13, 0, 13], + to: [16, 3, 16], faces: { - north: {uv: [13.25, 3.75, 14, 4.5], texture: '#0'}, - east: {uv: [12.5, 3.75, 13.25, 4.5], texture: '#0'}, - south: {uv: [14.75, 3.75, 15.5, 4.5], texture: '#0'}, - west: {uv: [14, 3.75, 14.75, 4.5], texture: '#0'}, - up: {uv: [13.25, 3, 14, 3.75], texture: '#0'}, - down: {uv: [14, 3, 14.75, 3.75], texture: '#0'}, + north: {uv: [14, 2.25, 14.75, 3], texture: '#0'}, + east: {uv: [13.25, 2.25, 14, 3], texture: '#0'}, + south: {uv: [12.5, 2.25, 13.25, 3], texture: '#0'}, + west: {uv: [14.75, 2.25, 15.5, 3], texture: '#0'}, + up: {uv: [13.25, 1.5, 14, 2.25], texture: '#0'}, + down: {uv: [14, 1.5, 14.75, 2.25], texture: '#0'}, }, }, ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none()) } - return new BlockModel(undefined, { - 0: `entity/bed/${color}`, - }, [ - { - from: [0, 3, 0], - to: [16, 9, 16], - faces: { - east: {uv: [0, 1.5, 1.5, 5.5], rotation: 270, texture: '#0'}, - south: {uv: [1.5, 0, 5.5, 1.5], rotation: 180, texture: '#0'}, - west: {uv: [5.5, 1.5, 7, 5.5], rotation: 90, texture: '#0'}, - up: {uv: [5.5, 5.5, 1.5, 1.5], texture: '#0'}, - down: {uv: [11, 1.5, 7, 5.5], texture: '#0'}, - }, - }, - { - from: [0, 0, 13], - to: [3, 3, 16], - faces: { - north: {uv: [14.75, 0.75, 15.5, 1.5], texture: '#0'}, - east: {uv: [14, 0.75, 14.75, 1.5], texture: '#0'}, - south: {uv: [13.25, 0.75, 14, 1.5], texture: '#0'}, - west: {uv: [12.5, 0.75, 13.25, 1.5], texture: '#0'}, - up: {uv: [13.25, 0, 14, 0.75], texture: '#0'}, - down: {uv: [14, 0, 14.75, 0.75], texture: '#0'}, - }, - }, - { - from: [13, 0, 13], - to: [16, 3, 16], - faces: { - north: {uv: [14, 2.25, 14.75, 3], texture: '#0'}, - east: {uv: [13.25, 2.25, 14, 3], texture: '#0'}, - south: {uv: [12.5, 2.25, 13.25, 3], texture: '#0'}, - west: {uv: [14.75, 2.25, 15.5, 3], texture: '#0'}, - up: {uv: [13.25, 1.5, 14, 2.25], texture: '#0'}, - down: {uv: [14, 1.5, 14.75, 2.25], texture: '#0'}, - }, - }, - ]).withUvEpsilon(1/128).getMesh(atlas, Cull.none()) } -} -function getStr(block: BlockState, key: string, fallback = '') { - return block.getProperty(key) ?? fallback -} - -function getInt(block: BlockState, key: string, fallback = '0') { - return parseInt(block.getProperty(key) ?? fallback) -} - -const ChestRenderers = new Map(Object.entries({ - 'minecraft:chest': chestRenderer('normal'), - 'minecraft:ender_chest': chestRenderer('ender'), - 'minecraft:trapped_chest': chestRenderer('trapped'), -})) - -const SkullRenderers = new Map(Object.entries({ - 'minecraft:skeleton_skull': skullRenderer('skeleton/skeleton', 2), - 'minecraft:wither_skeleton_skull': skullRenderer('skeleton/wither_skeleton', 2), - 'minecraft:zombie_head': skullRenderer('zombie/zombie', 1), - 'minecraft:creeper_head': skullRenderer('creeper/creeper', 2), - 'minecraft:dragon_head': dragonHeadRenderer, - 'minecraft:piglin_head': piglinHeadRenderer, - 'minecraft:player_head': skullRenderer('player/wide/steve', 1), // TODO: fix texture -})) - -const WoodTypes = [ - 'oak', - 'spruce', - 'birch', - 'jungle', - 'acacia', - 'dark_oak', - 'mangrove', - 'cherry', - 'bamboo', - 'crimson', - 'warped', -] - -const SignRenderers = new Map(WoodTypes.map(type => - [`minecraft:${type}_sign`, signRenderer(type)] -)) - -const WallSignRenderers = new Map(WoodTypes.map(type => - [`minecraft:${type}_wall_sign`, wallSignRenderer(type)] -)) - -const HangingSignRenderers = new Map(WoodTypes.map(type => - [`minecraft:${type}_hanging_sign`, hangingSignRenderer(type)] -)) - -const WallHangingSignRenderers = new Map(WoodTypes.map(type => - [`minecraft:${type}_wall_hanging_sign`, wallHangingSignRenderer(type)] -)) - -const DyeColors = [ - 'white', - 'orange', - 'magenta', - 'light_blue', - 'yellow', - 'lime', - 'pink', - 'gray', - 'light_gray', - 'cyan', - 'purple', - 'blue', - 'brown', - 'green', - 'red', - 'black', -] - -const ShulkerBoxRenderers = new Map(DyeColors.map(color => - [`minecraft:${color}_shulker_box`, shulkerBoxRenderer(color)] -)) + function getStr(block: BlockState, key: string, fallback = '') { + return block.getProperty(key) ?? fallback + } -const BedRenderers = new Map(DyeColors.map(color => - [`minecraft:${color}_bed`, bedRenderer(color)] -)) + function getInt(block: BlockState, key: string, fallback = '0') { + return parseInt(block.getProperty(key) ?? fallback) + } -const BannerRenderers = new Map(DyeColors.map(color => - [`minecraft:${color}_banner`, bannerRenderer(color)] -)) + const ChestRenderers = new Map(Object.entries({ + 'minecraft:chest': SpecialRenderers.chestRenderer(Identifier.create('normal')), + 'minecraft:ender_chest': SpecialRenderers.chestRenderer(Identifier.create('ender')), + 'minecraft:trapped_chest': SpecialRenderers.chestRenderer(Identifier.create('trapped')), + })) + + const SkullRenderers = new Map(Object.entries({ + 'minecraft:skeleton_skull': SpecialRenderers.headRenderer(Identifier.create('skeleton/skeleton'), 2), + 'minecraft:wither_skeleton_skull': SpecialRenderers.headRenderer(Identifier.create('skeleton/wither_skeleton'), 2), + 'minecraft:zombie_head': SpecialRenderers.headRenderer(Identifier.create('zombie/zombie'), 1), + 'minecraft:creeper_head': SpecialRenderers.headRenderer(Identifier.create('creeper/creeper'), 2), + 'minecraft:dragon_head': SpecialRenderers.dragonHeadRenderer(), + 'minecraft:piglin_head': SpecialRenderers.piglinHeadRenderer(), + 'minecraft:player_head': SpecialRenderers.headRenderer(Identifier.create('player/wide/steve'), 1), // TODO: fix texture + })) + + const WoodTypes = [ + 'oak', + 'spruce', + 'birch', + 'jungle', + 'acacia', + 'dark_oak', + 'mangrove', + 'cherry', + 'bamboo', + 'crimson', + 'warped', + ] + + const SignRenderers = new Map(WoodTypes.map(type => + [`minecraft:${type}_sign`, SpecialRenderers.signRenderer(Identifier.create(type))] + )) + + const WallSignRenderers = new Map(WoodTypes.map(type => + [`minecraft:${type}_wall_sign`, SpecialRenderers.wallSignRenderer(Identifier.create(type))] + )) + + const HangingSignRenderers = new Map(WoodTypes.map(type => + [`minecraft:${type}_hanging_sign`, SpecialRenderers.hangingSignRenderer(Identifier.create(type))] + )) + + const WallHangingSignRenderers = new Map(WoodTypes.map(type => + [`minecraft:${type}_wall_hanging_sign`, SpecialRenderers.wallHangingSignRenderer(type)] + )) + + const DyeColors = [ + 'white', + 'orange', + 'magenta', + 'light_blue', + 'yellow', + 'lime', + 'pink', + 'gray', + 'light_gray', + 'cyan', + 'purple', + 'blue', + 'brown', + 'green', + 'red', + 'black', + ] + + const ShulkerBoxRenderers = new Map(DyeColors.map(color => + [`minecraft:${color}_shulker_box`, SpecialRenderers.shulkerBoxRenderer(Identifier.create(`shulker_${color}`))] + )) + + const BedRenderers = new Map(DyeColors.map(color => + [`minecraft:${color}_bed`, SpecialRenderers.bedRenderer(Identifier.create(color))] + )) + + const BannerRenderers = new Map(DyeColors.map(color => + [`minecraft:${color}_banner`, SpecialRenderers.bannerRenderer(color)] + )) + + const WallBannerRenderers = new Map(DyeColors.map(color => + [`minecraft:${color}_wall_banner`, SpecialRenderers.wallBannerRenderer(color)] + )) -const WallBannerRenderers = new Map(DyeColors.map(color => - [`minecraft:${color}_wall_banner`, wallBannerRenderer(color)] -)) -export namespace SpecialRenderers { - function getMesh(block: BlockState, atlas: TextureAtlasProvider, cull: Cull): Mesh { + export function getBlockMesh(block: BlockState, atlas: TextureAtlasProvider, cull: Cull): Mesh { if (block.is('water')) { return liquidRenderer('water', getInt(block, 'level'), atlas, cull, 0) } @@ -1001,38 +1006,9 @@ export namespace SpecialRenderers { if (block.isWaterlogged()) { mesh.merge(liquidRenderer('water', 0, atlas, cull, 0)) } - return mesh - } - export function getBlockMesh(block: BlockState, atlas: TextureAtlasProvider, cull: Cull): Mesh { - const mesh = getMesh(block, atlas, cull) const t = mat4.create() mat4.scale(t, t, [0.0625, 0.0625, 0.0625]) return mesh.transform(t) } - - export function getItemMesh(item: ItemStack, atlas: TextureAtlasProvider): Mesh { - if (item.is('shield')) { - const shieldMesh = shieldRenderer(atlas) - const t = mat4.create() - mat4.translate(t, t, [-3, 1, 0]) - mat4.rotateX(t, t, -10 * Math.PI/180) - mat4.rotateY(t, t, -10 * Math.PI/180) - mat4.rotateZ(t, t, -5 * Math.PI/180) - return shieldMesh.transform(t) - } - const bedRenderer = BedRenderers.get(item.id.toString()) - if (bedRenderer !== undefined) { - const headMesh = getMesh(new BlockState(item.id, { part: 'head' }), atlas, Cull.none()) - const footMesh = getMesh(new BlockState(item.id, { part: 'foot' }), atlas, Cull.none()) - const t = mat4.create() - mat4.translate(t, t, [0, 0, -16]) - return headMesh.merge(footMesh.transform(t)) - } - if (item.is('bell') || SignRenderers.has(item.id.toString()) || HangingSignRenderers.has(item.id.toString())) { - return new Mesh() - } - // Assumes block and item ID are the same - return getMesh(new BlockState(item.id), atlas, Cull.none()) - } } diff --git a/src/render/index.ts b/src/render/index.ts index f636bd1..a679b60 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -3,7 +3,6 @@ export * from './BlockDefinition.js' export * from './BlockModel.js' export * from './ChunkBuilder.js' export * from './Cull.js' -export * from './ItemColors.js' export * from './ItemRenderer.js' export * from './Line.js' export * from './Mesh.js' @@ -15,3 +14,4 @@ export * from './StructureRenderer.js' export * from './TextureAtlas.js' export * from './Vertex.js' export * from './VoxelRenderer.js' + diff --git a/src/util/Color.ts b/src/util/Color.ts new file mode 100644 index 0000000..08ddbd7 --- /dev/null +++ b/src/util/Color.ts @@ -0,0 +1,29 @@ +import { NbtTag } from "../nbt/index.js" +import { Json } from "./Json.js" + +export type Color = [number, number, number] + +export namespace Color { + export function fromJson(obj: unknown): Color | undefined { + const packed = Json.readNumber(obj) + if (packed) return intToRgb(packed) + const array = Json.readArray(obj, o => Json.readNumber(o) ?? 0) + if (array === undefined || array.length !== 3) return undefined + return array as [number, number, number] + } + + export function fromNbt(nbt: NbtTag): Color | undefined { + if (nbt.isNumber()) return intToRgb(nbt.getAsNumber()) + if (!nbt.isListOrArray()) return undefined + const values = nbt.getItems() + if (values.length < 3) return undefined + return values.map(i => i.getAsNumber()) as Color + } + + export function intToRgb(n: number): Color { + const r = (n >> 16) & 255 + const g = (n >> 8) & 255 + const b = n & 255 + return [r / 255, g / 255, b / 255] + } +} \ No newline at end of file diff --git a/src/util/Util.ts b/src/util/Util.ts index cb6cb05..c2533a3 100644 --- a/src/util/Util.ts +++ b/src/util/Util.ts @@ -1,3 +1,4 @@ + export function lazy(getter: () => T): () => T { let value: T | null = null return () => { @@ -25,12 +26,3 @@ export function mutateWithDefault(map: Map, key: K, initialValue: V, map.set(key, value) return value } - -export type Color = [number, number, number] - -export function intToRgb(n: number): Color { - const r = (n >> 16) & 255 - const g = (n >> 8) & 255 - const b = n & 255 - return [r / 255, g / 255, b / 255] -} diff --git a/src/util/index.ts b/src/util/index.ts index b2495f9..ca3c298 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -1,3 +1,5 @@ +export * from './Color.js' export * from './Json.js' export * from './StringReader.js' export * from './Util.js' + diff --git a/test/render/ItemModel.test.ts b/test/render/ItemModel.test.ts new file mode 100644 index 0000000..caa6110 --- /dev/null +++ b/test/render/ItemModel.test.ts @@ -0,0 +1,301 @@ +import { describe, expect, it, vi } from 'vitest' +import { BlockModel, Color, Identifier, ItemRendererResources, ItemStack } from '../../src' +import { ItemModel } from '../../src/render/ItemModel' + +describe('ItemModel', () => { + const dummyItem = new ItemStack(Identifier.parse('dummy:dummy'), 1) + + const blockModels = { + 'test:1': new BlockModel(undefined, undefined, undefined), + 'test:2': new BlockModel(undefined, undefined, undefined) + } + + const blockModel1 = vi.spyOn(blockModels['test:1'], 'getMesh') + const blockModel2 = vi.spyOn(blockModels['test:2'], 'getMesh') + + const resources: ItemRendererResources = { + getBlockModel(id) { return blockModels[id.toString()] }, + getItemModel(id) { return null }, + getTextureAtlas() { return new ImageData(0, 0) }, + getTextureUV(texture) { return [0, 0, 0, 0] }, + } + + it('Model', () => { + const model = ItemModel.fromJson({ + type: 'model', + model: 'test:1', + tints: [ + { + type: "constant", + value: [0.5, 0.6, 0.7] + } + ] + }) + + blockModel1.mockClear() + model.getMesh(dummyItem, resources, {}) + expect(blockModel1).toHaveBeenCalledOnce() + const tint = blockModel1.mock.calls[0][2] + expect(tint).toBeTypeOf('function') + expect((tint as (index: number) => Color)(0)).toEqual([0.5, 0.6, 0.7]) + expect((tint as (index: number) => Color)(1)).toEqual([1, 1, 1]) + }) + + it('Composite', () => { + const model = ItemModel.fromJson({ + type: 'composite', + models: [ + { + type: 'model', + model: 'test:1', + }, + { + type: 'model', + model: 'test:1', + } + ], + }) + + blockModel1.mockClear() + model.getMesh(dummyItem, resources, {}) + expect(blockModel1).toHaveBeenCalledTimes(2) + }) + + + it('Condition', () => { + const model = ItemModel.fromJson({ + type: 'condition', + property: 'carried', + on_true: { + type: 'model', + model: 'test:1', + }, + on_false: { + type: 'model', + model: 'test:2', + } + }) + + blockModel1.mockClear() + blockModel2.mockClear() + model.getMesh(dummyItem, resources, {carried: true}) + expect(blockModel1).toHaveBeenCalledOnce() + expect(blockModel2).not.toHaveBeenCalled() + + blockModel1.mockClear() + blockModel2.mockClear() + model.getMesh(dummyItem, resources, {carried: false}) + expect(blockModel1).not.toHaveBeenCalled() + expect(blockModel2).toHaveBeenCalledOnce() + }) + + it('Condition properties', () => { + const fishing_rod_cast = ItemModel.Condition.propertyFromJson({property: 'fishing_rod/cast'}) + expect(fishing_rod_cast(dummyItem, {'fishing_rod/cast': true})).toBeTruthy() + expect(fishing_rod_cast(dummyItem, {'fishing_rod/cast': false})).toBeFalsy() + + const selected = ItemModel.Condition.propertyFromJson({property: 'selected'}) + expect(selected(dummyItem, {selected: true})).toBeTruthy() + expect(selected(dummyItem, {selected: false})).toBeFalsy() + + const carried = ItemModel.Condition.propertyFromJson({property: 'carried'}) + expect(carried(dummyItem, {carried: true})).toBeTruthy() + expect(carried(dummyItem, {carried: false})).toBeFalsy() + + const extended_view = ItemModel.Condition.propertyFromJson({property: 'extended_view'}) + expect(extended_view(dummyItem, {extended_view: true})).toBeTruthy() + expect(extended_view(dummyItem, {extended_view: false})).toBeFalsy() + + const view_entity = ItemModel.Condition.propertyFromJson({property: 'view_entity'}) + expect(view_entity(dummyItem, {context_entity_is_view_entity: true})).toBeTruthy() + expect(view_entity(dummyItem, {context_entity_is_view_entity: false})).toBeFalsy() + + const using_item = ItemModel.Condition.propertyFromJson({property: 'using_item'}) + expect(using_item(dummyItem, {use_duration: 0})).toBeTruthy() + expect(using_item(dummyItem, {use_duration: -1})).toBeFalsy() + + const bundle_has_selected_item = ItemModel.Condition.propertyFromJson({property: 'bundle/has_selected_item'}) + expect(bundle_has_selected_item(dummyItem, {'bundle/selected_item': 0})).toBeTruthy() + expect(bundle_has_selected_item(dummyItem, {'bundle/selected_item': -1})).toBeFalsy() + + const keybind_down = ItemModel.Condition.propertyFromJson({property: 'keybind_down', keybind: 'testkey'}) + expect(keybind_down(dummyItem, {keybind_down: ['testkey', 'a', 'b']})).toBeTruthy() + expect(keybind_down(dummyItem, {keybind_down: ['a', 'b']})).toBeFalsy() + + const broken = ItemModel.Condition.propertyFromJson({property: 'broken'}) + expect(broken(ItemStack.fromString('dummy:dummy[damage=99,max_damage=100]'), {})).toBeTruthy() + expect(broken(ItemStack.fromString('dummy:dummy[damage=98,max_damage=100]'), {})).toBeFalsy() + expect(broken(dummyItem, {})).toBeFalsy() + + const damaged = ItemModel.Condition.propertyFromJson({property: 'damaged'}) + expect(damaged(ItemStack.fromString('dummy:dummy[damage=1,max_damage=100]'), {})).toBeTruthy() + expect(damaged(ItemStack.fromString('dummy:dummy[damage=0,max_damage=100]'), {})).toBeFalsy() + expect(damaged(dummyItem, {})).toBeFalsy() + + const has_component = ItemModel.Condition.propertyFromJson({property: 'has_component', component: 'glider'}) + expect(has_component(ItemStack.fromString('dummy:dummy[minecraft:glider={}]'), {})).toBeTruthy() + expect(has_component(dummyItem, {})).toBeFalsy() + + const custom_model_data = ItemModel.Condition.propertyFromJson({property: 'custom_model_data', index: 1}) + expect(custom_model_data(ItemStack.fromString('dummy:dummy[custom_model_data={flags:[false, true, false]}]'), {})).toBeTruthy() + expect(custom_model_data(ItemStack.fromString('dummy:dummy[custom_model_data={flags:[true, false, true]}]'), {})).toBeFalsy() + expect(custom_model_data(ItemStack.fromString('dummy:dummy[custom_model_data={flags:[true]}]'), {})).toBeFalsy() + expect(custom_model_data(dummyItem, {})).toBeFalsy() + }) + + it('Select', () => { + const model = ItemModel.fromJson({ + type: 'select', + property: 'context_entity_type', + cases: [ + { + when: 'minecraft:zombie', + model: { + type: 'model', + model: 'test:1', + }, + } + ], + fallback: { + type: 'model', + model: 'test:2', + } + }) + + blockModel1.mockClear() + blockModel2.mockClear() + model.getMesh(dummyItem, resources, {context_entity_type: Identifier.create('zombie')}) + expect(blockModel1).toHaveBeenCalledOnce() + expect(blockModel2).not.toHaveBeenCalled() + + blockModel1.mockClear() + blockModel2.mockClear() + model.getMesh(dummyItem, resources, {context_entity_type: Identifier.create('skeleton')}) + expect(blockModel1).not.toHaveBeenCalled() + expect(blockModel2).toHaveBeenCalledOnce() + }) + + it('Select properties', () => { + const main_hand = ItemModel.Select.propertyFromJson({property: 'main_hand'}) + expect(main_hand(dummyItem, {'main_hand': 'left'})).toEqual('left') + expect(main_hand(dummyItem, {'main_hand': 'right'})).toEqual('right') + + const display_context = ItemModel.Select.propertyFromJson({property: 'display_context'}) + expect(display_context(dummyItem, {'display_context': 'gui'})).toEqual('gui') + expect(display_context(dummyItem, {'display_context': 'fixed'})).toEqual('fixed') + + const context_entity_type = ItemModel.Select.propertyFromJson({property: 'context_entity_type'}) + expect(context_entity_type(dummyItem, {'context_entity_type': Identifier.create('zombie')})).toEqual('minecraft:zombie') + expect(context_entity_type(dummyItem, {})).toBeNull() + + const context_dimension = ItemModel.Select.propertyFromJson({property: 'context_dimension'}) + expect(context_dimension(dummyItem, {'context_dimension': Identifier.parse('test:test')})).toEqual('test:test') + expect(context_dimension(dummyItem, {})).toBeNull() + + const charge_type = ItemModel.Select.propertyFromJson({property: 'charge_type'}) + expect(charge_type(ItemStack.fromString('dummy:dummy[charged_projectiles=[{id:"minecraft:arrow"}, {id:"minecraft:firework_rocket"}]]'), {})).toEqual('rocket') + expect(charge_type(ItemStack.fromString('dummy:dummy[charged_projectiles=[{id:"minecraft:arrow"}, {id:"minecraft:arrow"}]]'), {})).toEqual('arrow') + expect(charge_type(ItemStack.fromString('dummy:dummy[charged_projectiles=[]]'), {})).toEqual('none') + expect(charge_type(dummyItem, {})).toEqual('none') + + const trim_material = ItemModel.Select.propertyFromJson({property: 'trim_material'}) + expect(trim_material(ItemStack.fromString('dummy:dummy[trim={material:"gold"}]'), {})).toEqual('minecraft:gold') + expect(trim_material(dummyItem, {})).toBeNull() + + const block_state = ItemModel.Select.propertyFromJson({property: 'block_state', block_state_property: 'facing'}) + expect(block_state(ItemStack.fromString('dummy:dummy[block_state={facing: "east"}]'), {})).toEqual('east') + expect(block_state(dummyItem, {})).toBeNull() + + const custom_model_data = ItemModel.Select.propertyFromJson({property: 'custom_model_data', index: 1}) + expect(custom_model_data(ItemStack.fromString('dummy:dummy[custom_model_data={strings:["a", "b"]}]'), {})).toEqual('b') + expect(custom_model_data(ItemStack.fromString('dummy:dummy[custom_model_data={strings:["a"]}]'), {})).toBeNull() + expect(custom_model_data(dummyItem, {})).toBeNull() + + // not testing local_time as it is not implemented + }) + + it('RangeDisptach', () => { + const model = ItemModel.fromJson({ + type: 'range_dispatch', + property: 'use_duration', + remaining: false, + entries: [ + { + threshold: 2, + model: { + type: 'model', + model: 'test:1', + }, + } + ], + fallback: { + type: 'model', + model: 'test:2', + } + }) + + blockModel1.mockClear() + blockModel2.mockClear() + model.getMesh(dummyItem, resources, {use_duration: 3, max_use_duration:10}) + expect(blockModel1).toHaveBeenCalledOnce() + expect(blockModel2).not.toHaveBeenCalled() + + blockModel1.mockClear() + blockModel2.mockClear() + model.getMesh(dummyItem, resources, {use_duration: 1, max_use_duration:10}) + expect(blockModel1).not.toHaveBeenCalled() + expect(blockModel2).toHaveBeenCalled() + }) + + it('RangeDisptach properties', () => { + const time_daytime = ItemModel.RangeDispatch.propertyFromJson({property: 'time', source: 'daytime'}) + expect(time_daytime(dummyItem, {game_time: 6000})).toEqual(0) + expect(time_daytime(dummyItem, {game_time: 18000})).toEqual(0.5) + + const time_moon_phase = ItemModel.RangeDispatch.propertyFromJson({property: 'time', source: 'moon_phase'}) + expect(time_moon_phase(dummyItem, {game_time: 0})).toEqual(0) + expect(time_moon_phase(dummyItem, {game_time: 24000})).toEqual(1/8) + expect(time_moon_phase(dummyItem, {game_time: 7.5*24000})).toEqual(7.5/8) + expect(time_moon_phase(dummyItem, {game_time: 8*24000})).toEqual(0) + + const use_duration_remaining = ItemModel.RangeDispatch.propertyFromJson({property: 'use_duration', remaining: true}) + expect(use_duration_remaining(dummyItem, {use_duration: 70, max_use_duration: 100})).toEqual(30) + + const use_duration = ItemModel.RangeDispatch.propertyFromJson({property: 'use_duration', remaining: false}) + expect(use_duration(dummyItem, {use_duration: 70, max_use_duration: 100})).toEqual(70) + + const use_cycle = ItemModel.RangeDispatch.propertyFromJson({property: 'use_cycle', period: 20}) + expect(use_cycle(dummyItem, {use_duration: 70, max_use_duration: 100})).toEqual(10) + + const damage = ItemModel.RangeDispatch.propertyFromJson({property: 'damage', normalize: false}) + expect(damage(ItemStack.fromString('dummy:dummy[damage=70,max_damage=100]'), {})).toEqual(70) + expect(damage(ItemStack.fromString('dummy:dummy[damage=120,max_damage=100]'), {})).toEqual(100) + + const damage_normalized = ItemModel.RangeDispatch.propertyFromJson({property: 'damage', normalize: true}) + expect(damage_normalized(ItemStack.fromString('dummy:dummy[damage=70,max_damage=100]'), {})).toEqual(0.7) + expect(damage_normalized(ItemStack.fromString('dummy:dummy[damage=120,max_damage=100]'), {})).toEqual(1) + + const count = ItemModel.RangeDispatch.propertyFromJson({property: 'count', normalize: false}) + const count_normalized = ItemModel.RangeDispatch.propertyFromJson({property: 'count', normalize: true}) + const itemStack = ItemStack.fromString('dummy:dummy[max_stack_size=64]') + expect(count(itemStack, {})).toEqual(1) + expect(count_normalized(itemStack, {})).toEqual(1/64) + itemStack.count = 10 + expect(count(itemStack, {})).toEqual(10) + expect(count_normalized(itemStack, {})).toEqual(10/64) + itemStack.count = 100 + expect(count(itemStack, {})).toEqual(64) + expect(count_normalized(itemStack, {})).toEqual(1) + + const cooldown = ItemModel.RangeDispatch.propertyFromJson({property: 'cooldown'}) + expect(cooldown(dummyItem, {cooldown_percentage: {'dummy:dummy': 0.7}})).toEqual(0.7) + expect(cooldown(ItemStack.fromString('dummy:dummy[use_cooldown={cooldown_group:"test"}]'), {cooldown_percentage: {'minecraft:test': 0.6}})).toEqual(0.6) + + const custom_model_data = ItemModel.RangeDispatch.propertyFromJson({property: 'custom_model_data', index: 1}) + expect(custom_model_data(ItemStack.fromString('dummy:dummy[custom_model_data={floats:[0.7, 0.4]}]'), {})).toEqual(0.4) + expect(custom_model_data(ItemStack.fromString('dummy:dummy[custom_model_data={floats:[0.7]}]'), {})).toEqual(0) + expect(custom_model_data(dummyItem, {})).toEqual(0) + + // not testing compass and crossbow/pull as they are not properly implemented + }) +}) \ No newline at end of file diff --git a/test/render/ItemTint.test.ts b/test/render/ItemTint.test.ts new file mode 100644 index 0000000..95da081 --- /dev/null +++ b/test/render/ItemTint.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from 'vitest' +import { Color, ItemStack } from '../../src' +import { ItemTint } from '../../src/render/ItemTint' + +describe('ItemTint', () => { + const TEST_COLOR = [0.1, 0.2, 0.3] as Color + + it('fromJson', () => { + const constant = ItemTint.fromJson({type: 'constant', value: TEST_COLOR}) + expect(constant).toBeInstanceOf(ItemTint.Constant) + expect((constant as ItemTint.Constant).value).toEqual(TEST_COLOR) + + const dye = ItemTint.fromJson({type: 'dye', default: TEST_COLOR}) + expect(dye).toBeInstanceOf(ItemTint.Dye) + expect((dye as ItemTint.Dye).default_color).toEqual(TEST_COLOR) + + const grass = ItemTint.fromJson({type: 'grass', temperature: 0.7, downfall: 0.4}) + expect(grass).toBeInstanceOf(ItemTint.Grass) + expect((grass as ItemTint.Grass).temperature).toEqual(0.7) + expect((grass as ItemTint.Grass).downfall).toEqual(0.4) + + const firework = ItemTint.fromJson({type: 'firework', default: TEST_COLOR}) + expect(firework).toBeInstanceOf(ItemTint.Firework) + expect((firework as ItemTint.Firework).default_color).toEqual(TEST_COLOR) + + const potion = ItemTint.fromJson({type: 'potion', default: TEST_COLOR}) + expect(potion).toBeInstanceOf(ItemTint.Potion) + expect((potion as ItemTint.Potion).default_color).toEqual(TEST_COLOR) + + const map_color = ItemTint.fromJson({type: 'map_color', default: TEST_COLOR}) + expect(map_color).toBeInstanceOf(ItemTint.MapColor) + expect((map_color as ItemTint.MapColor).default_color).toEqual(TEST_COLOR) + + const custom_model_data = ItemTint.fromJson({type: 'custom_model_data', index: 2, default: TEST_COLOR}) + expect(custom_model_data).toBeInstanceOf(ItemTint.CustomModelData) + expect((custom_model_data as ItemTint.CustomModelData).index).toEqual(2) + expect((custom_model_data as ItemTint.CustomModelData).default_color).toEqual(TEST_COLOR) + }) + + it('Constant', () => { + expect(new ItemTint.Constant(TEST_COLOR).getTint(ItemStack.fromString('dummy:dummy'))).toEqual(TEST_COLOR) + }) + + it('Dye', () => { + const dyeTint = new ItemTint.Dye(TEST_COLOR) + expect(dyeTint.getTint(ItemStack.fromString('dummy:dummy'))).toEqual(TEST_COLOR) + expect(dyeTint.getTint(ItemStack.fromString('dummy:dummy[dyed_color=255]'))).toEqual([0, 0, 1]) + }) + + it('Firework', () => { + const fireworkTint = new ItemTint.Firework(TEST_COLOR) + expect(fireworkTint.getTint(ItemStack.fromString('dummy:dummy'))).toEqual(TEST_COLOR) + expect(fireworkTint.getTint(ItemStack.fromString('dummy:dummy[firework_explosion={colors:[255]}]'))).toEqual([0, 0, 1]) + expect(fireworkTint.getTint(ItemStack.fromString('dummy:dummy[firework_explosion={colors:[255, 0]}]'))).toEqual([0, 0, 0.5]) + }) + + it('Potion', () => { + const potionTint = new ItemTint.Potion(TEST_COLOR) + expect(potionTint.getTint(ItemStack.fromString('dummy:dummy'))).toEqual(TEST_COLOR) + expect(potionTint.getTint(ItemStack.fromString('dummy:dummy[potion_contents={custom_color:255}]'))).toEqual([0, 0, 1]) + expect(potionTint.getTint(ItemStack.fromString('dummy:dummy[potion_contents={potion:"water"}]'))).toEqual(Color.intToRgb(-13083194)) + expect(potionTint.getTint(ItemStack.fromString('dummy:dummy[potion_contents={potion:"leaping"}]'))).toEqual(Color.intToRgb(16646020)) + expect(potionTint.getTint(ItemStack.fromString('dummy:dummy[potion_contents={custom_effects:[{id:"jump_boost"}]}]'))).toEqual(Color.intToRgb(16646020)) + }) + + it('MapColor', () => { + const mapColorTint = new ItemTint.MapColor(TEST_COLOR) + expect(mapColorTint.getTint(ItemStack.fromString('dummy:dummy'))).toEqual(TEST_COLOR) + expect(mapColorTint.getTint(ItemStack.fromString('dummy:dummy[map_color=255]'))).toEqual([0, 0, 1]) + }) + + it('CustomModelData', () => { + const customModelDataTint = new ItemTint.CustomModelData(1, TEST_COLOR) + expect(customModelDataTint.getTint(ItemStack.fromString('dummy:dummy'))).toEqual(TEST_COLOR) + expect(customModelDataTint.getTint(ItemStack.fromString('dummy:dummy[custom_model_data={colors:[[0,0,1]]}]'))).toEqual(TEST_COLOR) + expect(customModelDataTint.getTint(ItemStack.fromString('dummy:dummy[custom_model_data={colors:[[0.0,0.0,0.5],[0.0,0.0,1.0]]}]'))).toEqual([0, 0, 1]) + }) + + // not testing grass as its not properly implemented +}) \ No newline at end of file