Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Item renderer update #41

Merged
merged 27 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
00eded2
implement default components and component removal
jacobsjo Nov 11, 2024
85868ab
start update item renderer
jacobsjo Nov 12, 2024
38ca03a
implement range dispatch
jacobsjo Nov 12, 2024
f7314fc
refactor Color to its own namespace, add fromJson
jacobsjo Nov 12, 2024
5b0eb45
implement item tints
jacobsjo Nov 12, 2024
b005e8a
fix imports
jacobsjo Nov 12, 2024
81cb1fa
update demo and fixes
jacobsjo Nov 12, 2024
de1a8ba
fix circular dependencies
jacobsjo Nov 12, 2024
6180c2c
add item component string parser
jacobsjo Nov 13, 2024
b2ecc70
don't error on missing item_model component & map color fix
jacobsjo Nov 13, 2024
06d0dca
fix circular dependencies
jacobsjo Nov 13, 2024
a6c3f4c
start special renderer
jacobsjo Nov 13, 2024
e89a37e
update from 24w46a
jacobsjo Nov 13, 2024
52a3252
fix imports
jacobsjo Nov 13, 2024
0350a6e
store id in itemstack
jacobsjo Nov 13, 2024
bbd3983
implement most special models
jacobsjo Nov 14, 2024
171e3e3
improve item registry
jacobsjo Nov 14, 2024
cab2ccf
implement bundle/selected_item
jacobsjo Nov 14, 2024
290af74
implement bundle/fullness
jacobsjo Nov 14, 2024
4cb1fbd
remove local_time, fix chest special renderer
jacobsjo Nov 14, 2024
0bce53b
minor fixes
jacobsjo Nov 14, 2024
c1b281a
add tests
jacobsjo Nov 14, 2024
f6dc6b0
fix defaults of properties and tints
jacobsjo Nov 16, 2024
9649fea
add more tests
jacobsjo Nov 19, 2024
623599e
undo unnecessary formatting changes
jacobsjo Nov 19, 2024
7f0533d
update item_definition url
jacobsjo Nov 19, 2024
eb6229d
add changes from 1.21.4-pre1
jacobsjo Nov 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions src/core/ItemStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,43 @@ import type { NbtTag } from '../nbt/index.js'
import { NbtCompound, NbtInt, NbtString } from '../nbt/index.js'
import { Identifier } from './Identifier.js'

export interface DefaultItemComponentProvider {
hasDefaultComponent(item: Identifier, component: Identifier): boolean
getDefaultComponent<T>(item: Identifier, component: Identifier): T | undefined
}


export class ItemStack {
constructor(
public id: Identifier,
public count: number,
public components: Map<string, NbtTag> = new Map(),
public components: Map<string, NbtTag | '!'> = new Map(),
public defaultProvider?: DefaultItemComponentProvider
jacobsjo marked this conversation as resolved.
Show resolved Hide resolved
) {}

public getComponent<T>(key: string | Identifier, reader: (tag: NbtTag) => T) {
public getComponent<T>(key: string | Identifier, reader: (tag: NbtTag) => T): T | undefined {
if (typeof key === 'string') {
key = Identifier.parse(key)
}
const value = this.components.get(key.toString())
if (value === '!') {
return undefined
}
if (value) {
return reader(value)
}
return undefined
return this.defaultProvider?.getDefaultComponent(this.id, key)
}

public hasComponent(key: string | Identifier) {
public hasComponent(key: string | Identifier): boolean {
if (typeof key === 'string') {
key = Identifier.parse(key)
}
return this.components.has(key.toString())
const value = this.components.get(key.toString())
if (value === '!') {
return false
}
return value !== undefined || (this.defaultProvider?.hasDefaultComponent(this.id, key) ?? false)
}

public clone(): ItemStack {
Expand Down Expand Up @@ -84,16 +98,22 @@ export class ItemStack {
result.set('count', new NbtInt(this.count))
}
if (this.components.size > 0) {
result.set('components', new NbtCompound(this.components))
result.set('components', new NbtCompound(new Map(Array.from(this.components).map(e => e[1] === '!' ? [`!${e[0]}`, new NbtCompound()] : e as [string, NbtTag]))))
}
return result
}

public static fromNbt(nbt: NbtCompound) {
const id = Identifier.parse(nbt.getString('id'))
const count = nbt.hasNumber('count') ? nbt.getNumber('count') : 1
const components = new Map(Object.entries(
nbt.getCompound('components').map((key, value) => [Identifier.parse(key).toString(), value])
const components: Map<string, NbtTag | '!'> = new Map(Object.entries(
nbt.getCompound('components').map((key, value): [string, NbtTag | '!'] => {
if (key.startsWith('!')){
return [Identifier.parse(key.substring(1)).toString(), '!']
} else {
return [Identifier.parse(key).toString(), value]
}
})
))
return new ItemStack(id, count, components)
}
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
197 changes: 197 additions & 0 deletions src/render/ItemModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { BlockModelProvider, Cull, DefaultItemComponentProvider, Identifier, ItemRenderingContext, ItemStack, Json, Mesh, TextureAtlasProvider } from "../index.js"


export interface ItemModelProvider {
getItemModel(id: Identifier): ItemModel | null
}

interface ItemModelResources extends BlockModelProvider, TextureAtlasProvider, DefaultItemComponentProvider {}

export abstract class ItemModel {

public abstract getMesh(item: ItemStack, resources: ItemModelResources, 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 'model': return new ModelItemModel(
Identifier.parse(Json.readString(root.model) ?? ''),
// TODO model tints
)
case 'composite': return new CompositeItemModel(
Json.readArray(root.models, ItemModel.fromJson) ?? []
)
case 'condition': return new ConditionItemModel(
ConditionItemModel.propertyFromJson(root),
ItemModel.fromJson(root.on_true),
ItemModel.fromJson(root.on_false)
)
case 'select': return new SelectItemModel(
SelectItemModel.propertyFromJson(root),
new Map(Json.readArray(root.cases, caseObj => {
const caseRoot = Json.readObject(caseObj) ?? {}
return [Json.readString(caseRoot.when) ?? '', ItemModel.fromJson(caseRoot.model)]
})),
ItemModel.fromJson(root.fallback)
)
case 'range_dispatch':
case 'special':
case 'bundle/selected_item':
default:
throw new Error(`Invalid item model type ${type}`)
}
}

class ModelItemModel extends ItemModel {
constructor(
private modelId: Identifier
) {
super()
}

public getMesh(item: ItemStack, resources: ItemModelResources, 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 = undefined // TODO model tints
const mesh = model.getMesh(resources, Cull.none())
mesh.transform(model.getDisplayTransform(context.display_context ?? 'gui'))
return mesh
}
}

class CompositeItemModel extends ItemModel {
constructor(
private models: ItemModel[]
) {
super()
}

public getMesh(item: ItemStack, resources: ItemModelResources, context: ItemRenderingContext): Mesh {
const mesh = new Mesh()
this.models.forEach(model => mesh.merge(model.getMesh(item, resources, context)))
return mesh
}
}

class ConditionItemModel extends ItemModel {
constructor(
private property: (item: ItemStack, context: ItemRenderingContext) => boolean,
private onTrue: ItemModel,
private onFalse: ItemModel
) {
super()
}

public getMesh(item: ItemStack, resources: ItemModelResources, 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 'using_item':
case 'fishing_rod/cast':
case 'bundle/has_selected_item':
case 'xmas':
case 'selected':
case 'carried':
case 'shift_down':
return (item, context) => context[property] ?? false
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) ?? '')
return (item, context) => item.hasComponent(componentId)
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').get(index)?.getAsNumber()
return flag !== undefined && flag !== 0
}) ?? false
default:
throw new Error(`Invalid condition property ${property}`)
}
}
}

class SelectItemModel extends ItemModel {
constructor(
private property: (item: ItemStack, context: ItemRenderingContext) => string,
private cases: Map<string, ItemModel>,
private fallback?: ItemModel
) {
super()
}

public getMesh(item: ItemStack, resources: ItemModelResources, context: ItemRenderingContext): Mesh {
const value = this.property(item, context)
return (this.cases.get(value) ?? this.fallback)?.getMesh(item, resources, context) ?? MISSING_MESH
}

static propertyFromJson(root: {[x: string]: unknown}): (item: ItemStack, context: ItemRenderingContext) => string{
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 'charge_type':
const FIREWORK = Identifier.create('firework_rocket')
return (item, context) => item.getComponent('charged_projectiles', tag => {
if (!tag.isList() || tag.length === 0) {
return 'none'
}
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()
}) ?? '' // TODO: verify default value
case 'block_state':
const block_state_property = Json.readString('block_state_property') ?? ''
return (item, context) => item.getComponent('block_state', tag => {
if (!tag.isCompound()) {
return undefined
}
return tag.getString(block_state_property)
}) ?? '' // TODO: verify default value
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('strings').getString(index)
}) ?? ''
default:
throw new Error(`Invalid select property ${property}`)

}
}
}
}
50 changes: 37 additions & 13 deletions src/render/ItemRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,68 @@
import { mat4 } from 'gl-matrix'
import { DefaultItemComponentProvider, ItemStack } from '../core/ItemStack.js'
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 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,
}

interface ItemRendererResources extends BlockModelProvider, TextureAtlasProvider {}
interface ItemRendererResources extends BlockModelProvider, TextureAtlasProvider, ItemModelProvider, DefaultItemComponentProvider {}

export type ItemRenderingContext = {
'display_context'?: Display

'using_item'?: boolean,
'fishing_rod/cast'?: boolean,
'bundle/has_selected_item'?: boolean,
'xmas'?: boolean,
'selected'?: boolean,
'carried'?: boolean,
'shift_down'?: boolean,

'main_hand'?: string
}

export class ItemRenderer extends Renderer {
private item: ItemStack
private mesh: Mesh
private readonly tint: Color | ((index: number) => Color) | undefined
private readonly atlasTexture: WebGLTexture


constructor(
gl: WebGLRenderingContext,
item: Identifier | ItemStack,
private readonly resources: ItemRendererResources,
options?: ModelRendererOptions,
) {
super(gl)
this.item = item instanceof ItemStack ? item : new ItemStack(item, 1)
this.item = item instanceof ItemStack ? item : new ItemStack(item, 1, new Map(), this.resources)
this.mesh = this.getItemMesh()
this.tint = options?.tint
this.atlasTexture = this.createAtlasTexture(this.resources.getTextureAtlas())
}

public setItem(item: Identifier | ItemStack) {
this.item = item instanceof ItemStack ? item : new ItemStack(item, 1)
this.item = item instanceof ItemStack ? item : new ItemStack(item, 1, new Map(), this.resources)
this.mesh = this.getItemMesh()
}

private getItemMesh() {
private getItemMesh(context: ItemRenderingContext = {}) {
const itemModelId = this.item.getComponent('item_model', tag => tag.getAsString())
if (itemModelId === undefined){
throw new Error(`Item ${this.item.toString()} does not have item_model component`)
}

const itemModel = this.resources.getItemModel(Identifier.parse(itemModelId))
if (!itemModel) {
throw new Error(`Item model ${itemModelId} does not exist (defined by item ${this.item.toString()})`)
}

return itemModel.getMesh(this.item, this.resources, context)

/*
const model = this.resources.getBlockModel(this.item.id.withPrefix('item/'))
if (!model) {
throw new Error(`Item model for ${this.item.toString()} does not exist`)
Expand All @@ -55,6 +78,7 @@ export class ItemRenderer extends Renderer {
mesh.computeNormals()
mesh.rebuild(this.gl, { pos: true, color: true, texture: true, normal: true })
return mesh
*/
}

protected override getPerspective() {
Expand Down