Skip to content

Commit

Permalink
Avoid quaternion transforms in particle rendering
Browse files Browse the repository at this point in the history
The billboard geometry can be computed using the
camera's left and up vectors, saving some cycles.

When rendering thousands of billboard particles, this
was ~10% faster than baseline in my observation.

Co-authored-by: MoePus <[email protected]>
  • Loading branch information
jellysquid3 and MoePus committed Jan 2, 2025
1 parent 7f25220 commit 1b0f7b9
Showing 1 changed file with 87 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package net.caffeinemc.mods.sodium.mixin.features.render.particle;

import net.caffeinemc.mods.sodium.api.vertex.format.common.ParticleVertex;
import com.mojang.blaze3d.vertex.VertexConsumer;
import net.caffeinemc.mods.sodium.api.util.ColorABGR;
import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter;
import net.caffeinemc.mods.sodium.api.vertex.format.common.ParticleVertex;
import net.caffeinemc.mods.sodium.client.render.vertex.VertexConsumerUtils;
import net.minecraft.client.Camera;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.particle.Particle;
import net.minecraft.client.particle.SingleQuadParticle;
import net.minecraft.util.Mth;
import net.minecraft.world.phys.Vec3;
import org.joml.Math;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import org.lwjgl.system.MemoryStack;
Expand Down Expand Up @@ -38,12 +43,63 @@ protected SingleQuadParticleMixin(ClientLevel level, double x, double y, double
super(level, x, y, z);
}

@Unique
private static final Vector3f TEMP_LEFT = new Vector3f();

@Unique
private static final Vector3f TEMP_UP = new Vector3f();

/**
* @reason Build vertex data using the left and up vectors to avoid quaternion calculations
* @author MoePus
*/
@Inject(method = "render(Lcom/mojang/blaze3d/vertex/VertexConsumer;Lnet/minecraft/client/Camera;F)V", at = @At("HEAD"), cancellable = true)
protected void render(VertexConsumer vertexConsumer, Camera camera, float tickDelta, CallbackInfo ci) {
final var writer = VertexConsumerUtils.convertOrLog(vertexConsumer);

if (writer == null) {
return;
}

ci.cancel();

float size = this.getQuadSize(tickDelta);

Vector3f left = TEMP_LEFT;
left.set(camera.getLeftVector())
.mul(size);

Vector3f up = TEMP_UP;
up.set(camera.getUpVector())
.mul(size);

if (!Mth.equal(this.roll, 0.0f)) {
float roll = Mth.lerp(tickDelta, this.oRoll, this.roll);

float sinRoll = Math.sin(roll);
float cosRoll = Math.cosFromSin(sinRoll, roll);

float rv1x = Math.fma(cosRoll, left.x, sinRoll * up.x),
rv1y = Math.fma(cosRoll, left.y, sinRoll * up.y),
rv1z = Math.fma(cosRoll, left.z, sinRoll * up.z);

float rv2x = Math.fma(-sinRoll, left.x, cosRoll * up.x),
rv2y = Math.fma(-sinRoll, left.y, cosRoll * up.y),
rv2z = Math.fma(-sinRoll, left.z, cosRoll * up.z);

left.set(rv1x, rv1y, rv1z);
up.set(rv2x, rv2y, rv2z);
}

this.sodium$emitVertices(writer, camera.getPosition(), left, up, tickDelta);
}

/**
* @reason Optimize function
* @author JellySquid
* @reason Build vertex data using the left and up vectors to avoid quaternion calculations
* @author MoePus
*/
@Inject(method = "renderRotatedQuad(Lcom/mojang/blaze3d/vertex/VertexConsumer;Lorg/joml/Quaternionf;FFFF)V", at = @At("HEAD"), cancellable = true)
protected void renderRotatedQuad(VertexConsumer vertexConsumer, Quaternionf quaternionf, float x, float y, float z, float tickDelta, CallbackInfo ci) {
@Inject(method = "renderRotatedQuad(Lcom/mojang/blaze3d/vertex/VertexConsumer;Lnet/minecraft/client/Camera;Lorg/joml/Quaternionf;F)V", at = @At("HEAD"), cancellable = true)
protected void renderRotatedQuad(VertexConsumer vertexConsumer, Camera camera, Quaternionf quaternion, float tickDelta, CallbackInfo ci) {
final var writer = VertexConsumerUtils.convertOrLog(vertexConsumer);

if (writer == null) {
Expand All @@ -53,45 +109,52 @@ protected void renderRotatedQuad(VertexConsumer vertexConsumer, Quaternionf quat
ci.cancel();

float size = this.getQuadSize(tickDelta);

// Some particle class implementations may call this function directly, in which case we cannot assume anything
// about the transform being used. However, we can still extract the left/up vectors from the quaternion,
// it's just slightly slower than using the camera's left/up vectors directly.
Vector3f left = TEMP_LEFT;
left.set(-size, 0.0f, 0.0f)
.rotate(quaternion);

Vector3f up = TEMP_UP;
up.set(0.0f, size, 0.0f)
.rotate(quaternion);

this.sodium$emitVertices(writer, camera.getPosition(), left, up, tickDelta);
}

@Unique
private void sodium$emitVertices(VertexBufferWriter writer, Vec3 camera, Vector3f left, Vector3f up, float tickDelta) {
float minU = this.getU0();
float maxU = this.getU1();
float minV = this.getV0();
float maxV = this.getV1();
int light = this.getLightColor(tickDelta);

int light = this.getLightColor(tickDelta);
int color = ColorABGR.pack(this.rCol, this.gCol, this.bCol, this.alpha);

float x = (float) (Mth.lerp(tickDelta, this.xo, this.x) - camera.x());
float y = (float) (Mth.lerp(tickDelta, this.yo, this.y) - camera.y());
float z = (float) (Mth.lerp(tickDelta, this.zo, this.z) - camera.z());

try (MemoryStack stack = MemoryStack.stackPush()) {
long buffer = stack.nmalloc(4 * ParticleVertex.STRIDE);
long ptr = buffer;

this.sodium$writeVertex(ptr, quaternionf, x, y, z, 1.0F, -1.0F, size, maxU, maxV, color, light);
ParticleVertex.put(ptr, -left.x - up.x + x, -left.y - up.y + y, -left.z - up.z + z, maxU, maxV, color, light);
ptr += ParticleVertex.STRIDE;

this.sodium$writeVertex(ptr, quaternionf, x, y, z, 1.0F, 1.0F, size, maxU, minV, color, light);
ParticleVertex.put(ptr, -left.x + up.x + x, -left.y + up.y + y, -left.z + up.z + z, maxU, minV, color, light);
ptr += ParticleVertex.STRIDE;

this.sodium$writeVertex(ptr, quaternionf, x, y, z, -1.0F, 1.0F, size, minU, minV, color, light);
ParticleVertex.put(ptr, left.x + up.x + x, left.y + up.y + y, left.z + up.z + z, minU, minV, color, light);
ptr += ParticleVertex.STRIDE;

this.sodium$writeVertex(ptr, quaternionf, x, y, z, -1.0F, -1.0F, size, minU, maxV, color, light);
ParticleVertex.put(ptr, left.x - up.x + x, left.y - up.y + y, left.z - up.z + z, minU, maxV, color, light);
ptr += ParticleVertex.STRIDE;

writer.push(stack, buffer, 4, ParticleVertex.FORMAT);
}
}

@Unique
private final Vector3f sodium$scratchVertex = new Vector3f(); // not thread-safe

@Unique
private void sodium$writeVertex(long ptr, Quaternionf quaternionf, float originX, float originY, float originZ, float posX, float posY, float size, float u, float v, int color, int light) {
final var vertex = this.sodium$scratchVertex;
vertex.set(posX, posY, 0.0f);
vertex.rotate(quaternionf);
vertex.mul(size);
vertex.add(originX, originY, originZ);

ParticleVertex.put(ptr, vertex.x(), vertex.y(), vertex.z(), u, v, color, light);
}
}

0 comments on commit 1b0f7b9

Please sign in to comment.