客户端粒子
粒子是用于润色游戏、增强沉浸感的视觉效果。由于它主要是视觉内容,关键部分只存在于物理客户端和逻辑客户端一端。
本文介绍粒子的渲染方面。关于通常用于生成粒子的粒子类型,以及指定粒子精灵的粒子描述,请参阅配套的粒子类型文档。
Particle 类
Particle 定义世界中生成、并展示给玩家的客户端对象。大多数属性和基础物理行为由 gravity、lifetime、hasPhysics、friction 等字段控制。最常重写的方法只有 tick 和 move,作用如其名称所示。因此,大多数自定义粒子的代码很短,通常只有一个设置所需字段的构造函数,偶尔重写这两个方法。
创建粒子最常用的两种方法是:继承 SingleQuadParticle 或它的某个实现(如 SimpleAnimatedParticle),将一张朝向视线的纹理绘制到屏幕;或者直接继承 Particle,完全控制提交用于渲染的特征。
单四边形粒子
继承 SingleQuadParticle 的粒子使用某个图集精灵,在屏幕上绘制单个四边形。该类提供许多辅助功能:通过 quadSize 字段或 scale 方法设置粒子大小,通过 setColor 和 setAlpha 调整纹理颜色等。四边形粒子最重要的两点,是作为纹理的 TextureAtlasSprite,以及由 SingleQuadParticle.Layer 决定的精灵来源和渲染方式。
首先,将 TextureAtlasSprite 传入构造函数。可以直接传入它,但更常见的是传入表示粒子整个生命周期纹理的 SpriteSet。最初,精灵会被设置到受保护的 sprite 字段;在 tick 中,可以分别调用 setSprite 或 setSpriteFromAge 更新它。
建议 如果在粒子构造函数中修改了
age或lifetime字段,应调用setSpriteFromAge,显示相应纹理。
接着,在特征提交过程中,SingleQuadParticle.Layer 决定使用哪个图集,以及将四边形绘制到屏幕时采用的管线。原版默认提供六层:
| 层 | 纹理图集 | 用途 |
|---|---|---|
OPAQUE_TERRAIN |
方块 | 使用无透明度方块纹理的粒子 |
TRANSLUCENT_TERRAIN |
方块 | 使用带透明度方块纹理的粒子 |
OPAQUE_ITEMS |
物品 | 使用无透明度物品纹理的粒子 |
TRANSLUCENT_ITEMS |
物品 | 使用带透明度物品纹理的粒子 |
OPAQUE |
粒子 | 无透明度粒子 |
TRANSLUCENT |
粒子 | 带透明度粒子 |
使用原版层时,可以方便地调用 SingleQuadParticle.Layer#bySprite 并传入纹理,确定粒子应属于哪一层。
直接调用构造函数即可创建自定义层。
public class MyQuadParticle extends SingleQuadParticle {
public static final SingleQuadParticle.Layer EXAMPLE_LAYER = new SingleQuadParticle.Layer(
// Whether the particle will have textures that are not fully opaque.
true,
// The texture atlas used to get the sprite from.
// This should match `TextureAtlasSprite#atlasLocation`.
TextureAtlas.LOCATION_PARTICLES,
// The render pipeline used to draw the particle.
// Custom render pipelines should be based from `RenderPipelines#PARTICLE_SNIPPET`
// to specify the available uniforms and samplers.
RenderPipelines.WEATHER_DEPTH_WRITE
);
private final SpriteSet spriteSet;
// First four parameters are self-explanatory.
// The sprite set or atlas sprite are typically given through the provider, see below.
// Additional parameters can be added as needed, e.g., xSpeed/ySpeed/zSpeed.
public MyQuadParticle(ClientLevel level, double x, double y, double z, SpriteSet spriteSet) {
// Initial sprite set in constructor
super(level, x, y, z, spriteSet.first());
this.spriteSet = spriteSet;
this.gravity = 0; // Our particle floats in midair now, because why not.
}
@Override
public void tick() {
// Let super handle movement.
// You may replace this with your own movement if needed.
// You may also override move() if you only want to modify the built-in movement.
super.tick();
// Set the sprite for the current particle age, i.e. advance the animation.
this.setSpriteFromAge(this.spriteSet);
}
@Override
protected abstract SingleQuadParticle.Layer getLayer() {
// Sets the layer used to get and submit the texture.
return EXAMPLE_LAYER;
}
}
警告 如果粒子的
SingleQuadParticle.Layer使用TextureAtlas#LOCATION_PARTICLES,就必须关联粒子描述,否则粒子所需纹理不会添加到图集中。
粒子组与渲染状态
如果粒子需要比四边形更复杂的表现,就需要自己的 ParticleGroup<P>,其中 P 是 Particle 的类型。ParticleGroup 负责更新特定子集中的 Particle,并在 Particle#isAlive 返回 false 时移除它们。每个组最多能容纳 16,384 个粒子,满后会淘汰最早加入的粒子。
// Let's assume we have the following particle class
public class ComplexParticle extends Particle {
// You are not required to use these fields or store these values.
// It is up to you to determine what you wish to render and get the
// appropriate data.
private final Model.Simple model;
private final SpriteId sprite;
public ComplexParticle(ClientLevel level, double x, double y, double z) {
super(level, x, y, z);
this.model = StandingSignRenderer.createSignModel(
Minecraft.getInstance().getEntityModels(), WoodType.OAK, PlainSignBlock.Attachment.GROUND
);
this.sprite = Sheets.getSignSprite(WoodType.OAK);
}
public Model.Simple model() {
return this.model;
}
public SpriteId sprite() {
return this.sprite;
}
}
// We can create a basic particle group like so
public class ComplexParticleGroup extends ParticleGroup<ComplexParticle> {
public ComplexParticleGroup(ParticleEngine engine) {
super(engine);
}
// ...
}
Particle 加入 ParticleGroup 后,会在特征提交时通过 ParticleGroup#extractRenderState 提取为 ParticleGroupRenderState。后者兼具两种职责:保存提取出的粒子渲染状态,以及通过 #submit 提交粒子元素以供渲染。
// The particle group render state
public record ComplexParticleRenderState(List<ComplexParticleRenderState.Entry> entries) implements ParticleGroupRenderState {
// Each entry represents a particle in the group
public record Entry(Model.Simple model, SpriteId sprite, PoseStack pose) {}
@Override
public void submit(SubmitNodeCollector collector, CameraRenderState camera) {
// Submit the particle elements to render
for (ComplexParticleRenderState.Entry entry : this.entries) {
collector.submitModel(...);
}
}
}
// And in the group...
public class ComplexParticleGroup extends ParticleGroup<ComplexParticle> {
// ...
@Override
public ParticleGroupRenderState extractRenderState(Frustum frustum, Camera camera, float partialTickTime) {
// Extract the render state from the particles
List<ComplexParticleRenderState.Entry> entries = new ArrayList<>();
for (ComplexParticle particle : this.particles) {
PoseStack pose = new PoseStack();
pose.pushPose();
pose.mulPose(camera.rotation());
entries.add(new ComplexParticleRenderState.Entry(particle.model(), particle.sprite(), pose));
}
return new ComplexParticleRenderState(entries);
}
}
Particle 自身不知道它属于哪个 ParticleGroup,ParticleEngine 也不知道该组存在。这些对象通过组的唯一标识符 ParticleRenderType 关联起来。在客户端 Mod 总线事件 RegisterParticleGroupsEvent 中,将 ParticleRenderType 关联到 ParticleGroup。随后,让 Particle#getGroup 返回所创建的类型,粒子就能使用该组。
// Create the render type
// The string passed in should be a stringified `Identifier`
public static final ParticleRenderType COMPLEX = new ParticleRenderType("examplemod:complex");
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerParticleProviders(RegisterParticleGroupsEvent event) {
// Link the render type to the particle group
event.register(COMPLEX, ComplexParticleGroup::new);
}
public class ComplexParticle extends Particle {
// ...
@Override
public ParticleRenderType getGroup() {
// Tell the particle to render using the particle group
return COMPLEX;
}
}
ParticleProvider
为某个粒子类型创建粒子后,必须通过 ParticleProvider 将两者关联。ParticleProvider 是仅限客户端的类,负责通过 createParticle 从 ParticleEngine 实际创建 Particle。虽然这里可以包含复杂逻辑,但许多粒子提供器都像下面这样简单:
// The generic type of ParticleProvider must match the type of the particle type this provider is for.
public class MyQuadParticleProvider implements ParticleProvider<SimpleParticleType> {
// A set of particle sprites.
private final SpriteSet spriteSet;
// The registration function passes a SpriteSet, so we accept that and store it for further use.
// If your particle does not require a SpriteSet, this constructor can be omitted.
public MyParticleProvider(SpriteSet spriteSet) {
this.spriteSet = spriteSet;
}
// This is where the magic happens. We return a new particle each time this method is called!
// The type of the first parameter matches the generic type passed to the super interface.
@Override
@Nullable
public Particle createParticle(SimpleParticleType particleType, ClientLevel level, double x, double y, double z, double xd, double yd, double zd, RandomSource random
) {
// We don't use the type, speed deltas, or engine random.
return new MyQuadParticle(level, x, y, z, this.spriteSet);
}
}
接着,必须在客户端 Mod 总线事件 RegisterParticleProvidersEvent 中,将粒子提供器与粒子类型关联:
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerParticleProviders(RegisterParticleProvidersEvent event) {
// There are multiple ways to register providers, all differing in the functional type they provide in the
// second parameter. For example, #registerSpriteSet represents a Function<SpriteSet, ParticleProvider<?>>:
event.registerSpriteSet(MyParticleTypes.MY_QUAD_PARTICLE.get(), MyQuadParticleProvider::new);
// #registerSpecial, on the other hand, maps to a ParticleProvider<?>.
// This should be used if the sprite is not obtained from the particle description.
}
警告 如果使用
registerSpriteSet,粒子类型还必须关联粒子描述。否则,会抛出说明“Failed to load description”的异常。
来源: NeoForged/Documentation(26.1)。中文翻译依据提交
4a680cdd4d15。