引入

在世界边境附近调试Mod实体时,偶然发现了这样的问题(图中激光渲染的位置出现异常偏移)

这是怎么回事呢?

常用技巧

在Minecraft的Mod开发中,开发者们可能常使用以下的代码辅助世界的渲染:

Vector3d projectedView = Minecraft.getInstance().gameRenderer.getMainCamera().getPosition();
matrixStack.translate(-projectedView.x, -projectedView.y, -projectedView.z);

这样做的好处显而易见——可以在渲染中用绝对坐标替换相对坐标。
不过使用这样的方式,有时也会导致一些问题,先从float和double的有效数字分析

Java中的浮点数

下表为float和double的一些属性比较

属性floatdouble
符号位/bit11
指数位/bit811
尾数位/bit2352
占用内存空间4 Byte (32bit)8 Byte (64bit)

由此可知float的有效数字位数为6~7位,而double则为15~16位

float的局限性

MatrixStack的translate方法

以下是MatrixStack类中translate方法的源代码

public void translate(double x, double y, double z) {
    MatrixStack.Entry entry = this.poseStack.getLast();
    entry.pose.multiply(Matrix4f.createTranslateMatrix((float) x, (float) y, (float) z));
}

可以发现,此方法内做了一个强制类型转换,将双精度的double转换为了单精度的float!

世界的大小

在World(SRG名,1.16.5)类中,有如下的私有静态方法

private static boolean isInWorldBoundsHorizontal(BlockPos pos) {
    return pos.getX() >= -30000000 && pos.getZ() >= -30000000 && pos.getX() < 30000000 && pos.getZ() < 30000000;
}

由此可知,MC的世界是60000001x60000001格的,这会使得当坐标足够大时,用float表示坐标会使float的小数部分丢失,从而发现了问题所在

问题发生的原因

在未解决bug时,由于直接在translate中传入世界真实坐标,导致强制类型转换,进而导致double后面的小数部分丢失

问题的解决

translate时,传入激光起始位置的坐标与玩家坐标的差,使数字变小,以确保float的小数部分保留

Vector3d cameraPos = Minecraft.getInstance().gameRenderer.getMainCamera().getPosition();
double x = src.x - cameraPos.x;
double y = src.y - cameraPos.y;
double z = src.z - cameraPos.z;
matrixStack.translate(x, y, z);

最后效果如下图所示

激光渲染的位置的异常偏移消失了!

总结

  1. float不精确,开发中无性能需求应尽量使用double(虽然也不是100%精确)
  2. Minecraft的世界坐标(x和z)的绝对值上限为30000000而不能无限增大,一定程度上与double的精度限制有关(毕竟每个世界坐标都是double的,但主要还是因为int能表示的范围有限)
  3. 原版下实体的生命上限为1024,这也在一定程度上与float的精度限制有关