313 lines
8.3 KiB
Kotlin
313 lines
8.3 KiB
Kotlin
package ru.dbotthepony.kstarbound.client
|
||
|
||
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
|
||
import ru.dbotthepony.kstarbound.client.render.BakedStaticMesh
|
||
import ru.dbotthepony.kstarbound.client.render.EntityRenderer
|
||
import ru.dbotthepony.kstarbound.client.render.ILayeredRenderer
|
||
import ru.dbotthepony.kstarbound.client.render.TileLayerList
|
||
import ru.dbotthepony.kstarbound.math.Matrix4fStack
|
||
import ru.dbotthepony.kstarbound.math.Vector2d
|
||
import ru.dbotthepony.kstarbound.world.*
|
||
import ru.dbotthepony.kstarbound.world.entities.Entity
|
||
import java.io.Closeable
|
||
|
||
/**
|
||
* Псевдо zPos у фоновых тайлов
|
||
*
|
||
* Добавление этого числа к zPos гарантирует, что фоновые тайлы будут отрисованы
|
||
* первыми (на самом дальнем плане)
|
||
*/
|
||
const val Z_LEVEL_BACKGROUND = 60000
|
||
|
||
class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, ClientChunk>(world, pos), Closeable, ILayeredRenderer {
|
||
val state: GLStateTracker get() = world.client.gl
|
||
|
||
private inner class TileLayerRenderer(private val layerChangeset: () -> Int, private val isBackground: Boolean) : AutoCloseable {
|
||
private val layers = TileLayerList()
|
||
val bakedMeshes = ArrayList<Pair<BakedStaticMesh, Int>>()
|
||
private var changeset = -1
|
||
|
||
fun bake(view: ITileChunk) {
|
||
if (state.isSameThread()) {
|
||
for (mesh in bakedMeshes) {
|
||
mesh.first.close()
|
||
}
|
||
|
||
bakedMeshes.clear()
|
||
} else {
|
||
for (mesh in bakedMeshes) {
|
||
unloadableBakedMeshes.add(mesh.first)
|
||
}
|
||
|
||
bakedMeshes.clear()
|
||
}
|
||
|
||
layers.clear()
|
||
|
||
for ((pos, tile) in view.posToTile) {
|
||
if (tile != null) {
|
||
val renderer = state.tileRenderers.get(tile.def.materialName)
|
||
renderer.tesselate(view, layers, pos, background = isBackground)
|
||
}
|
||
}
|
||
}
|
||
|
||
fun loadRenderers(view: ITileChunk) {
|
||
for ((_, tile) in view.posToTile) {
|
||
if (tile != null) {
|
||
state.tileRenderers.get(tile.def.materialName)
|
||
}
|
||
}
|
||
}
|
||
|
||
fun uploadStatic(clear: Boolean = true) {
|
||
for ((baked, builder, zLevel) in layers.buildList()) {
|
||
bakedMeshes.add(BakedStaticMesh(baked, builder) to zLevel)
|
||
}
|
||
|
||
if (clear) {
|
||
layers.clear()
|
||
}
|
||
}
|
||
|
||
fun render(stack: Matrix4fStack) {
|
||
val transform = stack.last
|
||
|
||
for (mesh in bakedMeshes) {
|
||
mesh.first.render(transform)
|
||
}
|
||
}
|
||
|
||
fun autoBake(provider: () -> ITileChunk) {
|
||
if (changeset != layerChangeset.invoke()) {
|
||
this.bake(provider.invoke())
|
||
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()
|
||
}
|
||
}
|
||
|
||
fun autoBakeAndUpload(provider: () -> ITileChunk) {
|
||
autoBake(provider)
|
||
autoUpload()
|
||
}
|
||
|
||
fun bakeAndRender(transform: Matrix4fStack, provider: () -> ITileChunk) {
|
||
autoBakeAndUpload(provider)
|
||
render(transform)
|
||
}
|
||
|
||
override fun close() {
|
||
for (mesh in bakedMeshes) {
|
||
mesh.first.close()
|
||
}
|
||
}
|
||
}
|
||
|
||
val debugCollisions get() = world.client.settings.debugCollisions
|
||
val posVector2d = Vector2d(x = pos.x * CHUNK_SIZEd, y = pos.y * CHUNK_SIZEd)
|
||
|
||
private val unloadableBakedMeshes = ArrayList<BakedStaticMesh>()
|
||
|
||
private val foregroundRenderer = TileLayerRenderer(foreground::changeset, isBackground = false)
|
||
private val backgroundRenderer = TileLayerRenderer(background::changeset, isBackground = true)
|
||
|
||
private fun getForegroundView(): ITileChunk {
|
||
return world.getForegroundView(pos)!!
|
||
}
|
||
|
||
private fun getBackgroundView(): ITileChunk {
|
||
return world.getBackgroundView(pos)!!
|
||
}
|
||
|
||
/**
|
||
* Принудительно подгружает в GLStateTracker все необходимые рендереры (ибо им нужны текстуры и прочее)
|
||
*
|
||
* Вызывается перед tesselateStatic()
|
||
*/
|
||
fun loadRenderers() {
|
||
unloadUnused()
|
||
|
||
foregroundRenderer.loadRenderers(getForegroundView())
|
||
backgroundRenderer.loadRenderers(getBackgroundView())
|
||
}
|
||
|
||
private fun unloadUnused() {
|
||
if (unloadableBakedMeshes.size != 0) {
|
||
for (baked in unloadableBakedMeshes) {
|
||
baked.close()
|
||
}
|
||
|
||
unloadableBakedMeshes.clear()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Отрисовывает всю геометрию напрямую
|
||
*/
|
||
fun render(stack: Matrix4fStack) {
|
||
unloadUnused()
|
||
|
||
backgroundRenderer.render(stack)
|
||
foregroundRenderer.render(stack)
|
||
}
|
||
|
||
/**
|
||
* Отрисовывает всю геометрию напрямую, с проверкой, изменился ли чанк
|
||
*/
|
||
fun bakeAndRender(stack: Matrix4fStack) {
|
||
unloadUnused()
|
||
|
||
backgroundRenderer.bakeAndRender(stack, this::getBackgroundView)
|
||
foregroundRenderer.bakeAndRender(stack, this::getForegroundView)
|
||
}
|
||
|
||
/**
|
||
* Тесселирует "статичную" геометрию в builders (к примеру тайлы), с проверкой, изменилось ли что либо,
|
||
* и загружает её в видеопамять.
|
||
*
|
||
* Может быть вызван вне рендер потока (ибо в любом случае он требует некой "стаитичности" данных в чанке)
|
||
* но только если до этого был вызыван loadRenderers() и геометрия чанка не поменялась
|
||
*
|
||
*/
|
||
fun bake() {
|
||
if (state.isSameThread())
|
||
unloadUnused()
|
||
|
||
backgroundRenderer.autoBake(this::getBackgroundView)
|
||
foregroundRenderer.autoBake(this::getForegroundView)
|
||
|
||
if (state.isSameThread())
|
||
upload()
|
||
}
|
||
|
||
/**
|
||
* Загружает в видеопамять всю геометрию напрямую, если есть что загружать
|
||
*/
|
||
fun upload() {
|
||
unloadUnused()
|
||
|
||
backgroundRenderer.autoUpload()
|
||
foregroundRenderer.autoUpload()
|
||
}
|
||
|
||
fun renderDebug() {
|
||
if (debugCollisions) {
|
||
state.quadWireframe {
|
||
it.quad(aabb.mins.x.toFloat(), aabb.mins.y.toFloat(), aabb.maxs.x.toFloat(), aabb.maxs.y.toFloat())
|
||
|
||
for (layer in foreground.collisionLayers()) {
|
||
it.quad(layer.mins.x.toFloat(), layer.mins.y.toFloat(), layer.maxs.x.toFloat(), layer.maxs.y.toFloat())
|
||
}
|
||
}
|
||
}
|
||
|
||
for (renderer in entityRenderers.values) {
|
||
renderer.renderDebug()
|
||
}
|
||
}
|
||
|
||
private val layerQueue = ArrayDeque<Pair<(Matrix4fStack) -> Unit, Int>>()
|
||
|
||
override fun renderLayerFromStack(zPos: Int, stack: Matrix4fStack): Int {
|
||
if (layerQueue.isEmpty())
|
||
return -1
|
||
|
||
stack.push().translateWithScale(x = pos.x * CHUNK_SIZEf, y = pos.y * CHUNK_SIZEf)
|
||
var pair = layerQueue.last()
|
||
|
||
while (pair.second >= zPos) {
|
||
pair.first.invoke(stack)
|
||
|
||
layerQueue.removeLast()
|
||
|
||
if (layerQueue.isEmpty()) {
|
||
stack.pop()
|
||
return -1
|
||
}
|
||
|
||
pair = layerQueue.last()
|
||
}
|
||
|
||
stack.pop()
|
||
return layerQueue.last().second
|
||
}
|
||
|
||
override fun bottomMostZLevel(): Int {
|
||
if (layerQueue.isEmpty()) {
|
||
return -1
|
||
}
|
||
|
||
return layerQueue.last().second
|
||
}
|
||
|
||
override fun prepareForLayeredRender() {
|
||
layerQueue.clear()
|
||
|
||
for ((baked, zLevel) in backgroundRenderer.bakedMeshes) {
|
||
layerQueue.add(baked::renderStacked to (zLevel + Z_LEVEL_BACKGROUND))
|
||
}
|
||
|
||
for ((baked, zLevel) in foregroundRenderer.bakedMeshes) {
|
||
layerQueue.add(baked::renderStacked to zLevel)
|
||
}
|
||
|
||
for (renderer in entityRenderers.values) {
|
||
layerQueue.add(lambda@{ it: Matrix4fStack ->
|
||
val relative = renderer.renderPos - posVector2d
|
||
it.push().translateWithScale(relative.x.toFloat(), relative.y.toFloat())
|
||
renderer.render(it)
|
||
it.pop()
|
||
return@lambda
|
||
} to renderer.layer)
|
||
}
|
||
|
||
layerQueue.sortBy {
|
||
return@sortBy it.second
|
||
}
|
||
}
|
||
|
||
private val entityRenderers = HashMap<Entity, EntityRenderer>()
|
||
|
||
override fun onEntityAdded(entity: Entity) {
|
||
entityRenderers[entity] = EntityRenderer(state, entity, this)
|
||
}
|
||
|
||
override fun onEntityTransferedToThis(entity: Entity, otherChunk: ClientChunk) {
|
||
val renderer = otherChunk.entityRenderers[entity] ?: throw IllegalStateException("$otherChunk has no renderer for $entity!")
|
||
entityRenderers[entity] = renderer
|
||
renderer.chunk = this
|
||
}
|
||
|
||
override fun onEntityTransferedFromThis(entity: Entity, otherChunk: ClientChunk) {
|
||
entityRenderers.remove(entity)
|
||
}
|
||
|
||
override fun onEntityRemoved(entity: Entity) {
|
||
entityRenderers.remove(entity)!!.close()
|
||
}
|
||
|
||
override fun close() {
|
||
backgroundRenderer.close()
|
||
foregroundRenderer.close()
|
||
|
||
for (renderer in entityRenderers.values) {
|
||
renderer.close()
|
||
}
|
||
}
|
||
}
|