Skip to main content
Version: 1.21.4

实体渲染器(Entity Renderers)

实体渲染器(Entity Renderers)用于定义某个实体的渲染行为。它们只存在于 逻辑和物理客户端

实体渲染采用了一种称为实体渲染状态(entity render states)的机制。简单来说,实体渲染状态就是一个对象,内部保存了渲染器所需的所有数值。每当实体被渲染时,渲染状态就会被更新,然后 #render 方法会使用这个渲染状态来渲染实体。这种设计很可能是为了未来能够适配延迟渲染(deferred rendering)系统:即先收集渲染信息(这一步甚至可以多线程处理),之后再在合适的时机进行实际渲染。

创建实体渲染器(Creating an Entity Renderer)

最简单的实体渲染器,就是直接继承自 EntityRenderer

// 父类中的泛型类型参数应设置为你想要渲染的实体类型。
// 如果你想让它能渲染任意实体,可以像这里一样使用 Entity。
// 同理,EntityRenderState 也应选择适合你需求的类型,后文会详细介绍。
public class MyEntityRenderer extends EntityRenderer<Entity, EntityRenderState> {
// 构造函数中直接调用父类构造函数即可。
public MyEntityRenderer(EntityRendererProvider.Context context) {
super(context);
}

// 告诉渲染引擎如何创建一个新的实体渲染状态。
@Override
public EntityRenderState createRenderState() {
return new EntityRenderState();
}

// 通过从传入的实体提取需要的数值,更新传入的渲染状态对象。
// Entity 和 EntityRenderState 都可以替换为更具体的类型,
// 具体取决于你在父类中指定的泛型参数。
@Override
public void extractRenderState(Entity entity, EntityRenderState state, float partialTick) {
super.extractRenderState(entity, state, partialTick);
// 在这里提取并存储你需要的额外数据到 state 中。
}

// 实际进行实体的渲染。第一个参数类型对应渲染状态的泛型类型。
// 如果需要渲染牵引绳和实体名字,调用 super 即可自动处理。
@Override
public void render(EntityRenderState state, PoseStack poseStack, MultiBufferSource bufferSource, int packedLight) {
super.render(state, poseStack, bufferSource, packedLight);
// 在这里实现你自己的渲染逻辑
}
}

有了自定义的实体渲染器之后,还需要将其注册,并关联到对应的实体类型上。这一步需要在 EntityRenderersEvent.RegisterRenderers 事件中完成,示例如下:

@SubscribeEvent
public static void registerEntityRenderers(EntityRenderersEvent.RegisterRenderers event) {
event.registerEntityRenderer(MY_ENTITY_TYPE.get(), MyEntityRenderer::new);
}

实体渲染状态(Entity Render States)

如前所述,实体渲染状态(entity render states)用于将渲染所需的数值与实体本身的数据分离。它们本质上只是可变的数据存储对象,没有更多复杂之处。因此,扩展它们非常简单:

public class MyEntityRenderState extends EntityRenderState {
public ItemStack stackInHand;
}

就是这么简单。继承该类,添加你的字段,然后将 EntityRenderer 中的泛型类型改为你的类,就可以使用了。现在唯一剩下的事情,就是在 EntityRenderer#extractRenderState 方法中更新 stackInHand 字段,具体如上文所述。

渲染状态修改(Render State Modifications)

除了可以自定义实体渲染状态(entity render states)之外,NeoForge 还引入了一套允许修改已有渲染状态的机制。

为此,你可以创建一个 ContextKey<T>(其中 T 是你想要修改的数据类型),并将其存储为静态字段。然后,你可以在 RegisterRenderStateModifiersEvent 的事件处理器中像下面这样使用它:

public static final ContextKey<String> EXAMPLE_CONTEXT = new ContextKey<>(
// 你的 context key 的 id。用于在内部区分不同的 key。
ResourceLocation.fromNamespaceAndPath("examplemod", "example_context"));

@SubscribeEvent
public static void registerRenderStateModifiers(RegisterRenderStateModifiersEvent event) {
event.registerEntityModifier(
// 渲染器的 TypeToken。必须以匿名类的方式实例化(即后面带有 {}),
// 并且要明确泛型参数,这是由于 Java 泛型的特殊性。
new TypeToken<LivingEntityRenderer<LivingEntity, LivingEntityRenderState, ?>>(){},
// 修饰器本身。它是一个 BiConsumer,参数分别是实体和实体渲染状态。
// 具体的泛型类型会根据渲染器类中的泛型自动推断。
(entity, state) -> state.setRenderData(EXAMPLE_CONTEXT, "Hello World!");
);

// 上述方法的重载版本,接受一个 Class<?> 参数。
// 仅适用于没有泛型参数的渲染器,例如 PlayerRenderer。
event.registerEntityModifier(
PlayerRenderer.class,
(entity, state) -> state.setRenderData(EXAMPLE_CONTEXT, "Hello World!");
);
}
tip

通过在 EntityRenderState#setRenderData 的第二个参数传入 null,可以清除该值。例如:

state.setRenderData(EXAMPLE_CONTEXT, null);

之后,你可以在需要时通过 EntityRenderState#getRenderData 方法获取这些数据。此外,还提供了 #getRenderDataOrThrow#getRenderDataOrDefault 等辅助方法。

层级结构(Hierarchy)

和实体本身类似,实体渲染器(entity renderers)也有自己的类层级结构,虽然没有实体那样复杂。这个层级结构中最重要的类之间的关系如下(红色类为 abstract 抽象类,蓝色类为具体实现类):

  • EntityRenderer:抽象基类。许多渲染器(尤其是几乎所有非生物实体的渲染器)都直接继承自这个类。
  • ArrowRendererAbstractBoatRendererAbstractMinecartRenderer:这些类主要是为了方便使用,作为更具体渲染器的父类。
  • LivingEntityRenderer:用于生物实体渲染器的抽象基类。其直接子类包括 ArmorStandRendererPlayerRenderer
  • ArmorStandRenderer:用于盔甲架渲染,含义直观。
  • PlayerRenderer:用于渲染玩家。需要注意的是,与大多数其他渲染器不同,这个类的多个实例可能会同时用于不同的上下文。
  • MobRenderer:用于 Mob 渲染器的抽象基类。许多渲染器会直接继承自它。
  • AgeableRenderer:用于具有子代变体(child variants)的 Mob 的渲染器的抽象基类。这包括拥有子代变体的怪物,例如猪灵兽(hoglin)。
  • HumanoidMobRenderer:人形实体渲染器的抽象基类。例如僵尸和骷髅就使用了它。

与各种实体类的继承结构类似,应根据你的具体需求选择合适的基类。需要注意的是,这些类的泛型参数通常有对应的类型限制;例如,LivingEntityRenderer 的泛型限定为 LivingEntityLivingEntityRenderState

实体模型、层定义与渲染层(Entity Models, Layer Definitions and Render Layers)

对于更复杂的实体渲染器,尤其是 LivingEntityRenderer,会采用分层系统(layer system),每一层由一个 RenderLayer 表示。一个渲染器可以使用多个 RenderLayer,并且可以根据需要决定在何时渲染哪些层。例如,鞘翅(elytra)就使用了独立的渲染层,这一层会独立于穿戴它的 LivingEntity 进行渲染。同样,玩家披风(cape)也是一个独立的渲染层。

RenderLayer 定义了一个 #render 方法,顾名思义——负责渲染该层。和其他大多数渲染方法一样,你可以在这里渲染你想要的任何内容。但最常见的用法是渲染一个独立的模型(model),比如盔甲或类似的装备部件。

为此,我们首先需要一个可以被渲染的模型。我们可以通过 EntityModel 类来实现。EntityModel 本质上是一个立方体(cube)及其关联纹理(texture)的列表,供渲染器使用。通常会在实体渲染器的构造函数首次创建时静态创建这些模型。

note

由于我们现在操作的是 LivingEntityRenderer,所以下面的代码会假设 MyEntity extends LivingEntity 并且 MyEntityRenderState extends LivingEntityRenderState,以符合泛型类型的要求。

创建实体模型类和层定义(Layer Definition)

让我们首先创建一个实体模型类:

public class MyEntityModel extends EntityModel<MyEntityRenderState> {}

需要注意的是,在上面的例子中,我们是直接继承了 EntityModel;但根据你的实际需求,可能更适合继承其某个子类。在创建新模型时,建议先参考一下与自己需求最接近的现有模型,然后再进行修改和扩展。

接下来,我们要创建一个 LayerDefinitionLayerDefinition 本质上是一个立方体(cube)列表,之后可以将其烘焙(bake)成一个 EntityModel。定义一个 LayerDefinition 的方式大致如下:

public class MyEntityModel extends EntityModel<MyEntityRenderState> {
// 一个静态方法,用于创建我们的图层定义。createBodyLayer() 是大多数原版模型使用的方法名。
// 如果你有多个层,就需要多个这样的静态方法。
public static LayerDefinition createBodyLayer() {
// 创建我们的网格(mesh)。
MeshDefinition mesh = new MeshDefinition();
// 网格最初只包含根节点,根节点是不可见的(尺寸为 0x0x0)。
PartDefinition root = mesh.getRoot();
// 我们添加一个头部(head)部件。
PartDefinition head = root.addOrReplaceChild(
// 部件的名称。
"head",
// 要添加的 CubeListBuilder。
CubeListBuilder.create()
// 纹理中使用的 UV 坐标。纹理的绑定方式将在下文介绍。
// 在本例中,U=10,V=20 作为起点。
.texOffs(10, 20)
// 添加一个立方体。可多次调用以添加多个立方体。
// 这是相对于父部件的位置。对于根部件,是相对于实体的位置。
// 注意,y 轴是反向的,也就是说“上”是减,“下”是加。
.addBox(
// 立方体左上后角相对于父对象的位置。
-5, -5, -5,
// 立方体的尺寸。
10, 10, 10
)
// 再次调用 texOffs 和 addBox 以添加另一个立方体。
.texOffs(30, 40)
.addBox(-1, -1, -1, 1, 1, 1)
// addBox() 有多种重载形式,允许进行额外操作,
// 例如纹理镜像、纹理缩放、指定渲染方向,
// 以及一个全局缩放(CubeDeformation),作用于所有立方体。
// 本例使用了最后一种,请查阅各个方法的用法以获得更多示例。
.texOffs(50, 60)
.addBox(5, 5, 5, 4, 4, 4, CubeDeformation.extend(1.2f)),
// 对 CubeListBuilder 的所有元素应用的初始定位。除了 PartPose#offset,
// 还可以使用 PartPose#offsetAndRotation。这个对象可以在多个 PartDefinition 之间复用。
// 并不是所有模型都会用到它。例如,自定义护甲层通常会用
// 关联玩家(或其他类人生物)渲染器的 PartPose,以便护甲“贴合”玩家模型。
PartPose.offset(0, 8, 0)
);
// 现在我们可以为任意 PartDefinition 添加子部件,从而创建层级结构。
PartDefinition part1 = root.addOrReplaceChild(...);
PartDefinition part2 = head.addOrReplaceChild(...);
PartDefinition part3 = part1.addOrReplaceChild(...);
// 最后,从 MeshDefinition 创建 LayerDefinition。
// 这两个整数是期望的纹理尺寸;在本例中为 64x32。
return LayerDefinition.create(mesh, 64, 32);
}
}
tip

Blockbench 建模程序在创建实体模型时非常有用。要实现这一点,在 Blockbench 中创建模型时请选择 Modded Entity 选项。

Blockbench 还提供将模型导出为 LayerDefinition 创建方法的选项,该选项位于 File -> Export -> Export Java Entity

注册 Layer Definition(层定义)

当我们拥有实体的层定义(layer definition)后,需要在 EntityRenderersEvent.RegisterLayerDefinitions 事件中进行注册。为此,我们需要一个 ModelLayerLocation,它本质上是该层的标识符(请记住,一个实体可以有多个层)。

// 我们的 ModelLayerLocation。
public static final ModelLayerLocation MY_LAYER = new ModelLayerLocation(
// 这里应该是该层所属实体的名称。
// 如果该层可以用于多个实体,也可以更通用一些。
ResourceLocation.fromNamespaceAndPath("examplemod", "example_entity"),
// 层本身的名称。对于实体的基础模型应为 main,
// 对于更具体的层(如“wings”)则应使用更具描述性的名称。
"main"
);

@SubscribeEvent
public static void registerLayerDefinitions(EntityRenderersEvent.RegisterLayerDefinitions event) {
// 在这里添加我们的层。
event.add(MY_LAYER, MyEntityModel::createBodyLayer);
}

创建渲染层与烘焙 Layer Definition(层定义)

下一步是对层定义(layer definition)进行烘焙。为此,我们首先回到实体模型类中:

public class MyEntityModel extends EntityModel<MyEntityRenderState> {
// 将特定的模型部件作为字段保存,供下方使用。
private final ModelPart head;

// 这里传入的 ModelPart 是我们烘焙后的模型的根节点。
// 稍后我们会介绍实际的烘焙过程。
public MyEntityModel(ModelPart root) {
// 父类构造器调用可以选择性地指定 RenderType。
super(root);
// 保存 head 部件,供下方使用。
this.head = root.getChild("head");
}

public static LayerDefinition createBodyLayer() {...}

// 使用此方法根据渲染状态更新模型的旋转、可见性等。如果你更改了 EntityModel 超类的泛型参数,这里的参数类型也会随之变化。
@Override
public void setupAnim(MyEntityRenderState state) {
// 调用父类方法,将所有值重置为默认值。
super.setupAnim(state);
// 修改模型部件。
head.visible = state.myBoolean();
head.xRot = state.myXRotation();
head.yRot = state.myYRotation();
head.zRot = state.myZRotation();
}
}

现在我们的模型已经能够正确接收烘焙后的 ModelPart,接下来可以创建自己的 RenderLayer 子类,并用它来烘焙 LayerDefinition,方式如下:

// 泛型参数需要与你之前其他地方使用的类型保持一致。
public class MyRenderLayer extends RenderLayer<MyEntityRenderState, MyEntityModel> {
private final MyEntityModel model;

// 创建渲染层(render layer)。renderer 参数是必须的,用于传递给父类(super)。
// 可以根据需要添加其他参数。例如,我们需要 EntityModelSet 来进行模型烘焙(model baking)。
public MyRenderLayer(MyEntityRenderer renderer, EntityModelSet entityModelSet) {
super(renderer);
// 使用我们注册层定义时的 ModelLayerLocation,烘焙并存储我们的层定义。
// 如有需要,也可以用这种方式存储多个模型,并在下面使用它们。
this.model = new MyEntityModel(entityModelSet.bakeLayer(MY_LAYER));
}

@Override
public void render(PoseStack poseStack, MultiBufferSource bufferSource, int packedLight, MyEntityRenderState renderState, float yRot, float xRot) {
// 在这里渲染该层。我们已经将实体模型存储在字段中,通常你会在这里使用它。
}
}

向实体渲染器添加渲染层(Adding a Render Layer to an Entity Renderer)

最后,为了将所有内容整合在一起,我们可以像下面这样将该渲染层添加到我们的渲染器中(如果你还记得,现在它需要是一个 living renderer):

// 将我们自定义的渲染状态类作为泛型类型传入。
// 此外,我们还需要实现 RenderLayerParent 接口。一些已有的渲染器(如 LivingEntityRenderer)会自动帮你实现。
public class MyEntityRenderer extends LivingEntityRenderer<MyEntity, MyEntityRenderState, MyEntityModel> {
public MyEntityRenderer(EntityRendererProvider.Context context) {
// 对于 LivingEntityRenderer,父类构造方法需要传入一个“基础”模型和一个阴影半径。
super(context, new MyEntityModel(entityModelSet.bakeLayer(MY_LAYER)), 0.5f);
// 添加渲染层。通过 context 获取 EntityModelSet。在本例中,
// 我们忽略了渲染层渲染的是“基础”模型,实际上这里应当是不同的模型。
this.addLayer(new MyRenderLayer(this, context.getModelSet()));
}

public MyEntityRenderState createRenderState() {
return new MyEntityRenderState();
}

@Override
public void extractRenderState(MyEntity entity, MyEntityRenderState state, float partialTick) {
super.extractRenderState(entity, state, partialTick);
// 在这里提取你自己的状态,具体内容见本文开头。
}

@Override
public void render(MyEntityRenderState state, PoseStack poseStack, MultiBufferSource bufferSource, int packedLight) {
// 调用父类方法会自动帮你渲染渲染层。
super.render(state, poseStack, bufferSource, packedLight);
// 然后,如有需要,在这里执行自定义渲染。
}

// getTextureLocation 是 LivingEntityRenderer 的抽象方法,需要我们重写。
// 纹理路径是相对于 textures/entity 的,因此在本例中,纹理应位于
// assets/examplemod/textures/entity/example_entity.png。该纹理将被模型加载并使用。
@Override
public ResourceLocation getTextureLocation(MyEntityRenderState state) {
return ResourceLocation.fromNamespaceAndPath("examplemod", "example_entity");
}
}

一次性汇总(All At Once)

内容有点多?由于这个系统相当复杂,这里再次列出所有组件(几乎没有赘述):

public class MyEntity extends LivingEntity {...}
public class MyEntityRenderState extends LivingEntityRenderState {...}
public class MyEntityModel extends EntityModel<MyEntityRenderState> {
public static final ModelLayerLocation MY_LAYER = new ModelLayerLocation(
ResourceLocation.fromNamespaceAndPath("examplemod", "example_entity"),
"main"
);
private final ModelPart head;

public MyEntityModel(ModelPart root) {
super(root);
this.head = root.getChild("head");
// 获取头部部件
}

public static LayerDefinition createBodyLayer() {
MeshDefinition mesh = new MeshDefinition();
PartDefinition root = mesh.getRoot();
PartDefinition head = root.addOrReplaceChild(
"head",
CubeListBuilder.create().texOffs(10, 20).addBox(-5, -5, -5, 10, 10, 10),
PartPose.offset(0, 8, 0)
);
// 添加头部部件
return LayerDefinition.create(mesh, 64, 32);
}

@Override
public void setupAnim(MyEntityRenderState state) {
super.setupAnim(state);
// 设置动画
}
}
public class MyRenderLayer extends RenderLayer<MyEntityRenderState, MyEntityModel> {
private final MyEntityModel model;

public MyRenderLayer(MyEntityRenderer renderer, EntityModelSet entityModelSet) {
super(renderer);
this.model = new MyEntityModel(entityModelSet.bakeLayer(MyEntityModel.MY_LAYER));
// 创建模型实例
}

@Override
public void render(PoseStack poseStack, MultiBufferSource bufferSource, int packedLight, MyEntityRenderState renderState, float yRot, float xRot) {
// 渲染渲染层
}
}
public class MyEntityRenderer extends LivingEntityRenderer<MyEntity, MyEntityRenderState, MyEntityModel> {
public MyEntityRenderer(EntityRendererProvider.Context context) {
super(context, new MyEntityModel(entityModelSet.bakeLayer(MY_LAYER)), 0.5f);
this.addLayer(new MyRenderLayer(this, context.getModelSet()));
// 添加自定义渲染层
}

public MyEntityRenderState createRenderState() {
return new MyEntityRenderState();
// 创建渲染状态对象
}

@Override
public void extractRenderState(MyEntity entity, MyEntityRenderState state, float partialTick) {
super.extractRenderState(entity, state, partialTick);
// 提取渲染状态
}

@Override
public void render(MyEntityRenderState state, PoseStack poseStack, MultiBufferSource bufferSource, int packedLight) {
super.render(state, poseStack, bufferSource, packedLight);
// 渲染实体
}

@Override
public ResourceLocation getTextureLocation(MyEntityRenderState state) {
return ResourceLocation.fromNamespaceAndPath("examplemod", "example_entity");
// 返回实体的纹理位置
}
}
@SubscribeEvent
public static void registerLayerDefinitions(EntityRenderersEvent.RegisterLayerDefinitions event) {
event.add(MyEntityModel.MY_LAYER, MyEntityModel::createBodyLayer);
// 注册模型层定义
}

@SubscribeEvent
public static void registerEntityRenderers(EntityRenderersEvent.RegisterRenderers event) {
event.registerEntityRenderer(MY_ENTITY_TYPE.get(), MyEntityRenderer::new);
// 注册实体渲染器
}

修改已有实体渲染器(Modifying Existing Entity Renderers)

在某些场景下,你可能希望对已有的实体渲染器进行扩展,例如为现有实体渲染额外的特效。大多数情况下,这会影响到生物实体(living entities),也就是拥有 LivingEntityRenderer 的实体。这样我们就可以像下面这样为实体添加 渲染层(render layer)

@SubscribeEvent
public static void addLayers(EntityRenderersEvent.AddLayers event) {
// 为每一个实体类型添加渲染层
for (EntityType<?> entityType : event.getEntityTypes()) {
// 获取对应的渲染器
EntityRenderer<?, ?> renderer = event.getRenderer(entityType);
// 检查我们的渲染层是否被该渲染器支持
// 如果你想做一个更通用的渲染层,需要使用通配泛型
if (renderer instanceof MyEntityRenderer myEntityRenderer) {
// 为渲染器添加渲染层,和上面一样,构造一个新的 MyRenderLayer
// EntityModelSet 可以通过 event 的 #getEntityModels 方法获取
myEntityRenderer.addLayer(new MyRenderLayer(renderer, event.getEntityModels()));
}
}
}

对于玩家实体,需要做一些特殊处理,因为实际上可能存在多个玩家渲染器。这些渲染器由事件单独管理。我们可以这样与它们交互:

@SubscribeEvent
public static void addPlayerLayers(EntityRenderersEvent.AddLayers event) {
// 遍历所有可能的玩家模型
for (PlayerSkin.Model skin : event.getSkins()) {
// 获取对应的 PlayerRenderer
if (event.getSkin(skin) instanceof PlayerRenderer playerRenderer) {
// 为渲染器添加渲染层。这里假定渲染层的泛型已正确支持玩家与玩家渲染器
playerRenderer.addLayer(new MyRenderLayer(playerRenderer, event.getEntityModels()));
}
}
}

动画(Animations)

Minecraft 通过 AnimationState 类为实体模型提供了动画系统。NeoForge 增加了一套系统,允许你用 JSON 文件定义这些实体动画,类似于第三方库 GeckoLib

动画通过 JSON 文件定义,文件路径为 assets/<namespace>/neoforge/animations/entity/<path>.json(例如对于 资源位置(resource location) examplemod:example,文件应位于 assets/examplemod/neoforge/animations/entity/example.json)。动画文件的格式如下:

{
// 动画的持续时间,单位为秒。
"length": 1.5,
// 动画是否循环播放(true),或在结束时停止(false)。
// 可选项,默认值为 false。
"loop": true,
// 需要被动画化的部件列表,以及它们的动画数据。
"animations": [
{
// 需要被动画化的部件名称。必须与上文 LayerDefinition 中定义的部件名称一致。
// 如果有多个匹配项,将选择深度优先搜索中遇到的第一个。
"bone": "head",
// 需要被更改的目标值。可用目标见下文说明。
"target": "minecraft:rotation",
// 该部件的关键帧列表。
"keyframes": [
{
// 关键帧的时间戳,单位为秒。
// 应当在 0 到动画总长度之间。
"timestamp": 0.5,
// 关键帧的实际“目标值”。
"target": [22.5, 0, 0],
// 所使用的插值方法。可用方法见下文。
"interpolation": "minecraft:linear"
}
]
}
]
}
tip

强烈建议将此系统与 Blockbench 建模软件结合使用,Blockbench 提供了一个 animation to JSON 插件,可以大大简化动画制作流程。

在你的模型中,可以像下面这样使用动画:

public class MyEntityModel extends EntityModel<MyEntityRenderState> {
// 创建并保存模型动画的引用。
public static final AnimationHolder EXAMPLE_ANIMATION =
Model.getAnimation(ResourceLocation.fromNamespaceAndPath("examplemod", "example"));

// 其他内容。

@Override
public void setupAnim(MyEntityRenderState state) {
super.setupAnim(state);
// 其他内容。

animate(
// 从你的 EntityRenderState 获取要使用的动画状态。
state.myAnimationState,
// 你的动画持有者。
EXAMPLE_ANIMATION,
// 实体的生存时间,单位为 tick。
state.ageInTicks
);
// animate() 的专用版本,适用于行走动画。
animateWalk(EXAMPLE_ANIMATION, state.walkAnimationPos, state.walkAnimationSpeed, 1, 1);
// 只应用动画第一帧的 animate() 变体。
applyStatic(EXAMPLE_ANIMATION).
}
}

关键帧目标(Keyframe Targets)

NeoForge 默认内置了以下关键帧目标(keyframe targets):

  • minecraft:position:目标值会被设置为该部件的位置(position)属性。
  • minecraft:rotation:目标值会被设置为该部件的旋转(rotation)属性。
  • minecraft:scale:目标值会被设置为该部件的缩放(scale)属性。 可以通过创建新的 AnimationTarget 并在 RegisterJsonAnimationTypesEvent 事件中注册,来添加自定义的动画目标(custom values),示例如下:
@SubscribeEvent
public static void registerJsonAnimationTypes(RegisterJsonAnimationTypesEvent event) {
event.registerTarget(
// 新动画目标的名称,将在 JSON 及其他地方使用。
ResourceLocation.fromNamespaceAndPath("examplemod", "example"),
// 要注册的 AnimationTarget。
new AnimationTarget(...)
);
}

关键帧插值(Keyframe Interpolations)

NeoForge 默认内置了以下几种关键帧插值方式:

  • minecraft:linear:线性插值。
  • minecraft:catmullrom:沿 Catmull-Rom 样条 的插值。

你可以通过创建新的 AnimationChannel.Interpolation(这是一个函数式接口)并在 RegisterJsonAnimationTypesEvent 事件中注册,来添加自定义插值方式,示例如下:

@SubscribeEvent
public static void registerJsonAnimationTypes(RegisterJsonAnimationTypesEvent event) {
event.registerInterpolation(
// 新插值方式的名称,将在 JSON 及其他地方使用。
ResourceLocation.fromNamespaceAndPath("examplemod", "example"),
// 要注册的 AnimationChannel.Interpolation。
(vector, keyframeDelta, keyframes, currentKeyframe, nextKeyframe, scale) -> {...}
);
}