客户端粒子

客户端粒子

粒子是用于润色游戏、增强沉浸感的视觉效果。由于它主要是视觉内容,关键部分只存在于物理客户端和逻辑客户端一端。

本文介绍粒子的渲染方面。关于通常用于生成粒子的粒子类型,以及指定粒子精灵的粒子描述,请参阅配套的粒子类型文档。

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。