From d5b20c9bdaff560fda421ebdbe2e4d6ef5ee18de Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Fri, 4 Feb 2022 20:50:20 +0700 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D1=81=D0=BB=D0=BE=D0=B9=D0=BD?= =?UTF-8?q?=D0=B0=D1=8F=20=D0=BE=D1=82=D1=80=D0=B8=D1=81=D0=BE=D0=B2=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B2=D1=81=D0=B5=D0=B9=20=D1=81=D1=86=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kstarbound/client/ClientWorld.kt | 18 ++- .../kstarbound/client/render/ChunkRenderer.kt | 138 ++++++++++++++++-- .../client/render/LayeredRenderTree.kt | 86 +++++++++++ .../kstarbound/client/render/TileRenderer.kt | 3 + 4 files changed, 229 insertions(+), 16 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/render/LayeredRenderTree.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt index 900973e9..3642b8f6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt @@ -1,9 +1,12 @@ package ru.dbotthepony.kstarbound.client -import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.api.IStruct2f import ru.dbotthepony.kstarbound.client.render.ChunkRenderer -import ru.dbotthepony.kstarbound.world.* +import ru.dbotthepony.kstarbound.client.render.renderLayeredList +import ru.dbotthepony.kstarbound.world.Chunk +import ru.dbotthepony.kstarbound.world.IWorldChunkTuple +import ru.dbotthepony.kstarbound.world.MutableWorldChunkTuple +import ru.dbotthepony.kstarbound.world.World class ClientWorldChunkTuple( world: World<*>, @@ -64,12 +67,13 @@ class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World() - client.gl.matrixStack.push().translateWithScale(x = x * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf, y = y * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf) - renderer.bakeAndRender() - client.gl.matrixStack.pop() + for (renderer in determineRenderers) { + renderList.add(renderer) + renderer.autoBakeStatic() } + + renderLayeredList(client.gl.matrixStack, renderList) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt index dc1afb9c..1eee4d55 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt @@ -1,27 +1,42 @@ package ru.dbotthepony.kstarbound.client.render +import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.client.ClientWorld import ru.dbotthepony.kstarbound.client.gl.GLStateTracker import ru.dbotthepony.kstarbound.math.FloatMatrix +import ru.dbotthepony.kstarbound.math.Matrix4f +import ru.dbotthepony.kstarbound.math.Matrix4fStack +import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.ITileChunk import kotlin.collections.ArrayList -class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: ClientWorld? = null) : AutoCloseable { +/** + * Псевдо zPos у фоновых тайлов + * + * Добавление этого числа к zPos гарантирует, что фоновые тайлы будут отрисованы + * первыми (на самом дальнем плане) + */ +const val Z_LEVEL_BACKGROUND = 60000 + +class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: ClientWorld? = null) : AutoCloseable, ILayeredRenderer { private inner class TileLayerRenderer(private val layerChangeset: () -> Int, private val isBackground: Boolean) : AutoCloseable { private val layers = TileLayerList() - private val bakedMeshes = ArrayList() + val bakedMeshes = ArrayList>() private var changeset = -1 fun tesselateStatic(view: ITileChunk) { if (state.isSameThread()) { for (mesh in bakedMeshes) { - mesh.close() + mesh.first.close() } bakedMeshes.clear() } else { - unloadableBakedMeshes.addAll(bakedMeshes) + for (mesh in bakedMeshes) { + unloadableBakedMeshes.add(mesh.first) + } + bakedMeshes.clear() } @@ -44,8 +59,8 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Clie } fun uploadStatic(clear: Boolean = true) { - for ((baked, builder) in layers.buildList()) { - bakedMeshes.add(BakedStaticMesh(baked, builder)) + for ((baked, builder, zLevel) in layers.buildList()) { + bakedMeshes.add(BakedStaticMesh(baked, builder) to zLevel) } if (clear) { @@ -55,7 +70,7 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Clie fun render(transform: FloatMatrix<*>) { for (mesh in bakedMeshes) { - mesh.render(transform) + mesh.first.render(transform) } } @@ -70,14 +85,40 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Clie render(transform) } + fun autoBake(provider: () -> ITileChunk) { + if (changeset != layerChangeset.invoke()) { + this.tesselateStatic(provider.invoke()) + this.uploadStatic() + + changeset = layerChangeset.invoke() + } + } + + fun autoUpload() { + if (layers.isNotEmpty) { + for (mesh in bakedMeshes) { + mesh.first.close() + } + + bakedMeshes.clear() + + for ((baked, builder, zLevel) in layers.buildList()) { + bakedMeshes.add(BakedStaticMesh(baked, builder) to zLevel) + } + + layers.clear() + } + } + override fun close() { for (mesh in bakedMeshes) { - mesh.close() + mesh.first.close() } } } - private val bakedMeshesForeground = ArrayList() + val transform = Matrix4f().translate(x = chunk.pos.x * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf, y = chunk.pos.x * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf) + private val unloadableBakedMeshes = ArrayList() private val foreground = TileLayerRenderer(chunk.foreground::changeset, isBackground = false) @@ -131,6 +172,9 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Clie background.uploadStatic(clear) } + /** + * Отрисовывает всю геометрию напрямую + */ fun render(transform: FloatMatrix<*> = state.matrixStack.last) { unloadUnused() @@ -138,6 +182,9 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Clie foreground.render(transform) } + /** + * Отрисовывает всю геометрию напрямую, с проверкой, изменился ли чанк + */ fun bakeAndRender(transform: FloatMatrix<*> = state.matrixStack.last) { unloadUnused() @@ -145,6 +192,79 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Clie foreground.bakeAndRender(transform, this::getForeground) } + /** + * Запекает всю геометрию напрямую, с проверкой, изменился ли чанк, + * и загружает её, если вызвано в рендер потоке + */ + fun autoBakeStatic() { + if (state.isSameThread()) + unloadUnused() + + background.autoBake(this::getBackground) + foreground.autoBake(this::getForeground) + + if (state.isSameThread()) + autoUploadStatic() + } + + /** + * Загружает в видеопамять всю геометрию напрямую, если есть что загружать + */ + fun autoUploadStatic() { + unloadUnused() + + background.autoUpload() + foreground.autoUpload() + } + + private val meshDeque = ArrayDeque>() + + override fun renderLayerFromStack(zPos: Int, transform: Matrix4fStack): Int { + if (meshDeque.isEmpty()) + return -1 + + transform.push().translateWithScale(x = chunk.pos.x * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf, y = chunk.pos.y * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf) + var pair = meshDeque.last() + + while (pair.second >= zPos) { + pair.first.render(transform.last) + + meshDeque.removeLast() + + if (meshDeque.isEmpty()) { + transform.pop() + return -1 + } + + pair = meshDeque.last() + } + + transform.pop() + return meshDeque.last().second + } + + override fun bottomMostZLevel(): Int { + if (meshDeque.isEmpty()) { + return -1 + } + + return meshDeque.last().second + } + + override fun prepareForLayeredRender() { + meshDeque.clear() + + for ((baked, zLevel) in background.bakedMeshes) { + meshDeque.add(baked to (zLevel + Z_LEVEL_BACKGROUND)) + } + + meshDeque.addAll(foreground.bakedMeshes) + + meshDeque.sortBy { + return@sortBy it.second + } + } + override fun close() { background.close() foreground.close() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/LayeredRenderTree.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/LayeredRenderTree.kt new file mode 100644 index 00000000..6a9fe3d6 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/LayeredRenderTree.kt @@ -0,0 +1,86 @@ +package ru.dbotthepony.kstarbound.client.render + +import ru.dbotthepony.kstarbound.math.FloatMatrix +import ru.dbotthepony.kstarbound.math.Matrix4f +import ru.dbotthepony.kstarbound.math.Matrix4fStack + +/** + * Интерфейс для отрисовки комплексных объектов, в которых множество слоёв, который + * определяет лишь один метод: [renderLayer] + * + * Используется вместе с другими [ILayeredRenderer], где необходимо отрисовывать комплексную сцену, + * реализуя ручную сортировку геометрии. + */ +interface ILayeredRenderer { + /** + * Главный метод отрисовки данной стопки слоёв. Вызов данного метода может что-либо + * отрисовать, а может вообще ничего не отрисовать. + * + * zLevel всегда положителен, и указывает на то, какой слой (или все слои за) надо отрисовать, + * т.е. при вызове этого метода отрисовываются все слои, у которых z позиция >= [zPos] + * + * Возвращается zNew следующего слоя (такой, что [zPos] > zNew). + * + * Если следующего слоя нет, вернуть -1, и данный объект + * будет считаться отрисованным. + */ + fun renderLayerFromStack(zPos: Int, transform: Matrix4fStack): Int + + /** + * Возвращает наибольшее zPos в данной стопке. + * + * Если стопка пуста, то необходимо вернуть -1. + * + * В зависимости от сцены, которую необходимо отрисовать, + * [renderLayerFromStack] может быть вызван сразу с этим же значением, + * если этот объект имеет самый дальний слой + */ + fun bottomMostZLevel(): Int + + /** + * Говорит о том, что вот-вот будет начата отрисовка в render pipeline + * и будут вызываться [renderLayerFromStack]. В данном методе должна построиться + * и отсортироваться стопка слоёв + */ + fun prepareForLayeredRender() +} + +fun renderLayeredList(transform: Matrix4fStack, potentialRenderers: List): Int { + val renderers = ArrayList(potentialRenderers.size) + var bottomMost = -1 + + for (render in potentialRenderers) { + render.prepareForLayeredRender() + val zLevel = render.bottomMostZLevel() + + if (zLevel >= 0) { + bottomMost = bottomMost.coerceAtLeast(zLevel) + renderers.add(render) + } + } + + var lastBottom = bottomMost + var renderCalls = 0 + + while (lastBottom > 0 && renderers.isNotEmpty()) { + var newBottom = lastBottom + + for (i in renderers.size - 1 downTo 0) { + val renderer = renderers[i] + + val newLevel = renderer.renderLayerFromStack(lastBottom, transform) + + renderCalls++ + + if (newLevel <= -1) { + renderers.removeAt(i) + } else { + newBottom = newBottom.coerceAtMost(newLevel) + } + } + + lastBottom = newBottom + } + + return renderCalls +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt index 2ebb14e3..09ab3a9b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt @@ -51,6 +51,9 @@ class TileLayerList { } fun clear() = layers.clear() + + val isEmpty get() = layers.isEmpty() + val isNotEmpty get() = layers.isNotEmpty() } class TileRenderers(val state: GLStateTracker) {