特殊物品模型与自定义渲染

特殊物品模型与自定义渲染

并非所有模型都能用基础 JSON 表示。有些包含动态部分,或复用 BlockEntityRenderer 创建的 Model。这时可使用特殊模型类型,指定提交渲染的特征。这些渲染器称为 SpecialModelRenderer,已有类型定义在 SpecialModelRenderers 中。

JSON

// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
    "model": {
        "type": "minecraft:special",

        // The parent model to read the particle texture and display transformation from
        // Points to 'assets/minecraft/models/item/template_skull.json'
        "base": "minecraft:item/template_skull",
        "model": {
            // The special model renderer to use
            "type": "minecraft:head",

            // Properties defined by `SkullSpecialRenderer.Unbaked`
            // The type of the skull block
            "kind": "wither_skeleton",
            // The texture to use when rendering the head
            // Points to 'assets/examplemod/textures/entity/heads/skeleton_override.png'
            "texture": "examplemod:heads/skeleton_override",
            // The animation float used to animate the head model
            "animation": 0.5
        }
    }
}

数据生成

// Assume there is some DeferredItem<Item> EXAMPLE_ITEM
// Within an extended ModelProvider
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
    itemModels.itemModelOutput.accept(
        EXAMPLE_ITEM.get(),
        new SpecialModelWrapper.Unbaked(
            // The parent model to read the particle texture and display transformation from
            // Points to 'assets/minecraft/models/item/template_skull.json'
            Identifier.fromNamespaceAndPath("minecraft", "item/template_skull"),
            // The special model renderer to use
            new SkullSpecialRenderer.Unbaked(
                // The type of the skull block
                SkullBlock.Types.WITHER_SKELETON,
                // The texture to use when rendering the head
                // Points to 'assets/examplemod/textures/entity/heads/skeleton_override.png'
                Optional.of(
                    Identifier.fromNamespaceAndPath("examplemod", "heads/skeleton_override")
                ),
                // The animation float used to animate the head model
                0.5f
            )
        )
    );
}

自定义特殊模型渲染器分三部分:负责提交渲染特征的 SpecialModelRenderer 实例、负责 JSON 读写的 SpecialModelRenderer.Unbaked 实例,以及将其用于物品、必要时用于方块的注册步骤。

首先是渲染器本体,工作方式与方块实体、实体等其他渲染器类似。构造时传入提交过程使用的静态数据,例如 Model 子类、纹理 SpriteId 等。需要关注两个方法:extractArgument 从 ItemStack 中只提取必要数据,限制 submit 可见的信息量。

提示 不确定需要哪些数据时,可以直接返回当前 ItemStack。如果完全不需要堆叠数据,可使用已实现此方法的 NoDataSpecialModelRenderer。

submit 接收提取的数据、姿态栈、用于提交特征的收集器、打包光照、叠加纹理、是否具有闪光(例如附魔),以及轮廓颜色。全部特征提交都应在此方法中完成。

public record ExampleSpecialRenderer(SpriteGetter spriteGetter, Model.Simple model, SpriteId sprite) implements SpecialModelRenderer<Boolean> {

    @Nullable
    public Boolean extractArgument(ItemStack stack) {
        // Extract the data to be used
        return stack.isBarVisible();
    }

    // Submit the features of the model
    @Override
    public void submit(Boolean argument, PoseStack poseStack, SubmitNodeCollector collector, int lightCoords, int overlayCoords, boolean hasFoil, int outlineColor) {
        collector.submitModel(
            this.model, Unit.INSTANCE,
            poseStack, this.sprite.renderType(barVisible ? RenderType::entityCutout : RenderType::entitySolid),
            lightCoords, overlayCoords, -1, this.spriteGetter.get(this.sprite), outlineColor, null
        );
    }
}

接着是 SpecialModelRenderer.Unbaked,保存可从文件读取、用于确定渲染器构造参数的数据。它有两个方法:bake 构建实际渲染器实例,type 提供文件编码和解码使用的 MapCodec。

public record ExampleSpecialRenderer(SpriteGetter spriteGetter, Model.Simple model, SpriteId sprite) implements SpecialModelRenderer<Boolean> {

    // ...

    public record Unbaked(Identifier texture) implements SpecialModelRenderer.Unbaked {

        public static final MapCodec<ExampleSpecialRenderer.Unbaked> MAP_CODEC = Identifier.CODEC.fieldOf("texture")
            .xmap(ExampleSpecialRenderer.Unbaked::new, ExampleSpecialRenderer.Unbaked::texture);

        @Override
        public MapCodec<ExampleSpecialRenderer.Unbaked> type() {
            return MAP_CODEC;
        }

        @Override
        public SpecialModelRenderer<?> bake(SpecialModelRenderer.BakingContext ctx) {
            // Resolve resource location to absolute path
            Identifier textureLoc = this.texture.withPath(path -> "textures/entity/" + path + ".png");

            // Get the model and the sprites to render
            return new ExampleSpecialRenderer(ctx.sprites(), ...);
        }
    }
}

最后注册对象。客户端物品通过 Mod 事件总线上的 RegisterSpecialModelRendererEvent 注册。如果渲染器也应作为 BlockEntityRenderer 的一部分,用于末影人搬运方块等类似物品的渲染上下文,还应在同一总线的 RegisterBlockModelsEvent 中注册方块对应的 Unbaked 版本。

// In some event handler class
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerSpecialRenderers(RegisterSpecialModelRendererEvent event) {
    event.register(
        // The name to reference as the type
        Identifier.fromNamespaceAndPath("examplemod", "example_special"),
        // The map codec
        ExampleSpecialRenderer.Unbaked.MAP_CODEC
    );
}

// For rendering a block in an item-like context
// Assume some DeferredBlock<ExampleBlock> EXAMPLE_BLOCK
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerSpecialBlockRenderers(RegisterBlockModelsEvent event) {
    event.register(
        // The unbaked instance to use
        new SpecialBlockModelWrapper.Unbaked(
            new ExampleSpecialRenderer.Unbaked(Identifier.fromNamespaceAndPath("examplemod", "entity/example_special")),
            Optional.empty()
        ),
        // The block to render for
        EXAMPLE_BLOCK.get()
    );
}

JSON

// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
    "model": {
        "type": "minecraft:special",

        // The parent model to read the particle texture and display transformation from
        // Points to 'assets/minecraft/models/item/template_skull.json'
        "base": "minecraft:item/template_skull",
        "model": {
            // The special model renderer to use
            "type": "examplemod:example_special",

            // Properties defined by `ExampleSpecialRenderer.Unbaked`
            // The texture to use
            // Points to 'assets/examplemod/textures/entity/example/example_texture.png'
            "texture": "examplemod:example/example_texture"
        }
    }
}

数据生成

// Assume there is some DeferredItem<Item> EXAMPLE_ITEM
// Within an extended ModelProvider
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
    itemModels.itemModelOutput.accept(
        EXAMPLE_ITEM.get(),
        new SpecialModelWrapper.Unbaked(
            // The parent model to read the particle texture and display transformation from
            // Points to 'assets/minecraft/models/item/template_skull.json'
            Identifier.fromNamespaceAndPath("minecraft", "item/template_skull"),
            // The special model renderer to use
            new ExampleSpecialRenderer.Unbaked(
                // The texture to use
                // Points to 'assets/examplemod/textures/entity/example/example_texture.png'
                Identifier.fromNamespaceAndPath("examplemod", "example/example_texture")
            )
        )
    );
}

动态流体容器

NeoForge 添加了动态流体容器物品模型,可在运行时根据内部流体重新设置纹理。

提示 要让流体颜色应用到纹理,物品必须具有 Capabilities.FluidHandler.ITEM。若物品既不是 BucketItem 本身,也不是其子类,就需要注册该能力。

JSON

// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
    "model": {
        "type": "neoforge:fluid_container",

        // The textures used to construct the container
        // These are in reference to the block atlas, so they are relative to the `textures` directory
        "textures": {
            // Sets the model particle sprite
            // If not set, uses the first texture that is not null:
            // - Fluid still texture
            // - Container base texture
            // - Container cover texture, if not used as a mask
            // Points to 'assets/minecraft/textures/item/bucket.png'
            "particle": "minecraft:item/bucket",
            // Sets the texture to use on the first layer, generally the container of the fluid
            // If not set, the layer will not be added
            // Points to 'assets/minecraft/textures/item/bucket.png'
            "base": "minecraft:item/bucket",
            // Sets the texture to use as the mask for the still fluid texture
            // Areas where the fluid is seen should be pure white
            // If not set or the fluid is empty, then the layer is not drawn
            // Points to 'assets/neoforge/textures/item/mask/bucket_fluid.png'
            "fluid": "neoforge:item/mask/bucket_fluid",
            // Sets the texture to use as either
            // - The overlay texture when 'cover_is_mask' is false
            // - The mask to apply to the base texture (should be pure white to see) when 'cover_is_mask' is true
            // If not set or no base texture is set when 'cover_is_mask' is true, then the layer is not drawn
            // Points to 'assets/neoforge/textures/item/mask/bucket_fluid_cover.png'
            "cover": "neoforge:item/mask/bucket_fluid_cover",
        },

        // When true, rotates the model 180 degrees for fluids whose density is negative or zero
        // Defaults to false
        "flip_gas": true,
        // When true, uses the cover texture as a mask for the base texture
        // Defaults to true
        "cover_is_mask": true,
        // When true, sets the lightmap of the fluid texture layer to its max value
        // for fluids whose light level is greater than zero
        // Defaults to true
        "apply_fluid_luminosity": false
    }
}

数据生成

// Assume there is some DeferredItem<ExampleFluidContainerItem> EXAMPLE_ITEM
// Within an extended ModelProvider
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
    itemModels.itemModelOutput.accept(
        EXAMPLE_ITEM.get(),
        new DynamicFluidContainerModel.Unbaked(
            // The textures used to construct the container
            // These are in reference to the block atlas, so they are relative to the `textures` directory
            new DynamicFluidContainerModel.Textures(
                // Sets the model particle sprite
                // If not set, uses the first texture that is not null:
                // - Fluid still texture
                // - Container base texture
                // - Container cover texture, if not used as a mask
                // Points to 'assets/minecraft/textures/item/bucket.png'
                Optional.of(Identifier.withDefaultNamespace("item/bucket")),
                // Sets the texture to use on the first layer, generally the container of the fluid
                // If not set, the layer will not be added
                // Points to 'assets/minecraft/textures/item/bucket.png'
                Optional.of(Identifier.withDefaultNamespace("item/bucket")),
                // Sets the texture to use as the mask for the still fluid texture
                // Areas where the fluid is seen should be pure white
                // If not set or the fluid is empty, then the layer is not rendered
                // Points to 'assets/neoforge/textures/item/mask/bucket_fluid.png'
                Optional.of(Identifier.fromNamespaceAndPath("neoforge", "item/mask/bucket_fluid")),
                // Sets the texture to use as either
                // - The overlay texture when 'cover_is_mask' is false
                // - The mask to apply to the base texture (should be pure white to see) when 'cover_is_mask' is true
                // If not set or no base texture is set when 'cover_is_mask' is true, then the layer is not rendered
                // Points to 'assets/neoforge/textures/item/mask/bucket_fluid_cover.png'
                Optional.of(Identifier.fromNamespaceAndPath("neoforge", "item/mask/bucket_fluid_cover"))
            ),
            // When true, rotates the model 180 degrees
            // Defaults to false
            true,
            // When true, uses the cover texture as a mask for the base texture
            // Defaults to true
            true,
            // When true, sets the lightmap of the fluid texture layer to its max value
            // Defaults to true
            false
        )
    );
}

手动提交物品渲染

若需要在 BlockEntityRenderer 或 EntityRenderer 等位置提交物品渲染特征,可分三步:先由渲染器创建 ItemStackRenderState 保存堆叠状态,再通过 ItemModelResolver 的方法将其更新为当前物品,最后调用 ItemStackRenderState#submit 提交。

渲染状态保存绘制数据。每个“模型”都有自己的 ItemStackRenderState.LayerRenderState,包含待渲染四边形、渲染类型、闪光状态、着色信息、动画标志、边界及特殊渲染器。用 newLayer 创建层,用 clear 清理渲染状态。如果层数固定,可调用 ensureCapacity 保证有足够层数。

提示 界面使用子类 TrackingItemStackRenderState,保存模型身份信息,以跨帧缓存已渲染状态。

ItemModelResolver 负责更新状态。生物持有的物品使用 updateForLiving,其他实体持有的物品使用 updateForNonLiving,其余情况使用 updateForTopItem。这些方法接收渲染状态、堆叠及显示上下文,其他参数提供手、世界、物品所有者及种子值。每个方法先调用 ItemStackRenderState#clear,再根据 DataComponents#ITEM_MODEL 获取模型并调用其 update。如果不在方块实体或实体渲染器等上下文中,可始终通过 Minecraft#getItemModelResolver 获取解析器。

自定义物品模型定义

自定义 ItemModel 分三部分:更新渲染状态的实例、负责 JSON 读写的 ItemModel.Unbaked,以及注册模型。

警告 先确认前面介绍的现有系统无法创建所需模型。大多数情况下无需自定义 ItemModel。

ItemModel 负责更新 ItemStackRenderState,让物品正确绘制。构造时传入提交过程使用的静态数据,例如四边形列表、属性信息等。唯一方法 update 接收渲染状态、堆叠、解析器、显示上下文、世界、物品所有者和种子值。只应修改 ItemStackRenderState,其余参数均视为只读。

public record ExampleModelWrapper(QuadCollection quads, List<ItemTintSource> tints, ModelRenderProperties properties, Matrix4fc transformation) implements ItemModel {

    // Update the render state
    @Override
    public void update(ItemStackRenderState state, ItemStack stack, ItemModelResolver resolver, ItemDisplayContext displayContext, @Nullable ClientLevel level, @Nullable ItemOwner owner, int seed) {
        // Set the identity used by the model
        state.appendModelIdentityElement(this);

        // Create a new layer
        ItemStackRenderState.LayerRenderState layerState = state.newLayer();

        // Sets the foil to use
        if (stack.hasFoil()) {
            layerState.setFoilType(ItemStackRenderState.FoilType.STANDARD);
            state.appendModelIdentityElement(ItemStackRenderState.FoilType.STANDARD);
        }

        // Apply the tint sources
        int tintSize = this.tints.size();
        int[] tintLayers = layerState.prepareTintLayers(tintSize);

        for (int idx = 0; idx < tintSize; idx++) {
            int tintColor = this.tints.get(idx).calculate(stack, level, owner.asLivingEntity());
            tintLayers[idx] = tintColor;
            state.appendModelIdentityElement(tintColor);
        }

        // Computes the bounds of the model
        // Used for GUI render bounds (when oversized) and item entity bobbing
        layerState.setExtents(CuboidItemModelWrapper.computeExtents(this.quads.getAll()));

        // Set the local transforms to apply for the client item
        layerState.setLocalTransform(this.transformation);

        // Set other common model properties
        this.properties.applyToLayer(layerState, displayContext);

        // Adds the quads to submit
        layerState.prepareQuadList().addAll(this.quads.getAll());

        // Set animated if it has the associated material flag
        if (this.quads.hasMaterialFlag(BakedQuad.FLAG_ANIMATED)) {
            layerState.setAnimated();
        }
    }
}

接着创建 ItemModel.Unbaked,保存从文件读取、用于构造物品模型的数据。它有两个方法:bake 构造 ItemModel 实例,type 提供文件编码和解码的 MapCodec。

public record ExampleModelWrapper(QuadCollection quads, List<ItemTintSource> tints, ModelRenderProperties properties, Matrix4fc transformation) implements ItemModel {

    // ...

     public record Unbaked(Identifier model, List<ItemTintSource> tints, Optional<Transformation> transformation) implements ItemModel.Unbaked {
        // The map codec to register
        public static final MapCodec<ExampleModelWrapper.Unbaked> MAP_CODEC = RecordCodecBuilder.mapCodec(instance ->
            instance.group(
                Identifier.CODEC.fieldOf("model").forGetter(ExampleModelWrapper.Unbaked::model),
                ItemTintSources.CODEC.listOf().optionalFieldOf("tints", List.of()).forGetter(ExampleModelWrapper.Unbaked::tints)
                Transformation.EXTENDED_CODEC.optionalFieldOf("transformation").forGetter(ExampleModelWrapper.Unbaked::transformation)
            )
            .apply(instance, ExampleModelWrapper.Unbaked::new)
        );

        @Override
        public void resolveDependencies(ResolvableModel.Resolver resolver) {
            // Mark all dependencies for this item model
            resolver.markDependency(this.model);
        }

        @Override
        public ItemModel bake(ItemModel.BakingContext context, Matrix4fc parentTransform) {
            // Get the baked quads and return
            ModelBaker baker = context.blockModelBaker();
            ResolvedModel resolvedModel = baker.getModel(this.model);
            TextureSlots slots = resolvedModel.getTopTextureSlots();

            return new ExampleModelWrapper(
                resolvedModel.bakeTopGeometry(slots, baker, BlockModelRotation.IDENTITY),
                this.tints,
                ModelRenderProperties.fromResolvedModel(baker, resolvedModel, slots),
                Transformation.compose(parentTransform, this.transformation)
            );
        }

        @Override
        public MapCodec<ExampleModelWrapper.Unbaked> type() {
            return MAP_CODEC;
        }
    }
}

随后通过 Mod 事件总线上的 RegisterItemModelsEvent 注册 map codec。

// In some event handler class
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerItemModels(RegisterItemModelsEvent event) {
    event.register(
        // The name to reference as the type
        Identifier.fromNamespaceAndPath("examplemod", "render_type"),
        // The map codec
        ExampleModelWrapper.Unbaked.MAP_CODEC
    )
}

最后,即可在 JSON 或数据生成中使用该模型。

JSON

// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
    "model": {
        "type": "examplemod:render_type",
        // Points to 'assets/examplemod/models/item/example_item.json'
        "model": "examplemod:item/example_item",
        // Any tints to apply to the model texture
        "tints": []
    }
}

数据生成

// Assume there is some DeferredItem<Item> EXAMPLE_ITEM
// Within an extended ModelProvider
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
    itemModels.itemModelOutput.accept(
        EXAMPLE_ITEM.get(),
        new ExampleModelWrapper.Unbaked(
            // Points to 'assets/examplemod/models/item/example_item.json'
            ModelLocationUtils.getModelLocation(EXAMPLE_ITEM.get()),
            // Any tints to apply to the model texture
            List.of(),
            // The transformations to apply after the model JSON transforms
            Optional.empty()
        )
    );
}

来源: NeoForged/Documentation(26.1,原文第 1135–1641 行)。中文翻译依据提交 4a680cdd4d15。