Note: Before diving in, check out Part A: The Smoke Pillar for context.
This guide lays out the mathematical setup for creating a mushroom cloud. Let’s get swirling.
A mushroom cloud consists of two main parts:
This page is all about Part B; the swirly gas vortex thingy.
Alright, time to model the swirly gases. Here’s a diagram to visualize what we’re dealing with:
We use sine and cosine to calculate how much each particle moves. And since describing math with words alone is painful, here’s more diagrams. Hooray!
By shifting a particle by (sin(angle), cos(angle)) as angle increases, we get a loop. At first, it looks more like a square than a circle, but don’t panic, we can just decrease the angle increment. Also, if you’re wondering why X is offset by sin and Y by cos (instead of the usual way), it’s so the particles move outward and then back in, meeting at a single point (which we can later tweak into a torus!).
Here’s what different increment values look like. Each set has one less visible point than labeled due to the 0→2π loop. More points = smoother circle.
In Minecraft, if you call super.tick()
before displacement, it smooths out movement, so even Set 2 will look like it has way more than 10 points.
Also, the more points you add, the bigger the circle gets. This happens because P' = P + (sin(angle), cos(angle)), and a smaller increment moves P' farther.
You could "fix" this with ratios or something fancy, but honestly, just test it in-game. Too big? Divide the displacement value. Too small? Multiply it.
Math is constant, but art is a process. You’ll have to tweak things. I highly recommend adding dev commands to adjust radius values live in-game, it'll save you from the dreaded
'Close Game -> Change Variable -> Open Game -> Test -> Repeat'.
Now this is a lot of confusing math, so if you learn by example or by good ol' copy-paste, here you go:
public void partB() {
swirlAngle += swirlIncrement;
if (swirlAngle >= 2 * Math.PI) swirlAngle -= 2 * Math.PI;
float rStart, rEnd, gStart, gEnd, bStart, bEnd;
if (swirlAngle < Math.PI) {
rStart = 255; rEnd = 115;
gStart = 75; gEnd = 115;
bStart = 0; bEnd = 115;
} else {
rStart = 125; rEnd = 255;
gStart = 115; gEnd = 75;
bStart = 115; bEnd = 0;
}
float progress = (float) ((swirlAngle % Math.PI) / Math.PI);
this.rCol = Math2.interpolateColor(rStart,rEnd,progress);
this.gCol = Math2.interpolateColor(gStart,gEnd,progress);
this.bCol = Math2.interpolateColor(bStart,bEnd,progress);
double verticalMovement = Math.cos(swirlAngle) * swirlRadius;
double horizontalMovement = Math.sin(swirlAngle) * swirlRadius * 1.5;
localY += verticalMovement;
localZ += horizontalMovement;
double relativeX = localX - originX;
double relativeY = localY - originY;
double relativeZ = localZ - originZ;
Vector2D rotatedXZ = Math2.rotateCoordinate2D(relativeX, relativeZ, rotationAngle).offset(offsetX, offsetZ).offset(originX, originZ);
this.setPos(rotatedXZ.x, originY + relativeY, rotatedXZ.y);
}