Minecraft渲染原理(1)

上回说到, 渲染一帧(包括绘制世界和绘制UI)的主要逻辑发生在 net/minecraft/client/render/GameRenderer.java 中. 而实际上, GameRenderer.java 又将渲染世界的任务分包给了同一目录下的 WorldRenderer.java.

GameRenderer.render 函数完成的主要工作罗列如下:

  • 处理暂停相关逻辑, 必要时将 MinecraftClient.currentScreen 设置成暂停界面
  • 根据屏幕尺寸设置视口
    824
    RenderSystem.viewport(0, 0, this.client.getWindow().getFramebufferWidth(), this.client.getWindow().getFramebufferHeight());
  • 渲染世界
    827
    this.renderWorld(tickDelta, startTime, new MatrixStack());
  • 将实体的轮廓线绘制到默认帧缓冲[1]
    829
    this.client.worldRenderer.drawEntityOutlinesFramebuffer();
  • 进行后处理[2]
    835
    this.shader.render(tickDelta);
  • 绑定 MinecraftClient.framebuffer 对应的 FBO
    837
    this.client.getFramebuffer().beginWrite(true);
  • 设置一番 RenderSystem 里的 MV 和 P 矩阵
    841
    842
    843
    844
    845
    846
    Matrix4f matrix4f = Matrix4f.projectionMatrix(0.0f, (float)((double)window.getFramebufferWidth() / window.getScaleFactor()), 0.0f, (float)((double)window.getFramebufferHeight() / window.getScaleFactor()), 1000.0f, 3000.0f);
    RenderSystem.setProjectionMatrix(matrix4f);
    MatrixStack matrixStack = RenderSystem.getModelViewStack();
    matrixStack.loadIdentity();
    matrixStack.translate(0.0, 0.0, -2000.0);
    RenderSystem.applyModelViewMatrix();
  • 设置渲染 GUI 所需的一些 uniform (?), 似乎是为了产生光照效果
    847
    DiffuseLighting.enableGuiDepthLighting();
  • 渲染反胃的特效(如果玩家处于相应状态的话)
    853
    this.renderNausea(f * (1.0f - this.client.options.distortionEffectScale));
  • 绘制悬浮项[3]和 HUD
    856
    857
    this.renderFloatingItem(this.client.getWindow().getScaledWidth(), this.client.getWindow().getScaledHeight(), tickDelta);
    this.client.inGameHud.render(matrixStack2, tickDelta);
  • 绘制叠加层[4]
    864
    this.client.getOverlay().render(matrixStack2, i, j, this.client.getLastFrameDuration());
  • 绘制 MinecraftClient.currentScreen
    875
    this.client.currentScreen.render(matrixStack2, i, j, this.client.getLastFrameDuration());

点进 currentScreen 所属的类 Screen 可以发现, 其 render 方法有十万个甚至九万个重写, 例如 BeaconScreen/BookScreen/DeathScreen 等类都继承了 Screen 类并重写了该方法, 令人望而生畏. 根据这些子类的名字可以判断出, 绘制 currentScreen 就是绘制游戏中的各种菜单和界面, 如信标选择效果的界面/打开书的界面/死亡重生界面等.

发光实体/后处理/反胃/不死图腾/叠加层等特效都不是我们所关心的, 只有 GameRenderer.renderWorld 函数里的东西才是热衷于烧显卡的程序员的热爱之物.

GameRenderer.renderWorld 干的事情如下:

  • GameRenderer.lightmapTextureManager 进行某种更新. 鉴于根本没有正经光影包会管游戏里自带的光照图, 我们可以当这件事情不存在.
  • 设置相机依附的实体, 以合理更新后处理着色器(比如说蜘蛛)
    978
    979
    980
    if (this.client.getCameraEntity() == null) {
    this.client.setCameraEntity(this.client.player);
    }
  • 更新十字叉丝瞄准的实体
    981
    this.updateTargetedEntity(tickDelta);
  • 进行了一堆神必的矩阵操作以获得 P 矩阵
    987
    988
    989
    990
    991
    992
    993
    994
    995
    996
    997
    998
    999
    1000
    1001
    1002
    1003
    1004
    1005
    MatrixStack matrixStack = new MatrixStack();
    double d = this.getFov(camera, tickDelta, true);
    matrixStack.peek().getPositionMatrix().multiply(this.getBasicProjectionMatrix(d));
    this.bobViewWhenHurt(matrixStack, tickDelta);
    if (this.client.options.bobView) {
    this.bobView(matrixStack, tickDelta);
    }
    if ((f = MathHelper.lerp(tickDelta, this.client.player.lastNauseaStrength, this.client.player.nextNauseaStrength) * (this.client.options.distortionEffectScale * this.client.options.distortionEffectScale)) > 0.0f) {
    int i = this.client.player.hasStatusEffect(StatusEffects.NAUSEA) ? 7 : 20;
    float g = 5.0f / (f * f + 5.0f) - f * 0.04f;
    g *= g;
    Vec3f vec3f = new Vec3f(0.0f, MathHelper.SQUARE_ROOT_OF_TWO / 2.0f, MathHelper.SQUARE_ROOT_OF_TWO / 2.0f);
    matrixStack.multiply(vec3f.getDegreesQuaternion(((float)this.ticks + tickDelta) * (float)i));
    matrixStack.scale(1.0f / g, 1.0f, 1.0f);
    float h = -((float)this.ticks + tickDelta) * (float)i;
    matrixStack.multiply(vec3f.getDegreesQuaternion(h));
    }
    Matrix4f matrix4f = matrixStack.peek().getPositionMatrix();
    this.loadProjectionMatrix(matrix4f);
  • 更新摄像机的状态, 如依附的实体/旋转角/位置等量, 并进行了一堆神必的矩阵操作将摄像机的位姿加入 MV 矩阵
    1006
    1007
    1008
    1009
    1010
    1011
    1012
    camera.update(this.client.world, this.client.getCameraEntity() == null ? this.client.player : this.client.getCameraEntity(), !this.client.options.getPerspective().isFirstPerson(), this.client.options.getPerspective().isFrontView(), tickDelta);
    matrices.multiply(Vec3f.POSITIVE_X.getDegreesQuaternion(camera.getPitch()));
    matrices.multiply(Vec3f.POSITIVE_Y.getDegreesQuaternion(camera.getYaw() + 180.0f));
    Matrix3f matrix3f = matrices.peek().getNormalMatrix().copy();
    if (matrix3f.invert()) {
    RenderSystem.setInverseViewRotationMatrix(matrix3f);
    }
  • 设置 WorldRenderer 的视锥体, 并进行渲染(要来力! (狂喜))
    1013
    1014
    this.client.worldRenderer.setupFrustum(matrices, camera.getPos(), this.getBasicProjectionMatrix(Math.max(d, this.client.options.fov)));
    this.client.worldRenderer.render(matrices, tickDelta, limitTime, bl, camera, this, this.lightmapTextureManager, matrix4f);
  • 绘制手(和手里拿着的物品)
    1016
    1017
    1018
    1019
    if (this.renderHand) {
    RenderSystem.clear(256, MinecraftClient.IS_SYSTEM_MAC);
    this.renderHand(matrices, camera, tickDelta);
    }

欲知 WorldRenderer.render(MatrixStack ...) 如何, 且听下回分解.


  1. 1.带有发光效果的实体,其特效就是通过这一步实现的.
  2. 2.例如观察者模式下的蜘蛛/末影人/苦力怕视角.
  3. 3.floatItem似乎只出现在不死图腾被触发的时刻,因此可以猜测其内容就是不死图腾的特效.
  4. 4.似乎只有一种overlay,即SplashOverlay,根据其引用的资源textures/gui/title/mojangstudios.png可以猜测是渲染启动游戏或重新载入时的红底白字界面.