Послойная отрисовка всей сцены!
This commit is contained in:
parent
ff6dba143e
commit
d5b20c9bda
@ -1,9 +1,12 @@
|
|||||||
package ru.dbotthepony.kstarbound.client
|
package ru.dbotthepony.kstarbound.client
|
||||||
|
|
||||||
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
|
|
||||||
import ru.dbotthepony.kstarbound.api.IStruct2f
|
import ru.dbotthepony.kstarbound.api.IStruct2f
|
||||||
import ru.dbotthepony.kstarbound.client.render.ChunkRenderer
|
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(
|
class ClientWorldChunkTuple(
|
||||||
world: World<*>,
|
world: World<*>,
|
||||||
@ -64,12 +67,13 @@ class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWo
|
|||||||
determineRenderers.add(chunk.renderer)
|
determineRenderers.add(chunk.renderer)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (renderer in determineRenderers) {
|
val renderList = ArrayList<ChunkRenderer>()
|
||||||
val (x, y) = renderer.chunk.pos
|
|
||||||
|
|
||||||
client.gl.matrixStack.push().translateWithScale(x = x * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf, y = y * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf)
|
for (renderer in determineRenderers) {
|
||||||
renderer.bakeAndRender()
|
renderList.add(renderer)
|
||||||
client.gl.matrixStack.pop()
|
renderer.autoBakeStatic()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderLayeredList(client.gl.matrixStack, renderList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,42 @@
|
|||||||
package ru.dbotthepony.kstarbound.client.render
|
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.ClientWorld
|
||||||
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
|
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
|
||||||
import ru.dbotthepony.kstarbound.math.FloatMatrix
|
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.Chunk
|
||||||
import ru.dbotthepony.kstarbound.world.ITileChunk
|
import ru.dbotthepony.kstarbound.world.ITileChunk
|
||||||
import kotlin.collections.ArrayList
|
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 inner class TileLayerRenderer(private val layerChangeset: () -> Int, private val isBackground: Boolean) : AutoCloseable {
|
||||||
private val layers = TileLayerList()
|
private val layers = TileLayerList()
|
||||||
private val bakedMeshes = ArrayList<BakedStaticMesh>()
|
val bakedMeshes = ArrayList<Pair<BakedStaticMesh, Int>>()
|
||||||
private var changeset = -1
|
private var changeset = -1
|
||||||
|
|
||||||
fun tesselateStatic(view: ITileChunk) {
|
fun tesselateStatic(view: ITileChunk) {
|
||||||
if (state.isSameThread()) {
|
if (state.isSameThread()) {
|
||||||
for (mesh in bakedMeshes) {
|
for (mesh in bakedMeshes) {
|
||||||
mesh.close()
|
mesh.first.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
bakedMeshes.clear()
|
bakedMeshes.clear()
|
||||||
} else {
|
} else {
|
||||||
unloadableBakedMeshes.addAll(bakedMeshes)
|
for (mesh in bakedMeshes) {
|
||||||
|
unloadableBakedMeshes.add(mesh.first)
|
||||||
|
}
|
||||||
|
|
||||||
bakedMeshes.clear()
|
bakedMeshes.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,8 +59,8 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Clie
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun uploadStatic(clear: Boolean = true) {
|
fun uploadStatic(clear: Boolean = true) {
|
||||||
for ((baked, builder) in layers.buildList()) {
|
for ((baked, builder, zLevel) in layers.buildList()) {
|
||||||
bakedMeshes.add(BakedStaticMesh(baked, builder))
|
bakedMeshes.add(BakedStaticMesh(baked, builder) to zLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clear) {
|
if (clear) {
|
||||||
@ -55,7 +70,7 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Clie
|
|||||||
|
|
||||||
fun render(transform: FloatMatrix<*>) {
|
fun render(transform: FloatMatrix<*>) {
|
||||||
for (mesh in bakedMeshes) {
|
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)
|
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() {
|
override fun close() {
|
||||||
for (mesh in bakedMeshes) {
|
for (mesh in bakedMeshes) {
|
||||||
mesh.close()
|
mesh.first.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val bakedMeshesForeground = ArrayList<BakedStaticMesh>()
|
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<BakedStaticMesh>()
|
private val unloadableBakedMeshes = ArrayList<BakedStaticMesh>()
|
||||||
|
|
||||||
private val foreground = TileLayerRenderer(chunk.foreground::changeset, isBackground = false)
|
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)
|
background.uploadStatic(clear)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отрисовывает всю геометрию напрямую
|
||||||
|
*/
|
||||||
fun render(transform: FloatMatrix<*> = state.matrixStack.last) {
|
fun render(transform: FloatMatrix<*> = state.matrixStack.last) {
|
||||||
unloadUnused()
|
unloadUnused()
|
||||||
|
|
||||||
@ -138,6 +182,9 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Clie
|
|||||||
foreground.render(transform)
|
foreground.render(transform)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отрисовывает всю геометрию напрямую, с проверкой, изменился ли чанк
|
||||||
|
*/
|
||||||
fun bakeAndRender(transform: FloatMatrix<*> = state.matrixStack.last) {
|
fun bakeAndRender(transform: FloatMatrix<*> = state.matrixStack.last) {
|
||||||
unloadUnused()
|
unloadUnused()
|
||||||
|
|
||||||
@ -145,6 +192,79 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Clie
|
|||||||
foreground.bakeAndRender(transform, this::getForeground)
|
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<Pair<BakedStaticMesh, Int>>()
|
||||||
|
|
||||||
|
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() {
|
override fun close() {
|
||||||
background.close()
|
background.close()
|
||||||
foreground.close()
|
foreground.close()
|
||||||
|
@ -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<ILayeredRenderer>): Int {
|
||||||
|
val renderers = ArrayList<ILayeredRenderer>(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
|
||||||
|
}
|
@ -51,6 +51,9 @@ class TileLayerList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun clear() = layers.clear()
|
fun clear() = layers.clear()
|
||||||
|
|
||||||
|
val isEmpty get() = layers.isEmpty()
|
||||||
|
val isNotEmpty get() = layers.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
class TileRenderers(val state: GLStateTracker) {
|
class TileRenderers(val state: GLStateTracker) {
|
||||||
|
Loading…
Reference in New Issue
Block a user