引入
在世界边境附近调试Mod实体时,偶然发现了这样的问题(图中激光渲染的位置出现异常偏移)
这是怎么回事呢?
常用技巧
在Minecraft的Mod开发中,开发者们可能常使用以下的代码辅助世界的渲染:
Vector3d projectedView = Minecraft.getInstance().gameRenderer.getMainCamera().getPosition();
matrixStack.translate(-projectedView.x, -projectedView.y, -projectedView.z);
这样做的好处显而易见——可以在渲染中用绝对坐标替换相对坐标。
不过使用这样的方式,有时也会导致一些问题,先从float和double的有效数字分析
Java中的浮点数
下表为float和double的一些属性比较
属性 | float | double |
---|---|---|
符号位/bit | 1 | 1 |
指数位/bit | 8 | 11 |
尾数位/bit | 23 | 52 |
占用内存空间 | 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);
最后效果如下图所示
激光渲染的位置的异常偏移消失了!
总结
- float不精确,开发中无性能需求应尽量使用double(虽然也不是100%精确)
- Minecraft的世界坐标(x和z)的绝对值上限为30000000而不能无限增大,一定程度上与double的精度限制有关(毕竟每个世界坐标都是double的,但主要还是因为int能表示的范围有限)
- 原版下实体的生命上限为1024,这也在一定程度上与float的精度限制有关