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