KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt

313 lines
8.3 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
}
}
}