上回说到, 对世界的渲染被分包到了 WorldRenderer.render(MatrixStack ...)
这个函数. 这是一个将近 300 行的巨大函数, 再次展现了 ojng 招聘的员工素质.
net/minecraft/client/render/WorldRenderer.java:992
992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005
|
public void render(MatrixStack matrices, float tickDelta, long limitTime, boolean renderBlockOutline, Camera camera, GameRenderer gameRenderer, LightmapTextureManager lightmapTextureManager, Matrix4f positionMatrix) { int l; BlockPos blockPos; Frustum frustum; boolean bl2; RenderSystem.setShaderGameTime(this.world.getTime(), tickDelta); this.blockEntityRenderDispatcher.configure(this.world, camera, this.client.crosshairTarget); this.entityRenderDispatcher.configure(this.world, camera, this.client.targetedEntity); Profiler profiler = this.world.getProfiler(); profiler.swap("light_update_queue"); this.world.runQueuedChunkUpdates(); profiler.swap("light_updates"); boolean bl = this.world.hasNoChunkUpdaters(); this.world.getChunkManager().getLightingProvider().doLightUpdates(Integer.MAX_VALUE, bl, true);
|
992 行至 1005 行设置了 RenderSystem 内部的 shaderGameTime
值, 以备之后作为 uniform 上传给着色器; 对方块实体和实体渲染派发器进行了一些设置, 暂且略去不谈; 进行了一些光照更新, 这种更新只和游戏机制有关, 也不是我们所关心的.
net/minecraft/client/render/WorldRenderer.java:1012
1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023
|
boolean bl3 = bl2 = this.capturedFrustum != null; if (bl2) { frustum = this.capturedFrustum; frustum.setPosition(this.capturedFrustumPosition.x, this.capturedFrustumPosition.y, this.capturedFrustumPosition.z); } else { frustum = this.frustum; } this.client.getProfiler().swap("captureFrustum"); if (this.shouldCaptureFrustum) { this.captureFrustum(matrix4f, positionMatrix, vec3d.x, vec3d.y, vec3d.z, bl2 ? new Frustum(matrix4f, positionMatrix) : frustum); this.shouldCaptureFrustum = false; }
|
1012 至 1023 行是某些关于调试视锥体的代码. 一般执行的分支是 1017 行的, 对正常的执行流毫无影响.
net/minecraft/client/render/WorldRenderer.java:1025
1025 1026 1027 1028 1029 1030 1031 1032 1033 1034
|
BackgroundRenderer.render(camera, tickDelta, this.client.world, this.client.options.getViewDistance(), gameRenderer.getSkyDarkness(tickDelta)); BackgroundRenderer.setFogBlack(); RenderSystem.clear(16640, MinecraftClient.IS_SYSTEM_MAC); float g = gameRenderer.getViewDistance(); boolean bl32 = this.client.world.getDimensionEffects().useThickFog(MathHelper.floor(d), MathHelper.floor(e)) || this.client.inGameHud.getBossBarHud().shouldThickenFog(); profiler.swap("sky"); RenderSystem.setShader(GameRenderer::getPositionShader); this.renderSky(matrices, positionMatrix, tickDelta, camera, bl32, () -> BackgroundRenderer.applyFog(camera, BackgroundRenderer.FogType.FOG_SKY, g, bl32)); profiler.swap("fog"); BackgroundRenderer.applyFog(camera, BackgroundRenderer.FogType.FOG_TERRAIN, Math.max(g, 32.0f), bl32);
|
1025 至 1034 行是关于绘制天空和处理背景/雾效的代码. 鉴于正经光影包既不会用到原版天空也不会用到原版雾效, 我们也不关心这些代码的行为, 但作为我们碰到的第一段实际进行世界中物体的渲染的代码, 我们可以简单研究一下 renderSky
函数, 来挖掘 ojng 程序员惯用的编码模式.
net/minecraft/client/render/WorldRenderer.java
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
|
public WorldRenderer(MinecraftClient client, BufferBuilderStorage bufferBuilders) { this.client = client; this.entityRenderDispatcher = client.getEntityRenderDispatcher(); this.blockEntityRenderDispatcher = client.getBlockEntityRenderDispatcher(); this.bufferBuilders = bufferBuilders; for (int i = 0; i < 32; ++i) { for (int j = 0; j < 32; ++j) { float f = j - 16; float g = i - 16; float h = MathHelper.sqrt(f * f + g * g); this.field_20794[i << 5 | j] = -g / h; this.field_20795[i << 5 | j] = f / h; } } this.renderStars(); this.renderLightSky(); this.renderDarkSky(); }
|
以上是 WorldRenderer
的构造函数. 考虑到构造函数只执行一次, 而熟悉 OpenGL 的同学可能会认为渲染相关的逻辑需要每帧都执行, 函数体末尾三个 render*()
值得解释一下其作用. 以 renderStars()
函数为例:
net/minecraft/client/render/WorldRenderer.java
577 578 579 580 581 582 583 584 585 586 587 588
|
private void renderStars() { Tessellator tessellator = Tessellator.getInstance(); BufferBuilder bufferBuilder = tessellator.getBuffer(); RenderSystem.setShader(GameRenderer::getPositionShader); if (this.starsBuffer != null) { this.starsBuffer.close(); } this.starsBuffer = new VertexBuffer(); this.renderStars(bufferBuilder); bufferBuilder.end(); this.starsBuffer.upload(bufferBuilder); }
|
容易看出, 这一函数的实际作用是填充了 this.starsBuffer
背后的顶点缓冲的内容.
通过搜索 starsBuffer
的用法, 可以发现正是 public void renderSky(MatrixStack...)
函数执行了星星和天空的绘制:
net/minecraft/client/render/WorldRenderer.java:1630
1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659
|
this.lightSkyBuffer.setShader(matrices.peek().getPositionMatrix(), projectionMatrix, shader); RenderSystem.enableBlend(); RenderSystem.defaultBlendFunc(); float[] fs = this.world.getDimensionEffects().getFogColorOverride(this.world.getSkyAngle(tickDelta), tickDelta); if (fs != null) { RenderSystem.setShader(GameRenderer::getPositionColorShader); RenderSystem.disableTexture(); RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); matrices.push(); matrices.multiply(Vec3f.POSITIVE_X.getDegreesQuaternion(90.0f)); i = MathHelper.sin(this.world.getSkyAngleRadians(tickDelta)) < 0.0f ? 180.0f : 0.0f; matrices.multiply(Vec3f.POSITIVE_Z.getDegreesQuaternion(i)); matrices.multiply(Vec3f.POSITIVE_Z.getDegreesQuaternion(90.0f)); float j = fs[0]; k = fs[1]; float l = fs[2]; Matrix4f matrix4f = matrices.peek().getPositionMatrix(); bufferBuilder.begin(VertexFormat.DrawMode.TRIANGLE_FAN, VertexFormats.POSITION_COLOR); bufferBuilder.vertex(matrix4f, 0.0f, 100.0f, 0.0f).color(j, k, l, fs[3]).next(); m = 16; for (int n = 0; n <= 16; ++n) { o = (float)n * ((float)Math.PI * 2) / 16.0f; p = MathHelper.sin(o); q = MathHelper.cos(o); bufferBuilder.vertex(matrix4f, p * 120.0f, q * 120.0f, -q * 40.0f * fs[3]).color(fs[0], fs[1], fs[2], 0.0f).next(); } bufferBuilder.end(); BufferRenderer.draw(bufferBuilder); matrices.pop(); }
|
1630 行的
this.lightSkyBuffer.setShader(...)
看似只是进行了类似于
glUseProgram
的操作, 但实际上同时进行了绘制(
glDrawElements
), 惊不惊喜意不意外?
yarn 有的方法命名还是比较有问题的 在后面的
getFogColorOverride
返回非空的东西时
[1], ojng 当场造了个 buffer, 用 BufferRender 来现场渲染.
要复用这种不太变的 buffer 就坚持到底嘛.
此处还出现了一个在源码中很常见的类: BufferBuilder. 参考 private void renderStars(BufferBuilder buffer)
函数, 可以得到 BufferBuilder 的主要使用模式:
BufferBuilder usage pattern
1 2 3 4 5 6 7 8 9 10 11 12 13
| BufferBuilder bufferBuilder = new BufferBuilder(); VertexBuffer vertexBuffer = new VertexBuffer();
bufferBuilder.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION); buffer.vertex(x, y, z).next(); ... buffer.end(); RenderSystem.setShader(GameRenderer::getXXXShader);
vertexBuffer.upload(buffer); vertexBuffer.setShader(MV, P, RenderSystem.getShader())
BufferRenderer.draw(bufferBuilder);
|
BufferBuilder 类本质上是对 ByteBuffer 的一个包装, 同时也是多个顶点数据和相关绘制参数构成的栈.[2]
bufferBuilder.begin()
可以指定绘制模式和顶点格式, 并将 bufferBuilder 置于准备接受新一个几何体的顶点数据的状态.
bufferBuilder.vertex(x, y, z)
会将 x,y,z 的值按顺序写入内置的 ByteBuffer, 并将内部状态设置为对下一个元素写入
bufferBuilder.next()
则会告知 bufferBuilder 完成对当前顶点的写入, 适当调整内部 ByteBuffer 的容量, 并准备好写入下一个顶点.
bufferBuilder.end()
则结束对当前顶点数据的收集, 根据一定条件补充索引数据(即供 OpenGL 使用的 index buffer), 并将收集到的绘制参数(例如顶点数、绘制模式、顶点格式等)打包, 压入 bufferBuilder.parameters
中, 随后重置 bufferBuilder 的状态.
vertexBuffer.upload(bufferBuilder)
则会弹出栈顶的数据, 并上传到其对应的 OpenGL 顶点缓冲区中.
bufferBuilder
在某些条件下还会将几何面按照离摄像机从近到远的顺序排序, 这是为了充分利用显卡 early-z 的特性, 减少过度绘制.
绘制模式(DrawMode)和顶点格式(VertexFormat)的详细说明
绘制模式是顶点构成图元(Primitive)的方式. 例如, VertexFormat.DrawMode.QUADS
就是一种用于绘制四边形的绘制模式; 顶点流中相邻的 4 个顶点被分为一组(即一个图元), 构成一个 QUAD, 其中 (0,1,2), (2,3,0) 号顶点用于绘制两个三角形, 这两个邻接的三角形构成了期望得到的四边形. 这样的绘制模式的好处在于能节约顶点缓冲区中重复的顶点, 相比 VertexFormat.DrawMode.TRIANGLE
(即相邻 3 个顶点构成一个三角形的绘制模式) 节约了 1/3 的空间. 除此之外, 还有 TRIANGLE_FANS/TRIANGLE_STRIP 等绘制模式可用.
顶点格式就是对每个顶点包含的数据的布局的描述, 比如说如下的代码中
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
public class VertexFormats { public static final VertexFormatElement POSITION_ELEMENT = new VertexFormatElement(0, VertexFormatElement.DataType.FLOAT, VertexFormatElement.Type.POSITION, 3); public static VertexFormatElement COLOR_ELEMENT = new VertexFormatElement(0, VertexFormatElement.DataType.UBYTE, VertexFormatElement.Type.COLOR, 4); public static final VertexFormatElement TEXTURE_0_ELEMENT = new VertexFormatElement(0, VertexFormatElement.DataType.FLOAT, VertexFormatElement.Type.UV, 2); public static final VertexFormatElement OVERLAY_ELEMENT = new VertexFormatElement(1, VertexFormatElement.DataType.SHORT, VertexFormatElement.Type.UV, 2); public static final VertexFormatElement LIGHT_ELEMENT = new VertexFormatElement(2, VertexFormatElement.DataType.SHORT, VertexFormatElement.Type.UV, 2); public static final VertexFormatElement NORMAL_ELEMENT = new VertexFormatElement(0, VertexFormatElement.DataType.BYTE, VertexFormatElement.Type.NORMAL, 3); public static final VertexFormatElement PADDING_ELEMENT = new VertexFormatElement(0, VertexFormatElement.DataType.BYTE, VertexFormatElement.Type.PADDING, 1); public static final VertexFormatElement TEXTURE_ELEMENT = TEXTURE_0_ELEMENT; public static final VertexFormat BLIT_SCREEN = new VertexFormat(ImmutableMap.builder().put("Position", POSITION_ELEMENT).put("UV", TEXTURE_ELEMENT).put("Color", COLOR_ELEMENT).build()); public static final VertexFormat POSITION_COLOR_TEXTURE_LIGHT_NORMAL = new VertexFormat(ImmutableMap.builder().put("Position", POSITION_ELEMENT).put("Color", COLOR_ELEMENT).put("UV0", TEXTURE_0_ELEMENT).put("UV2", LIGHT_ELEMENT).put("Normal", NORMAL_ELEMENT).put("Padding", PADDING_ELEMENT).build()); public static final VertexFormat POSITION_COLOR_TEXTURE_OVERLAY_LIGHT_NORMAL = new VertexFormat(ImmutableMap.builder().put("Position", POSITION_ELEMENT).put("Color", COLOR_ELEMENT).put("UV0", TEXTURE_0_ELEMENT).put("UV1", OVERLAY_ELEMENT).put("UV2", LIGHT_ELEMENT).put("Normal", NORMAL_ELEMENT).put("Padding", PADDING_ELEMENT).build()); public static final VertexFormat POSITION_TEXTURE_COLOR_LIGHT = new VertexFormat(ImmutableMap.builder().put("Position", POSITION_ELEMENT).put("UV0", TEXTURE_0_ELEMENT).put("Color", COLOR_ELEMENT).put("UV2", LIGHT_ELEMENT).build()); public static final VertexFormat POSITION = new VertexFormat(ImmutableMap.builder().put("Position", POSITION_ELEMENT).build()); public static final VertexFormat POSITION_COLOR = new VertexFormat(ImmutableMap.builder().put("Position", POSITION_ELEMENT).put("Color", COLOR_ELEMENT).build()); public static final VertexFormat LINES = new VertexFormat(ImmutableMap.builder().put("Position", POSITION_ELEMENT).put("Color", COLOR_ELEMENT).put("Normal", NORMAL_ELEMENT).put("Padding", PADDING_ELEMENT).build()); public static final VertexFormat POSITION_COLOR_LIGHT = new VertexFormat(ImmutableMap.builder().put("Position", POSITION_ELEMENT).put("Color", COLOR_ELEMENT).put("UV2", LIGHT_ELEMENT).build()); public static final VertexFormat POSITION_TEXTURE = new VertexFormat(ImmutableMap.builder().put("Position", POSITION_ELEMENT).put("UV0", TEXTURE_0_ELEMENT).build()); public static final VertexFormat POSITION_COLOR_TEXTURE = new VertexFormat(ImmutableMap.builder().put("Position", POSITION_ELEMENT).put("Color", COLOR_ELEMENT).put("UV0", TEXTURE_0_ELEMENT).build()); public static final VertexFormat POSITION_TEXTURE_COLOR = new VertexFormat(ImmutableMap.builder().put("Position", POSITION_ELEMENT).put("UV0", TEXTURE_0_ELEMENT).put("Color", COLOR_ELEMENT).build()); public static final VertexFormat POSITION_COLOR_TEXTURE_LIGHT = new VertexFormat(ImmutableMap.builder().put("Position", POSITION_ELEMENT).put("Color", COLOR_ELEMENT).put("UV0", TEXTURE_0_ELEMENT).put("UV2", LIGHT_ELEMENT).build()); public static final VertexFormat POSITION_TEXTURE_LIGHT_COLOR = new VertexFormat(ImmutableMap.builder().put("Position", POSITION_ELEMENT).put("UV0", TEXTURE_0_ELEMENT).put("UV2", LIGHT_ELEMENT).put("Color", COLOR_ELEMENT).build()); public static final VertexFormat POSITION_TEXTURE_COLOR_NORMAL = new VertexFormat(ImmutableMap.builder().put("Position", POSITION_ELEMENT).put("UV0", TEXTURE_0_ELEMENT).put("Color", COLOR_ELEMENT).put("Normal", NORMAL_ELEMENT).put("Padding", PADDING_ELEMENT).build()); }
|
VertexFormats.POSITION_ELEMENT
表示由 3 个 FLOAT 构成的元素, 用于传递坐标值; VertexFormats.COLOR_ELEMENT
表示由 4 个 UBYTE 构成的元素, 用于传递顶点色. VertexFormats.POSITION
表示一种只包含坐标值的顶点格式, 坐标值绑定到 Position
上, 对应于以下着色器声明: 1
| layout(location = 0) in vec3 Position;
|
VertexFormats.POSITION_COLOR
表示既包含坐标元素又包含顶点色元素的顶点格式, 分别绑定到 Position
和 Color
上, 对应于以下着色器声明: 1 2
| layout(location = 0) in vec3 Position; layout(location = 1) in vec4 Color;
|
回到对 WorldRenderer.render
的分析上来.
net/minecraft/client/render/WorldRenderer.java:1035
1035 1036 1037 1038
|
profiler.swap("terrain_setup"); this.setupTerrain(camera, frustum, bl2, this.client.player.isSpectator()); profiler.swap("compilechunks"); this.updateChunks(camera);
|
1036 和 1038 行的代码是我们啃屎山的开始.
屎山: setupTerrain
WorldRenderer.render
中的第一座屎山叫 setupTerrain
. 建议读者直接跳转到本节末尾阅读结论.
net/minecraft/client/render/WorldRenderer.java:763
763 764 765 766 767 768 769 770 771 772
|
if (this.cameraChunkX != i || this.cameraChunkY != j || this.cameraChunkZ != k) { this.lastCameraChunkUpdateX = d; this.lastCameraChunkUpdateY = e; this.lastCameraChunkUpdateZ = f; this.cameraChunkX = i; this.cameraChunkY = j; this.cameraChunkZ = k; this.chunks.updateCameraPosition(d, f); } this.chunkBuilder.setCameraPosition(vec3d);
|
setupTerrain
函数首先检查了相比上一帧, 摄影机是否移动到了另一个 ChunkSection
. ChunkSection
就是 大小的那个被一般玩家叫做区块的东西, 但 ojng 叫它 ChunkSection
. 如果发生了移动, 则调用 BuiltChunkStorage.updateCameraPosition(x, z)
来对部分区块设置其 origin
成员(仿佛结果是 x
和 z
的某种阶梯函数):[3]
net/minecraft/client/render/BuiltChunkStorage.java:68
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
|
public void updateCameraPosition(double x, double z) { int i = MathHelper.ceil(x); int j = MathHelper.ceil(z); for (int k = 0; k < this.sizeX; ++k) { int l = this.sizeX * 16; int m = i - 8 - l / 2; int n = m + Math.floorMod(k * 16 - m, l); for (int o = 0; o < this.sizeZ; ++o) { int p = this.sizeZ * 16; int q = j - 8 - p / 2; int r = q + Math.floorMod(o * 16 - q, p); for (int s = 0; s < this.sizeY; ++s) { int t = this.world.getBottomY() + s * 16; ChunkBuilder.BuiltChunk builtChunk = this.chunks[this.getChunkIndex(k, s, o)]; BlockPos blockPos = builtChunk.getOrigin(); if (n == blockPos.getX() && t == blockPos.getY() && r == blockPos.getZ()) continue; builtChunk.setOrigin(n, t, r); } } } }
|
随后通过调用
ChunkBuilder.setCameraPosition()
告知
chunkBuilder
摄像机的新位置. 这是为了帮助 chunkBuilder 进行诸如将区块按离摄像机的距离来排序的操作.
此函数中涉及到了多个 yarn 中未被赋予名字的变量和方法, 不妨在此处给出其 mojang 名以供参考:
WorldRenderer |
- |
net.minecraft.client.renderer.LevelRenderer |
ChunkInfoList |
- |
LevelRendererRenderChunkStorage |
field_34808 |
Future |
lastFullRenderChunkUpdate |
field_34809 |
AtomicBoolean |
needsFrustumUpdate |
field_34810 |
boolean |
needsFullRenderChunkUpdate |
field_34811 |
AtomicLong |
nextFullUpdateMillis |
field_34817 |
AtomicReference |
renderChunkStorage |
field_34818 |
WorldRenderer.ChunkInfoList |
renderInfoMap |
field_34819 |
LinkedHashSet |
renderChunks |
method_34808 |
void method_34808(LinkedHashSet, ChunkInfoList, Vec3d, Queue, boolean); |
updateRenderChunks |
method_38549 |
private void method_38549(Camera, Queue); |
initializeQueueForFullUpdate |
如果相机相比上一帧移到了不同的 空间, 或者 needsFullRenderChunkUpdate
标记被设置时, 则进行一些与区块遮挡剔除相关的操作. 如果没有因为调试而固定使用某个此前捕获的视锥体来进行剔除 (!hasForcedFrustum
), 且 lastFullRenderChunkUpdate
为空或者不是正在进行的, 则将以下操作提交到 MAIN_WORKER_EXECUTOR
线程上执行:
- 初始化某个广度优先的 FIFO 队列, 即方法
initializeQueueForFullUpdate
的内容: 如果当前摄像机所在区块已经被渲染(可能需要换个词, 反正就是 this.chunks.getRenderedChunk(blockPos)!=null
), 则将当前区块对应的 ChunkInfo
加入队列以作为广度优先的开始; 否则将视距内某个高度上所有已渲染的区块对应的 ChunkInfo
按从近到远的顺序加入该队列 801 802
| ArrayDeque<ChunkInfo> queue = Queues.newArrayDeque(); this.method_38549(camera, queue);
|
- 进行某种奇怪的广度优先操作, 即方法
updateRenderChunks
的内容: 从队列中取出一个 ChunkInfo
, 将其放入 field_34817.get().field_34819
, 并对其 6 个方向上邻接的区块进行某些神必的判断(可能是为了剔除掉例如 6 个方向上都被遮挡的区块), 更新其剔除状态(updateCullingState
), 并计算其 propagationLevel
以便于调试的可视化; 最后在某些情况下将邻接区块放回队列, 并更新 field_34817.get().field_34818
对于该区块的值; 有时还把 WorldRenderer.field_34811
设置到 500 毫秒以后, 以规划一次完整的遮挡剔除运算 803 804
| class_6600 lv = new class_6600(this.chunks.chunks.length); this.method_34808(lv.field_34819, lv.field_34818, vec3d, queue, bl2);
|
- 最后将以上结果填充到
field_34817
中, 并设置 field_34809
以表明有新的剔除结果可用.
提交以上任务后, 渲染线程立即调用 field_34817.get()
, 以得到此前已有的任务结果. 如果 builtChunks
非空, 则从 builtChunks
选取 field_34817.get().field_34818
中存在的区块加入到另一个队列中, 然后再重复 method_34808
的操作.
最后, 在转动视角或有新鲜的剔除结果(field_34809
被设置)时, 通过调用 WorldRenderer.applyFrustum()
, 将 field_34817
内所有包围盒与视锥相交或者在视锥体内的区块加入 WorldRenderer.chunkInfos
中.
总的来看, setupTerrain
函数的主要功能是对区块进行遮挡剔除和视锥剔除; 如此复杂的函数笔者不想用心读, 写出来的东西读者也不忍心看, 想必 ojng 的程序员也不想用心维护, 难免有几个 bug .[4]
屎山: updateChunks
上一座屎山 setupTerrain
函数过滤过的区块被加入了 WorldRenderer.chunkInfos
, 因此这座屎山就继续对 WorldRenderer.chunkInfos
的内容进行遍历. 只有在某个区块既带有 needsRebuild
标记(即满足 builtChunk.needsRebuild()
)且 WorldRenderer.world.getChunk(chunkPos.x, chunkPos.z).shouldRenderOnUpdate()
时, 才会继续对其处理. 对于被处理的区块, 以下行为将发生:
- 在客户端选项里的
chunkBuilderMode
是邻近(ChunkBuilderMode.NEARBY
)时, 带有 needsImportantRebuild
标记或者离摄像机距离小于 的区块会在渲染线程中立即被同步地重建(rebuild).
- 在客户端选项里的
chunkBuilderMode
是受玩家影响的(ChunkBuilderMode.PLAYER_AFFECTED
)时, 只有带有 needsImportantRebuild
标记的区块会立即被同步重建.
- 以上的同步重建完成后会清除
needsRebuild
标记和 needsImportantRebuild
标记, 以避免重建重复发生; 重建完成时一个上传重建结果至显存的任务将被添加到 WorldRenderer.chunkBuilder
内部的上传队列 uploadQueue
- 在
WorldRenderer.chunkInfos
完成遍历后, uploadQueue
将被执行直至清空.
- 不满足以上重建条件的区块会被收集起来, 丢进
WorldRenderer.chunkBuilder
内部的重建队列 prioritizedTaskQueue
或 taskQueue
里异步重建, 重建完成时上传重建结果的任务同样将被添加到上传队列; 在提交重建任务后, 重建相关标记也会被清除.
区块重建任务最终会调用 ChunkBuilder.BuiltChunk.RebuildTask.render
函数, 以收集方块/方块实体的信息, 构建区块的顶点数据, 并写入不同的 RenderLayer
的缓冲区中:[5]
net/minecraft/client/render/chunk/ChunkBuilder.java:576
576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637
|
private Set<BlockEntity> render(float cameraX, float cameraY, float cameraZ, ChunkData data, BlockBufferBuilderStorage buffers) { boolean i = true; BlockPos blockPos = BuiltChunk.this.origin.toImmutable(); BlockPos blockPos2 = blockPos.add(15, 15, 15); ChunkOcclusionDataBuilder chunkOcclusionDataBuilder = new ChunkOcclusionDataBuilder(); HashSet<BlockEntity> set = Sets.newHashSet(); ChunkRendererRegion chunkRendererRegion = this.region; this.region = null; MatrixStack matrixStack = new MatrixStack(); if (chunkRendererRegion != null) { BlockModelRenderer.enableBrightnessCache(); Random random = new Random(); BlockRenderManager blockRenderManager = MinecraftClient.getInstance().getBlockRenderManager(); for (BlockPos blockPos3 : BlockPos.iterate(blockPos, blockPos2)) { BufferBuilder bufferBuilder; RenderLayer renderLayer; BlockState blockState2; FluidState fluidState; BlockEntity blockEntity; BlockState blockState = chunkRendererRegion.getBlockState(blockPos3); if (blockState.isOpaqueFullCube(chunkRendererRegion, blockPos3)) { chunkOcclusionDataBuilder.markClosed(blockPos3); } if (blockState.hasBlockEntity() && (blockEntity = chunkRendererRegion.getBlockEntity(blockPos3)) != null) { this.addBlockEntity(data, set, blockEntity); } if (!(fluidState = (blockState2 = chunkRendererRegion.getBlockState(blockPos3)).getFluidState()).isEmpty()) { renderLayer = RenderLayers.getFluidLayer(fluidState); bufferBuilder = buffers.get(renderLayer); if (data.initializedLayers.add(renderLayer)) { BuiltChunk.this.beginBufferBuilding(bufferBuilder); } if (blockRenderManager.renderFluid(blockPos3, chunkRendererRegion, bufferBuilder, blockState2, fluidState)) { data.empty = false; data.nonEmptyLayers.add(renderLayer); } } if (blockState.getRenderType() == BlockRenderType.INVISIBLE) continue; renderLayer = RenderLayers.getBlockLayer(blockState); bufferBuilder = buffers.get(renderLayer); if (data.initializedLayers.add(renderLayer)) { BuiltChunk.this.beginBufferBuilding(bufferBuilder); } matrixStack.push(); matrixStack.translate(blockPos3.getX() & 0xF, blockPos3.getY() & 0xF, blockPos3.getZ() & 0xF); if (blockRenderManager.renderBlock(blockState, blockPos3, chunkRendererRegion, matrixStack, bufferBuilder, true, random)) { data.empty = false; data.nonEmptyLayers.add(renderLayer); } matrixStack.pop(); } if (data.nonEmptyLayers.contains(RenderLayer.getTranslucent())) { BufferBuilder bufferBuilder2 = buffers.get(RenderLayer.getTranslucent()); bufferBuilder2.sortFrom(cameraX - (float)blockPos.getX(), cameraY - (float)blockPos.getY(), cameraZ - (float)blockPos.getZ()); data.bufferState = bufferBuilder2.popState(); } data.initializedLayers.stream().map(buffers::get).forEach(BufferBuilder::end); BlockModelRenderer.disableBrightnessCache(); } data.occlusionGraph = chunkOcclusionDataBuilder.build(); return set; }
|
在完成区块剔除/区块更新和上传顶点缓冲区后, 各 RenderLayer
对应的顶点缓冲区就准备好绘制了. WorldRenderer.renderLayer(RenderLayer, MatrixStack, double, double, double, Matrix4f)
是具体执行此类绘制的函数. 欲知后事如何, 且听下回分解.