API/Utilities for Particle creation and registration
kgscialdone opened this issue ยท 15 comments
As it currently stands, creating a custom Particle
is... quite the ordeal. There are several steps involved in doing so:
- Creating the
Particle
subclass (client) - Registering any required sprites (client)
- Registering the appropriate
ParticleType
(client/server) - Registering a
ParticleFactory
(client) - Actually spawning the particle (client)
Of these 5, only registering the ParticleType
is straightforward, and even that can't be easily understood by looking at how Vanilla handles particles. Thus, I'd like to propose a Fabric wrapper around the particle system to simplify this process.
Simple Steps - Particle
and ParticleType
Creating a custom Particle
is almost as straightforward and easy as a Block
or Item
as long as you don't need to do anything particularly fancy with it. The trouble only really comes when you try to apply a custom texture. It's almost as simple as this:
@Environment(EnvType.CLIENT)
public class TestParticle extends SpriteBillboardParticle {
static final Identifier sprite = new Identifier("particletest:particle/test_particle");
public TestParticle(World world, double x, double y, double z) { this(world, x, y, z, 0, 0, 0); }
public TestParticle(World world, double x, double y, double z, double vx, double vy, double vz) {
super(world, x, y, z, vx, vy, vz);
this.setSprite(MinecraftClient.getInstance().getSpriteAtlas().getSprite(sprite));
}
public ParticleTextureSheet getType() { return ParticleTextureSheet.PARTICLE_SHEET_OPAQUE; }
}
Registering the ParticleType
is similarly straightforward, albeit extremely obtuse:
public class ParticleTestCommon implements ModInitializer {
public static DefaultParticleType testParticleType = new DefaultParticleType(false){};
public void onInitialize() {
Registry.register(Registry.PARTICLE_TYPE, "particletest:test_particle", testParticleType);
}
}
Problem 1 - Registering ParticleFactory
s
ParticleFactory
registry, on the other hand, is quite the ordeal. There is no Registry
type for ParticleFactory
s, and all the relevant methods on ParticleManager
are private. The only solution I've found is a client-side mixin:
@Mixin(ParticleManager.class)
public abstract class MixinParticleManager {
@Shadow
private <T extends ParticleEffect> void registerFactory(ParticleType<T> pt, ParticleFactory<T> pf) {}
@Inject(method = "net/minecraft/client/particle/ParticleManager.registerDefaultFactories()V", at = @At("RETURN"))
private void registerCustomFactories(CallbackInfo cbi) {
this.registerFactory(ParticleTestCommon.testParticleType, (pt, world, x, y, z, vx, vy, vz) -> {
return new TestParticle(world, x, y, z, vx, vy, vz);
});
}
}
We can then spawn our particle as follows:
world.addParticle(testParticleType, x, y, z, 0, 0, 0);
Or, if we need to pass custom properties of some kind, we can bypass the ParticleFactory
altogether (keep in mind that this will make your particle incompatible with or feature-limited when spawned via the /particle
command - this should really be done through a custom ParticleType
rather than bypassing ParticleFactory
but I haven't messed with that yet so I can't give examples):
MinecraftClient.getInstance().particleManager.addParticle(new TestParticle(world, x, y, z, vx, vy, vz, ...));
This works passably, but as we'll see in just a moment, it comes with an inherent and deadly problem.
Problem 2 - Registering Textures
Let's get the biggest problem out of the way now - registering custom particle textures is nearly if not impossible to do "Vanilla-esque". There are two separate levels involved; the client texture registry, and a modification to how your ParticleFactory
is registered. Registering the texture with the client is (theoretically) simple, via ClientSpriteRegistryCallback
:
@Environment(EnvType.CLIENT)
public class ParticleTestClient implements ClientModInitializer {
public void onInitializeClient() {
ClientSpriteRegistryCallback.EVENT.register((atlas, registry) -> {
if(atlas == MinecraftClient.getInstance().getSpriteAtlas()) {
registry.register(TestParticle.sprite);
}
});
}
}
However, the problem comes when we try to update our ParticleFactory
to properly use this texture. Registering a ParticleFactory
that can use this texture requires a SpriteProvider
, which uses a different version of ParticleManager#registerFactory
. Unfortunately, ParticleManager
uses two package-private inner classes in the process of this version of the registry method: SimpleSpriteProvider
and class_4091
(henceforth called ParticleFactoryFactory
for simplicity, because that's literally what it is). It's impossible to shadow the proper version of registerFactory
, because it uses ParticleFactoryFactory
in its signature. It's also impossible to shadow the maps that registerFactory
saves the registered information to and create our own registerFactory
method, since one of those maps has SimpleSpriteProvider
in its signature. As of the time of writing, I have been unable to find any alternative method that uses the built-in particle sprite registry.
The Workaround
I have, however, found one workaround - skipping the vanilla particle texture registry altogether. This reduces the process to only 4 steps:
- Creating the
Particle
subclass (client) - Registering the appropriate
ParticleType
(client/server) - Registering a
ParticleFactory
(client) - Actually spawning the particle (client)
Since we no longer need to use the vanilla sprite registry, there's no need to use the more complex version of ParticleManager#registerFactory
, and we bypass the issue of the package-private inner classes altogether. There are a couple of other changes that come along with this:
- The client-side entrypoint is no longer needed
- Our custom
Particle
needs to extendBillboardParticle
instead ofSpriteBillboardParticle
(cutting one link out of the inheritance chain), which will require a couple of extra abstract method implementations - We need to manually acquire and bind our particle texture when we're ready to draw our particle
This means the following changes to our Particle
class:
@Environment(EnvType.CLIENT)
public class TestParticle extends /*Sprite*/BillboardParticle { // <-- HERE
static final Identifier sprite = new Identifier("particletest:textures/particle/test_particle.png"); // <-- HERE
public TestParticle(World world, double x, double y, double z) { this(world, x, y, z, 0, 0, 0); }
public TestParticle(World world, double x, double y, double z, double vx, double vy, double vz) {
super(world, x, y, z, vx, vy, vz);
// this.setSprite(MinecraftClient.getInstance().getSpriteAtlas().getSprite(sprite)); // <-- HERE
}
public ParticleTextureSheet getType() { return ParticleTextureSheet.PARTICLE_SHEET_OPAQUE; }
// v ADDED METHODS v
public void buildGeometry(BufferBuilder var1, Camera var2, float var3, float var4, float var5, float var6, float var7, float var8) {
MinecraftClient.getInstance().getTextureManager().bindTexture(texture);
super.buildGeometry(var1, var2, var3, var4, var5, var6, var7, var8);
}
protected float getMinU() { return 0; }
protected float getMaxU() { return 1; }
protected float getMinV() { return 0; }
protected float getMaxV() { return 1; }
}
This works perfectly fine. We can revert our MixinParticleManager
to before we tried to shadow the more complex overload of registerFactory
, and spawn our particles into the world the same way.
Proposed API
I would personally propose the following additions to Fabric's API:
- A
FabricParticle
class which can be extended rather than extendingBillboardParticle
directly, which implements the basic method changes likegetMin/MaxU/V
andbuildGeometry
and makes the texture location a superctor parameter (potentially simplified from the full path needed?)- Any particles which are more complicated than
BillboardParticle
is set up for will need to do this for themselves, but currently all Vanilla particles except the following extendBillboardParticle
at least indirectly, so these cases should be rare.ElderGuardianAppearanceParticle
EmitterParticle
ExplosionEmitterParticle
FireworksSparkParticle
andFireworkParticle
ItemPickupParticle
- Some of the base "extend me" particle classes that aren't actual ingame particles on their own
- Any particles which are more complicated than
- A
FabricParticleTypeRegistry
and/orFabricParticleFactoryRegistry
which can be publicly accessed- The former would be for registering
ParticleTypes
-DefaultParticleType
is almost always sufficient for this, but it would be trivial to add an overload that accepts a manually-constructedParticleType
. The registry method would take an identifier and optional customParticleType
, callRegistry.register(PARTICLE_TYPE, ...)
, and return the registeredParticleType
so it could be saved for later use.- Note that while this is not strictly necessary for the API to function, it would smooth the process of understanding how to register a particle significantly, as it cuts out the middle man of understanding where you're supposed to get a
ParticleType
and how to construct a trivial one for simple particles.
- Note that while this is not strictly necessary for the API to function, it would smooth the process of understanding how to register a particle significantly, as it cuts out the middle man of understanding where you're supposed to get a
- The latter would be for registering
ParticleFactory
s. The registry method would take aParticleType
(usually from the above method) and a lambda for the factory body, and store them in a temporary map.
- The former would be for registering
- A mixin to
ParticleManager#registerDefaultFactories
, as done manually above, that accesses the temporary map fromFabricParticleFactoryRegistry
and registers all of the contained factories withParticleManager#registerFactory
. - Potentially a simplified way of creating custom
ParticleType
s?
These additions would drastically simplify the process of creating a custom particle, allowing modders to bypass potential hours of digging through Vanilla's code before realizing that they can't actually register a particle the same way Vanilla does.
I'm having 3 problems...
-
getting errors for super.buildGeometry variables 1 - 8: "buildGeometry(net.minecraft.client.render.VertexConsumer, net.minecraft.client.render.Camera, float)' in 'net.minecraft.client.particle.BillboardParticle' cannot be applied to '(net.minecraft.client.render.BufferBuilder, net.minecraft.client.render.Camera, float, float, float, float, float, float"
-
java.lang.IllegalStateException: Failed to load description for particle endmod:end_bubble
-
java.io.FileNotFoundException: endmod:particles/end_bubble.json
@Shadowhunter22 This issue is extremely outdated and none of the code in it is relevant to current Fabric development. It was replaced by a PR in #264, which itself was superceded by #341, which has been merged for almost 2 years now. If you're trying to use the code from this issue, don't. If you're not, you're in the wrong place.
Thank you for the detailed write-up, by the way! This is probably the first issue/PR in Fabric where I didn't have to go back to the source code to understand the problem domain.
Shouldn't you be able to use fabric-textures to register on the particle sheet already? (The tough part is detecting that specific sheet, but I'm not too worried)
My proposal differs a bit:
- A FabricParticleType class, extending ParticleType, which requires you to specify your own ParticleFactory (and registers it for you!) in fabric-object-builders
- Adding additional methods to fabric-textures to detect the particle sheet
- A simplified variant, letting you register Identifiers for textures right in the FabricParticleType.
I hadn't seen fabric-textures, I'll go through and test that later today/tomorrow. Assuming that works, though, there are a couple other issues with your reproposal:
ParticleFactory
is a client side concept, whileParticleType
is both. It would significantly complicate the process to mix the two directly.- In Vanilla, textures are specified (indirectly via SpriteProvider) in the
ParticleFactory
for all particles that use the particle texture atlas. Why would that move toParticleType
? - Custom
ParticleType
s are necessary for purposely handling additional required data. This kind of leaves people who need that out in the cold.
The only way I could really see your version working is if we completely hide the concept of a ParticleFactory
from modders and replace it with a lambda returning a constructed Particle
. We'd then have to, on the client side only, wrap that lambda in a proper ParticleFactory
to provide sprite data, and register that factory silently in the background. If that lambda ever leaks to the server side and gets called, we're screwed and the game crashes, because Particle
doesn't exist on the server side. This still leaves custom ParticleType
s out in the cold a bit, but is perhaps feasible.
This also doesn't really touch on Particle
s themselves - the actual Particle
class, while simple once understood, is a nightmare to figure out how to extend by looking at Vanilla. Would it be feasible to have a FabricSimpleParticle
or similar that can be more painlessly extended than SpriteBillboardParticle
, perhaps by passing the aforementioned FabricParticleType
to the superctor to provide sprite location info?
Actually, on second thought that may be better anyways. I'm sure there's a best solution we haven't thought of yet, but an @Enviroment(Env.CLIENT)
getter for a private mapping of ParticleType
s to factory bodies would more or less solve the side conflict issues. Removing the idea of an overarching ParticleFactory
system from the front end would definitely simplify understanding how to do particles for modders. At that point, our only major issues are Particle
and custom ParticleType
s, both of which are certainly feasible to design the API to handle. And since the only server-side reference to the Particle
class would be wrapped in a lambda that never gets called on the server side, we'd be golden in terms of side safety (if potentially slightly confusing when you look closely).
Is PR #195 an adequate solution for Problem 2? If so, that leaves Problem 1.
I need to do a couple tests, but at a glance it seems like it should solve the sprite registry side of problem 2. However, it's still not possible to register a sprite factory that properly makes use of said sprite, at least in the same way Vanilla does - that would require the ParticleManager
mixin to shadow either a method with ParticleFactoryFactory
in its signature or a field with SimpleSpriteProvider
in its signature, both of which are package-private. Bypassing the client sprite registry is just a side effect of the workaround, really, not the goal in itself.
After extensive testing, I finally made a breakthrough and figured out what was happening to prevent sprites from loading when not passed through the whole ParticleFactory
/SpriteProvider
chain. This means that sprites can now be registered and used as expected, rather than using the above workaround.
The problem turned out to be a lack of access to ParticleManager#particleAtlasTexture
. The method I was using to get a SpriteAtlasTexture
before was MinecraftClient.getInstance().getSpriteAtlas()
, which returns the block atlas texture rather than the particle atlas texture. We should definitely still wrap this in an API module to make it more intuitive, but it should be much simpler to wrap now.
Below is the test code I used to achieve this, which will need to be cleaned up and reorganized for a full API addition, and should probably be abstracted away behind a SpriteBillboardParticle
subclass.
@Environment(EnvType.CLIENT)
public class ParticleTestClient implements ClientModInitializer {
public void onInitializeClient() {
// This throws a couple nonfatal errors, should replace with sheet-specific code from #195
ClientSpriteRegistryCallback.EVENT.register((atlas, registry) -> {
registry.register(TestParticle.sprite);
});
}
}
public interface ParticleManagerHooks {
public SpriteAtlasTexture getParticleAtlasTexture();
}
@Mixin(ParticleManager.class)
public abstract class MixinParticleManager implements ParticleManagerHooks {
// Omitted - the registerFactory code from earlier in the proposal
@Shadow
private SpriteAtlasTexture particleAtlasTexture;
public SpriteAtlasTexture getParticleAtlasTexture() { return particleAtlasTexture; }
}
@Environment(EnvType.CLIENT)
public class TestParticle extends SpriteBillboardParticle {
static final Identifier sprite = new Identifier("particletest:test_particle");
public TestParticle(World world, double x, double y, double z) { this(world, x, y, z, 0, 0, 0); }
public TestParticle(World world, double x, double y, double z, double vx, double vy, double vz) {
super(world, x, y, z, vx, vy, vz);
SpriteAtlasTexture sat = ((ParticleManagerHooks)MinecraftClient.getInstance().particleManager).getParticleAtlasTexture();
this.setSprite(sat.getSprite(this.sprite));
}
public ParticleTextureSheet getType() { return ParticleTextureSheet.PARTICLE_SHEET_OPAQUE; }
}
Well... yes.
I'm glad we don't have to replicate the SpriteProvider system, at least.
Yes, I know I'm blind. That said, it's not exactly like there were any other visibly relevant methods ๐
Either way, this and #195 resolves problem 2 entirely. Problem 1 should be trivial to wrap the API around, so that leaves us with the following (imo):
- Create an API for easily creating/registering
ParticleFactory
s andParticleType
s - Implement a
SpriteBillboardParticle
subclass that abstracts away the process of assigning the sprite
I need to look more into how custom ParticleType
s work, but they don't seem particularly complicated all things considered, so they should be trivial to either support directly in the API or allow for using them by bypassing the createParticleType
(or w/e) method.
Closing this now that #264 is open.