实体渲染器
实体渲染器定义实体的渲染行为,只存在于逻辑客户端和物理客户端。
实体渲染使用实体渲染状态。它是一个保存渲染器所需全部数值的对象。每次渲染实体时,先更新渲染状态,再由 #submit 使用它提交所需的特征,供稍后的渲染阶段使用。
创建实体渲染器
最简单的实体渲染器直接继承 EntityRenderer:
// The generic type in the superclass should be set to what entity you want to render.
// If you wanted to enable rendering for any entity, you'd use Entity, like we do here.
// You'd also use an EntityRenderState that fits your use case. More on this below.
public class MyEntityRenderer extends EntityRenderer<Entity, EntityRenderState> {
// In our constructor, we just forward to super.
public MyEntityRenderer(EntityRendererProvider.Context context) {
super(context);
}
// Tell the render engine how to create a new entity render state.
@Override
public EntityRenderState createRenderState() {
return new EntityRenderState();
}
// Update the render state by copying the needed values from the passed entity to the passed state.
// Both Entity and EntityRenderState may be replaced with more concrete types,
// based on the generic types that have been passed to the supertype.
@Override
public void extractRenderState(Entity entity, EntityRenderState state, float partialTick) {
super.extractRenderState(entity, state, partialTick);
// Extract and store any additional values in the state here.
}
// Actually submit the features of the entity to render.
// The first parameter matches the render state's generic type.
// Calling super will handle leash and name tag submission for you, if applicable.
@Override
public void submit(EntityRenderState renderState, PoseStack poseStack, SubmitNodeCollector collector, CameraRenderState cameraState) {
super.submit(renderState, poseStack, collector, cameraState);
// Do your own submission here
}
}
创建渲染器后,还需要注册并关联到对应实体。在 EntityRenderersEvent.RegisterRenderers 中完成:
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerEntityRenderers(EntityRenderersEvent.RegisterRenderers event) {
event.registerEntityRenderer(MY_ENTITY_TYPE.get(), MyEntityRenderer::new);
}
实体渲染状态
实体渲染状态用于将渲染所需数值与实体本身的数据分开,它就是可变的数据存储对象,因此扩展很简单:
public class MyEntityRenderState extends EntityRenderState {
public ItemStackRenderState stackInHand;
}
只需继承该类、添加字段,并将 EntityRenderer 的泛型参数改成自己的状态类。然后按照前面的说明,在 EntityRenderer#extractRenderState 中更新 stackInHand 字段即可。
修改渲染状态
除定义新的实体渲染状态外,NeoForge 还提供修改现有渲染状态的系统。
创建 ContextKey<T> 并保存在静态字段中,其中 T 为数据类型,然后在 RegisterRenderStateModifiersEvent 的事件处理器中使用:
public static final ContextKey<String> EXAMPLE_CONTEXT = new ContextKey<>(
// The id of your context key. Used for distinguishing between keys internally.
Identifier.fromNamespaceAndPath("examplemod", "example_context"));
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerRenderStateModifiers(RegisterRenderStateModifiersEvent event) {
event.registerEntityModifier(
// A TypeToken for the renderer. It is REQUIRED for this to be instantiated as an anonymous class
// (i.e., with {} at the end) and to have explicit generic parameters, due to generics nonsense.
new TypeToken<LivingEntityRenderer<LivingEntity, LivingEntityRenderState, ?>>(){},
// The modifier itself. This is a BiConsumer of the entity and the entity render state.
// Exact generic types are inferred from the generics in the renderer class used.
(entity, state) -> state.setRenderData(EXAMPLE_CONTEXT, "Hello World!");
);
// Overload of the above method that accepts a Class<?>.
// This should ONLY be used for renderers without any generics, such as PigRenderer.
event.registerEntityModifier(
PigRenderer.class,
(entity, state) -> state.setRenderData(EXAMPLE_CONTEXT, "Hello World!");
);
// Convenience method for working around issues around modifying an avatar's
// render state (e.g. players).
event.registerAvatarEntityModifier(new AvatarRenderStateModifier() {
@Override
public <T extends Avatar & ClientAvatarEntity> void accept(T avatar, AvatarRenderState state) {
state.setRenderData(EXAMPLE_CONTEXT, "Hello World!");
}
});
}
建议 向
EntityRenderState#setRenderData的第二个参数传入null可以清除数据,例如:state.setRenderData(EXAMPLE_CONTEXT, null);
随后可按需通过 EntityRenderState#getRenderData 读取。另有辅助方法 #getRenderDataOrThrow 和 #getRenderDataOrDefault。
继承层级
与实体一样,实体渲染器也有继承层级,不过层次较少。主要类的关系如下:
graph LR;
EntityRenderer --> ArrowRenderer & LivingEntityRenderer & AbstractBoatRenderer & AbstractMinecartRenderer;
LivingEntityRenderer --> ArmorStandRenderer;
LivingEntityRenderer --> MobRenderer --> AgeableMobRenderer --> HumanoidMobRenderer;
LivingEntityRenderer --> AvatarRenderer;
class EntityRenderer,AbstractBoatRenderer,AbstractMinecartRenderer,ArrowRenderer,LivingEntityRenderer,MobRenderer,AgeableMobRenderer,HumanoidMobRenderer yellow;
class ArmorStandRenderer,AvatarRenderer blue;
黄色类是抽象类,蓝色类不是抽象类。
EntityRenderer:抽象基类,许多渲染器直接继承它,尤其是几乎所有非生物实体的渲染器。ArrowRenderer、AbstractBoatRenderer、AbstractMinecartRenderer:主要为方便复用而存在,作为具体渲染器的父类。LivingEntityRenderer:生物实体渲染器的抽象基类,直接子类包括ArmorStandRenderer和AvatarRenderer。ArmorStandRenderer:盔甲架渲染器。AvatarRenderer:渲染玩家等 Avatar。与多数渲染器不同,为不同上下文服务的多个实例可能同时存在。MobRenderer:Mob渲染器的抽象基类,许多具体渲染器直接继承它。AgeableMobRenderer:有幼年变体的Mob的渲染器抽象基类,也包括疣猪兽等有幼年变体的怪物。HumanoidMobRenderer:人形实体渲染器抽象基类,例如僵尸和骷髅使用它。
与选择实体类一样,应选择最符合需求的类。注意它们通常有对应的泛型类型约束,例如 LivingEntityRenderer 的泛型约束包括 LivingEntity 和 LivingEntityRenderState。
实体模型、图层定义与渲染层
复杂的实体渲染器,尤其是 LivingEntityRenderer,会使用图层系统,每层由一个 RenderLayer 表示。一个渲染器可使用多个层,并决定何时提交哪些层。例如,鞘翅独立成层,与穿戴它的 LivingEntity 分开处理;玩家披风也是独立层。
RenderLayer 定义 #submit,用于提交渲染该层所需的特征。与其他提交方法一样,这里可以提交所需的各种内容。常见用途是提交独立模型,例如护甲或其他装备。
首先需要可提交的模型。Model 本质上是供渲染器使用的一组立方体及相关纹理,通常在实体渲染器首次构造时静态创建。
提示 下面开始使用
LivingEntityRenderer,因此假设MyEntity extends LivingEntity且MyEntityRenderState extends LivingEntityRenderState,以满足泛型约束。
创建实体模型类和图层定义
首先创建实体模型类:
public class MyEntityModel extends EntityModel<MyEntityRenderState> {}
示例直接继承 EntityModel。根据需求,也可能更适合继承其某个子类,甚至直接使用 Model 或与实体无关的 Model 子类。创建新模型时,建议先查找最接近所需用途的现有模型,再以它为基础实现。
接着创建 LayerDefinition。它基本上是一组立方体定义,之后可烘焙为 EntityModel。定义方式大致如下:
public class MyEntityModel extends EntityModel<MyEntityRenderState> {
// A static method in which we create our layer definition. createBodyLayer() is the name
// most vanilla models use. If you have multiple layers, you will have multiple of these static methods.
public static LayerDefinition createBodyLayer() {
// Create our mesh.
MeshDefinition mesh = new MeshDefinition();
// The mesh initially contains no object other than the root, which is invisible (has a size of 0x0x0).
PartDefinition root = mesh.getRoot();
// We add a head part.
PartDefinition head = root.addOrReplaceChild(
// The name of the part.
"head",
// The CubeListBuilder we want to add.
CubeListBuilder.create()
// The UV coordinates to use within the texture. Texture binding itself is explained below.
// In this example, we start at U=10, V=20.
.texOffs(10, 20)
// Add our cube. May be called multiple times to add multiple cubes.
// This is relative to the parent part. For the root part, it is relative to the entity's position.
// Be aware that the y axis is flipped, i.e. "up" is subtractive and "down" is additive.
.addBox(
// The top-left-back corner of the cube, relative to the parent object's position.
-5, -5, -5,
// The size of the cube.
10, 10, 10
)
// Call texOffs and addBox again to add another cube.
.texOffs(30, 40)
.addBox(-1, -1, -1, 1, 1, 1)
// Various overloads of addBox() are available, which allow for additional operations
// such as texture mirroring, texture scaling, specifying the directions to be rendered,
// and a global scale to all cubes, known as a CubeDeformation.
// This example uses the latter, please check the usages of the individual methods for more examples.
.texOffs(50, 60)
.addBox(5, 5, 5, 4, 4, 4, CubeDeformation.extend(1.2f)),
// The initial positioning to apply to all elements of the CubeListBuilder. Besides PartPose#offset,
// PartPose#offsetAndRotation is also available. This can be reused across multiple PartDefinitions.
// This may not be used by all models. For example, making custom armor layers will use the associated
// player (or other humanoid) renderer's PartPose instead to have the armor "snap" to the player model.
PartPose.offset(0, 8, 0)
);
// We can now add children to any PartDefinition, thus creating a hierarchy.
PartDefinition part1 = root.addOrReplaceChild(...);
PartDefinition part2 = head.addOrReplaceChild(...);
PartDefinition part3 = part1.addOrReplaceChild(...);
// At the end, we create a LayerDefinition from the MeshDefinition.
// The two integers are the expected dimensions of the texture; 64x32 in our example.
return LayerDefinition.create(mesh, 64, 32);
}
}
建议 Blockbench 建模软件对创建实体模型很有帮助。新建模型时选择 Modded Entity(模组实体)。
Blockbench 还支持将模型导出为创建
LayerDefinition的方法,菜单路径为File -> Export -> Export Java Entity。
注册图层定义
有了实体图层定义,需要在 EntityRenderersEvent.RegisterLayerDefinitions 中注册。为此需要 ModelLayerLocation,它相当于图层标识符,因为同一个实体可以有多个层。
// Our ModelLayerLocation.
public static final ModelLayerLocation MY_LAYER = new ModelLayerLocation(
// Should be the name of the entity this layer belongs to.
// May be more generic if this layer can be used on multiple entities.
Identifier.fromNamespaceAndPath("examplemod", "example_entity"),
// The name of the layer itself. Should be main for the entity's base model,
// and a more descriptive name (e.g. "wings") for more specific layers.
"main"
);
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerLayerDefinitions(EntityRenderersEvent.RegisterLayerDefinitions event) {
// Add our layer here.
event.add(MY_LAYER, MyEntityModel::createBodyLayer);
}
创建渲染层并烘焙图层定义
下一步烘焙图层定义,先回到实体模型类:
public class MyEntityModel extends EntityModel<MyEntityRenderState> {
// Storing specific model parts as fields for use below.
private final ModelPart head;
// The ModelPart passed here is the root of our baked model.
// We will get to the actual baking in just a moment.
public MyEntityModel(ModelPart root) {
// The super constructor call can optionally specify a RenderType.
super(root);
// Store the head part for use below.
this.head = root.getChild("head");
}
public static LayerDefinition createBodyLayer() {...}
// Use this method to update the model rotations, visibility etc. from the render state. If you change the
// generic parameter of the EntityModel superclass, this parameter type changes with it.
@Override
public void setupAnim(MyEntityRenderState state) {
// Calling super to reset all values to default.
super.setupAnim(state);
// Change the model parts.
head.visible = state.myBoolean();
head.xRot = state.myXRotation();
head.yRot = state.myYRotation();
head.zRot = state.myZRotation();
}
}
模型能够正确接收烘焙后的 ModelPart 后,就可以创建 RenderLayer 子类,并在其中烘焙 LayerDefinition:
// The generic parameters need the proper types you used everywhere else up to this point.
public class MyRenderLayer extends RenderLayer<MyEntityRenderState, MyEntityModel> {
private final MyEntityModel model;
// Create the render layer. The renderer parameter is required for passing to super.
// Other parameters can be added as needed. For example, we need the EntityModelSet for model baking.
public MyRenderLayer(MyEntityRenderer renderer, EntityModelSet entityModelSet) {
super(renderer);
// Bake and store our layer definition, using the ModelLayerLocation from back when we registered the layer definition.
// If applicable, you can also store multiple models this way and use them below.
this.model = new MyEntityModel(entityModelSet.bakeLayer(MY_LAYER));
}
@Override
public void submit(PoseStack poseStack, SubmitNodeCollector collector, int lightCoords, MyEntityRenderState renderState, float yRot, float xRot) {
// Submit the features for the layer here. We have stored the entity model in a field, you probably want to use it in some way.
collector
.order(1) // We submit the feature on a later iteration so it renders on top of the entity
.submitModel(this.model, renderState, poseStack, ...);
}
}
向实体渲染器添加渲染层
最后,将渲染层添加到渲染器中,将各部分连接起来。注意,此时渲染器需要是生物实体渲染器:
// Plugging in our custom render state class as the generic type.
// Also, we need to implement RenderLayerParent. Some existing renderers, such as LivingEntityRenderer, do this for you.
public class MyEntityRenderer extends LivingEntityRenderer<MyEntity, MyEntityRenderState, MyEntityModel> {
public MyEntityRenderer(EntityRendererProvider.Context context) {
// For LivingEntityRenderer, the super constructor requires a "base" model and a shadow radius to be supplied.
super(context, new MyEntityModel(context.bakeLayer(MY_LAYER)), 0.5f);
// Add the layer. Get the EntityModelSet from the context. For the purpose of the example,
// we ignore that the render layer submits the "base" model, this would be a different model in practice.
this.addLayer(new MyRenderLayer(this, context.getModelSet()));
}
@Override
public MyEntityRenderState createRenderState() {
return new MyEntityRenderState();
}
@Override
public void extractRenderState(MyEntity entity, MyEntityRenderState state, float partialTick) {
super.extractRenderState(entity, state, partialTick);
// Extract your own stuff here, see the beginning of the article.
}
@Override
public void submit(MyEntityRenderState renderState, PoseStack poseStack, SubmitNodeCollector collector, CameraRenderState cameraState) {
// Calling super will automatically submit the features of the layer for you.
super.submit(renderState, poseStack, collector, cameraState);
// Then, do custom submission here, if applicable.
}
// getTextureLocation is an abstract method in LivingEntityRenderer that we need to override.
// The texture path is relative to the namespace, so it must specify the exact path within the namespace in the assets directory.
// In this example, the texture should be located at `assets/examplemod/textures/entity/example_entity.png`.
// The texture will then be supplied to and used by the model.
@Override
public Identifier getTextureLocation(MyEntityRenderState state) {
return Identifier.fromNamespaceAndPath("examplemod", "textures/entity/example_entity.png");
}
}
完整示例
系统较复杂,下面将全部组件集中列出,省略大部分说明:
public class MyEntity extends LivingEntity {...}
public class MyEntityRenderState extends LivingEntityRenderState {...}
public class MyEntityModel extends EntityModel<MyEntityRenderState> {
public static final ModelLayerLocation MY_LAYER = new ModelLayerLocation(
Identifier.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(MY_LAYER));
}
@Override
public void submit(PoseStack poseStack, SubmitNodeCollector collector, int lightCoords, MyEntityRenderState renderState, float yRot, float xRot) {
// ...
}
}
public class MyEntityRenderer extends LivingEntityRenderer<MyEntity, MyEntityRenderState, MyEntityModel> {
public MyEntityRenderer(EntityRendererProvider.Context context) {
super(context, new MyEntityModel(context.bakeLayer(MY_LAYER)), 0.5f);
this.addLayer(new MyRenderLayer(this, context.getModelSet()));
}
@Override
public MyEntityRenderState createRenderState() {
return new MyEntityRenderState();
}
@Override
public void extractRenderState(MyEntity entity, MyEntityRenderState state, float partialTick) {
super.extractRenderState(entity, state, partialTick);
// ...
}
@Override
public void submit(MyEntityRenderState renderState, PoseStack poseStack, SubmitNodeCollector collector, CameraRenderState cameraState) {
super.submit(renderState, poseStack, collector, cameraState);
// ...
}
@Override
public Identifier getTextureLocation(MyEntityRenderState state) {
return Identifier.fromNamespaceAndPath("examplemod", "textures/entity/example_entity.png");
}
}
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerLayerDefinitions(EntityRenderersEvent.RegisterLayerDefinitions event) {
event.add(MyEntityModel.MY_LAYER, MyEntityModel::createBodyLayer);
}
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerEntityRenderers(EntityRenderersEvent.RegisterRenderers event) {
event.registerEntityRenderer(MY_ENTITY_TYPE.get(), MyEntityRenderer::new);
}
修改已有实体渲染器
有时需要扩展已有实体渲染器,例如为已有实体增加视觉效果。多数情况下涉及带有 LivingEntityRenderer 的生物实体,可以通过添加渲染层实现:
@SubscribeEvent // on the mod event bus only on the physical client
public static void addLayers(EntityRenderersEvent.AddLayers event) {
// Add a layer to every single entity type.
for (EntityType<?> entityType : event.getEntityTypes()) {
// Get our renderer.
EntityRenderer<?, ?> renderer = event.getRenderer(entityType);
// We check if our render layer is supported by the renderer.
// If you want a more general-purpose render layer, you will need to work with wildcard generics.
if (renderer instanceof MyEntityRenderer myEntityRenderer) {
// Add the layer to the renderer. Like above, construct a new MyRenderLayer.
// The EntityModelSet can be retrieved from the event through #getEntityModels.
myEntityRenderer.addLayer(new MyRenderLayer(renderer, event.getEntityModels()));
}
}
}
玩家需要特殊处理,因为可能存在多个玩家渲染器,事件会单独管理它们。可以这样访问:
@SubscribeEvent // on the mod event bus only on the physical client
public static void addPlayerLayers(EntityRenderersEvent.AddLayers event) {
// Iterate over all possible player models.
for (PlayerModelType type : event.getSkins()) {
// Get the associated AvatarRenderer.
AvatarRenderer<AbstractClientPlayer> playerRenderer = event.getPlayerRenderer(type);
if (playerRenderer != null) {
// Add the layer to the renderer. This assumes that the render layer
// has proper generics to support players and player renderers.
playerRenderer.addLayer(new MyRenderLayer(playerRenderer, event.getEntityModels()));
}
}
}
动画
Minecraft 通过 AnimationDefinition 提供实体模型动画系统。NeoForge 在此基础上支持用 JSON 文件定义实体动画,类似 GeckoLib 等第三方库。
动画文件位于 assets/<namespace>/neoforge/animations/entity/<path>.json。例如,资源标识符 examplemod:example 对应 assets/examplemod/neoforge/animations/entity/example.json。格式如下:
{
// The duration of the animation, in seconds.
"length": 1.5,
// Whether the animation should loop (true) or stop (false) when finished.
// Optional, defaults to false.
"loop": true,
// A list of parts to be animated, and their animation data.
"animations": [
{
// The name of the part to be animated. Must match the name of a part
// defined in your LayerDefinition (see above). If there are multiple matches,
// the first match from the performed depth-first search will be picked.
"bone": "head",
// The value to be changed. See below for available targets.
"target": "minecraft:rotation",
// A list of keyframes for the part.
"keyframes": [
{
// The timestamp of the keyframe, in seconds.
// Should be between 0 and the animation length.
"timestamp": 0.5,
// The actual "value" of the keyframe.
"target": [22.5, 0, 0],
// The interpolation method to use. See below for available methods.
"interpolation": "minecraft:linear"
}
]
}
]
}
建议 强烈建议配合 Blockbench 建模软件使用,它提供动画 JSON 插件。
在模型中可以这样使用动画:
public class MyEntityModel extends EntityModel<MyEntityRenderState> {
// Create and store a reference to the animation holder.
public static final AnimationHolder EXAMPLE_ANIMATION =
Model.getAnimation(Identifier.fromNamespaceAndPath("examplemod", "example"));
// A field to hold the baked animation
private final KeyframeAnimation example;
public MyEntityModel(ModelPart root) {
// Bake the animation for the model
// Pass in whatever 'ModelPart' that the animation is applied to
// It should cover all referenced bones
this.example = EXAMPLE_ANIMATION.get().bake(root);
}
// Other stuff here.
@Override
public void setupAnim(MyEntityRenderState state) {
super.setupAnim(state);
// Other stuff here.
this.example.apply(
// Get the animation state to use from your EntityRenderState.
state.myAnimationState,
// Your entity age, in ticks.
state.ageInTicks
);
// A specialized version of apply(), designed for walking animations.
this.example.applyWalk(state.walkAnimationPos, state.walkAnimationSpeed, 1, 1);
// A version of apply() that only applies the first frame of animation.
this.example.applyStatic();
}
}
关键帧目标
NeoForge 默认提供以下关键帧目标:
minecraft:position:将目标值设为模型部件的位置。minecraft:rotation:将目标值设为模型部件的旋转。minecraft:scale:将目标值设为模型部件的缩放。
要添加自定义目标,可创建 AnimationTarget 并在 RegisterJsonAnimationTypesEvent 中注册:
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerJsonAnimationTypes(RegisterJsonAnimationTypesEvent event) {
event.registerTarget(
// The name of the new target, to be used in JSON and other places.
Identifier.fromNamespaceAndPath("examplemod", "example"),
// The AnimationTarget to register.
new AnimationTarget(...)
);
}
关键帧插值
NeoForge 默认提供以下插值方式:
minecraft:linear:线性插值。minecraft:catmullrom:沿 Catmull-Rom 样条插值。
要添加自定义插值,可创建 AnimationChannel.Interpolation(函数式接口),并在 RegisterJsonAnimationTypesEvent 中注册:
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerJsonAnimationTypes(RegisterJsonAnimationTypesEvent event) {
event.registerInterpolation(
// The name of the new interpolation, to be used in JSON and other places.
Identifier.fromNamespaceAndPath("examplemod", "example"),
// The AnimationChannel.Interpolation to register.
(vector, keyframeDelta, keyframes, currentKeyframe, nextKeyframe, scale) -> {...}
);
}
来源: NeoForged/Documentation(26.1)。中文翻译依据提交
4a680cdd4d15。