Skip to content

Commit

Permalink
Item renderer update (#41)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jacobsjo authored Dec 3, 2024
1 parent c38265e commit 8774cb2
Show file tree
Hide file tree
Showing 22 changed files with 2,185 additions and 1,045 deletions.
1 change: 1 addition & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
input {
padding: 4px;
margin-left: 8px;
width: 500px;
}
.invalid {
color: #cb0000;
Expand Down
39 changes: 33 additions & 6 deletions demo/main.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -66,14 +68,16 @@ 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<HTMLImageElement>(res => {
const image = new Image()
image.onload = () => res(image)
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 ===

Expand All @@ -97,6 +101,21 @@ Promise.all([
})
Object.values(blockModels).forEach((m: any) => m.flatten({ getBlockModel: id => blockModels[id] }))


const itemModels: Record<string, ItemModel> = {}
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<string, NbtTag>()
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
Expand All @@ -112,28 +131,36 @@ 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) },
getTextureAtlas() { return textureAtlas.getTextureAtlas() },
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)
Expand Down
9 changes: 4 additions & 5 deletions src/core/Effects.ts
Original file line number Diff line number Diff line change
@@ -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<string, number>([
Expand Down Expand Up @@ -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)
Expand All @@ -162,15 +161,15 @@ 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]
b += amplifier * rgb[2]
total += amplifier
}
if (total === 0) {
return intToRgb(-13083194)
return Color.intToRgb(-13083194)
}
r = r / total
g = g / total
Expand Down
31 changes: 31 additions & 0 deletions src/core/Item.ts
Original file line number Diff line number Diff line change
@@ -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>('item')

constructor(
public components: Map<string, NbtTag> = new Map(),
) {
}

public getComponent<T>(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())
}
}
77 changes: 70 additions & 7 deletions src/core/ItemStack.ts
Original file line number Diff line number Diff line change
@@ -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<Item | undefined>

constructor(
public id: Identifier,
public readonly id: Identifier,
public count: number,
public components: Map<string, NbtTag> = new Map(),
) {}
public readonly components: Map<string, NbtTag> = new Map(),
) {
this.item = Holder.reference(Item.REGISTRY, id, false)
}

public getComponent<T>(key: string | Identifier, reader: (tag: NbtTag) => T) {
public getComponent<T>(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 {
Expand Down Expand Up @@ -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<string, NbtTag>()
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()))
Expand Down
2 changes: 2 additions & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

23 changes: 23 additions & 0 deletions src/nbt/tags/Util.ts
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 2 additions & 0 deletions src/nbt/tags/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ export * from './NbtShort.js'
export * from './NbtString.js'
export * from './NbtTag.js'
export * from './NbtType.js'
export * from './Util.js'

15 changes: 7 additions & 8 deletions src/render/BlockColors.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/render/BlockModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 8774cb2

Please sign in to comment.