package ru.dbotthepony.kstarbound.client.render import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap import it.unimi.dsi.fastutil.ints.Int2ObjectFunction import org.apache.logging.log4j.LogManager import org.lwjgl.opengl.GL46.* import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.gl.* import ru.dbotthepony.kstarbound.client.gl.vertex.GLAttributeList import ru.dbotthepony.kstarbound.client.gl.vertex.* import ru.dbotthepony.kstarbound.defs.tile.* import ru.dbotthepony.kstarbound.world.ITileChunk import ru.dbotthepony.kstarbound.world.ITileState import ru.dbotthepony.kvector.vector.Color import ru.dbotthepony.kvector.vector.nint.Vector2i import kotlin.collections.HashMap data class TileLayer( val bakedProgramState: ConfiguredShaderProgram, val vertexBuilder: AbstractVertexBuilder<*>, val zPos: Int ) class TileLayerList { private val layers = HashMap>() /** * Получает геометрию слоя ([DynamicVertexBuilder]), который имеет программу для отрисовки [program] и располагается на [zLevel]. * * Если такого слоя нет, вызывается [compute] и создаётся новый [TileLayer], затем возвращается результат [compute]. */ fun computeIfAbsent(program: ConfiguredShaderProgram, zLevel: Int, compute: () -> AbstractVertexBuilder<*>): AbstractVertexBuilder<*> { return layers.computeIfAbsent(program) { Int2ObjectAVLTreeMap() }.computeIfAbsent(zLevel, Int2ObjectFunction { return@Int2ObjectFunction TileLayer(program, compute.invoke(), zLevel) }).vertexBuilder } fun buildSortedLayerList(): List { val list = ArrayList() for (getList in layers.values) { list.addAll(getList.values) } list.sortBy { // унарный минус для инвентирования порядка (сначала большие, потом маленькие) return@sortBy -it.zPos } return list } fun clear() = layers.clear() val isEmpty get() = layers.isEmpty() val isNotEmpty get() = layers.isNotEmpty() } /** * Хранит в себе программы для отрисовки определённых [TileDefinition] * * Создаётся единожды как потомок [GLStateTracker] */ class TileRenderers(val client: StarboundClient) { val state get() = client.gl private val foregroundTilePrograms = HashMap() private val backgroundTilePrograms = HashMap() private val tileRenderersCache = HashMap() private val modifierRenderersCache = HashMap() fun getTileRenderer(defName: String): TileRenderer { return tileRenderersCache.computeIfAbsent(defName) { val def = client.starbound.tiles[defName] // TODO: Пустой рендерер return@computeIfAbsent TileRenderer(this, def!!.value) } } fun getModifierRenderer(defName: String): TileRenderer { return modifierRenderersCache.computeIfAbsent(defName) { val def = client.starbound.tileModifiers[defName] // TODO: Пустой рендерер return@computeIfAbsent TileRenderer(this, def!!.value) } } private inner class ForegroundTileProgram(private val texture: GLTexture2D) : ConfiguredShaderProgram(state.programs.tile) { override fun setup() { super.setup() state.activeTexture = 0 state.depthTest = false program["_texture"] = 0 texture.bind() texture.textureMagFilter = GL_NEAREST texture.textureMinFilter = GL_NEAREST program["_color"] = FOREGROUND_COLOR } override fun equals(other: Any?): Boolean { if (this === other) { return true } if (other is ForegroundTileProgram) { return texture == other.texture } return super.equals(other) } override fun hashCode(): Int { return texture.hashCode() } } private inner class BackgroundTileProgram(private val texture: GLTexture2D) : ConfiguredShaderProgram(state.programs.tile) { override fun setup() { super.setup() state.activeTexture = 0 state.depthTest = false program["_texture"] = 0 texture.bind() texture.textureMagFilter = GL_NEAREST texture.textureMinFilter = GL_NEAREST program["_color"] = BACKGROUND_COLOR } override fun equals(other: Any?): Boolean { if (this === other) { return true } if (other is BackgroundTileProgram) { return texture == other.texture } return super.equals(other) } override fun hashCode(): Int { return texture.hashCode() } } /** * Возвращает запечённое состояние шейдера shaderVertexTexture с данной текстурой */ fun foreground(texture: GLTexture2D): ConfiguredShaderProgram { return foregroundTilePrograms.computeIfAbsent(texture, ::ForegroundTileProgram) } /** * Возвращает запечённое состояние шейдера shaderVertexTextureColor с данной текстурой */ fun background(texture: GLTexture2D): ConfiguredShaderProgram { return backgroundTilePrograms.computeIfAbsent(texture, ::BackgroundTileProgram) } companion object { val BACKGROUND_COLOR = Color(0.4f, 0.4f, 0.4f) val FOREGROUND_COLOR = Color(1f, 1f, 1f) } } private enum class TileRenderTesselateResult { NO_MATCH, CONTINUE, HALT } private fun vertexTextureBuilder() = HeapVertexBuilder(GLAttributeList.TILE, GeometryType.QUADS) private class TileEqualityTester(val definition: TileDefinition) : EqualityRuleTester { override fun test(thisTile: ITileState, otherTile: ITileState): Boolean { return otherTile.material == definition && thisTile.hueShift == otherTile.hueShift } } private class ModifierEqualityTester(val definition: MaterialModifier) : EqualityRuleTester { override fun test(thisTile: ITileState, otherTile: ITileState): Boolean { return otherTile.modifier == definition } } class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) { val state get() = renderers.state val texture = state.loadNamedTexture(def.renderParameters.texture.image).also { it.textureMagFilter = GL_NEAREST } val equalityTester: EqualityRuleTester = when (def) { is TileDefinition -> TileEqualityTester(def) is MaterialModifier -> ModifierEqualityTester(def) else -> throw IllegalStateException() } val bakedProgramState = renderers.foreground(texture) val bakedBackgroundProgramState = renderers.background(texture) // private var notifiedDepth = false private fun tesselateAt(self: ITileState, piece: RenderPiece, getter: ITileChunk, builder: AbstractVertexBuilder<*>, pos: Vector2i, offset: Vector2i = Vector2i.ZERO, isModifier: Boolean) { val fx = pos.x.toFloat() val fy = pos.y.toFloat() var a = fx var b = fy var c = fx + piece.textureSize.x / PIXELS_IN_STARBOUND_UNITf var d = fy + piece.textureSize.y / PIXELS_IN_STARBOUND_UNITf if (offset != Vector2i.ZERO) { a += offset.x / PIXELS_IN_STARBOUND_UNITf // в json файлах Y указан как положительный вверх, // что соответствует нашему миру b += offset.y / PIXELS_IN_STARBOUND_UNITf c += offset.x / PIXELS_IN_STARBOUND_UNITf d += offset.y / PIXELS_IN_STARBOUND_UNITf } var mins = piece.texturePosition var maxs = piece.texturePosition + piece.textureSize if (def.renderParameters.variants != 0 && piece.variantStride != null && piece.texture == null) { val variant = (getter.randomDoubleFor(pos) * def.renderParameters.variants).toInt() mins += piece.variantStride * variant maxs += piece.variantStride * variant } if (def.renderParameters.multiColored && piece.colorStride != null && self.color != 0) { mins += piece.colorStride * self.color maxs += piece.colorStride * self.color } val (u0, v0) = texture.pixelToUV(mins) val (u1, v1) = texture.pixelToUV(maxs) builder.quadZ(a, b, c, d, Z_LEVEL, QuadTransformers.uv(u0, v1, u1, v0).after { it, _ -> it.push(if (isModifier) self.modifierHueShift else self.hueShift) }) } private fun tesselatePiece( self: ITileState, matchPiece: RenderMatch, getter: ITileChunk, layers: TileLayerList, pos: Vector2i, thisBuilder: AbstractVertexBuilder<*>, background: Boolean, isModifier: Boolean, ): TileRenderTesselateResult { if (matchPiece.test(getter, equalityTester, pos)) { for (renderPiece in matchPiece.pieces) { if (renderPiece.piece.texture != null) { val program = if (background) { renderers.background(state.loadNamedTexture(renderPiece.piece.texture!!)) } else { renderers.foreground(state.loadNamedTexture(renderPiece.piece.texture!!)) } tesselateAt(self, renderPiece.piece, getter, layers.computeIfAbsent(program, def.renderParameters.zLevel, ::vertexTextureBuilder), pos, renderPiece.offset, isModifier) } else { tesselateAt(self, renderPiece.piece, getter, thisBuilder, pos, renderPiece.offset, isModifier) } } for (subPiece in matchPiece.subMatches) { val matched = tesselatePiece(self, subPiece, getter, layers, pos, thisBuilder, background, isModifier) if (matched == TileRenderTesselateResult.HALT || matched == TileRenderTesselateResult.CONTINUE && matchPiece.haltOnSubMatch) { return TileRenderTesselateResult.HALT } } if (matchPiece.haltOnMatch) { return TileRenderTesselateResult.HALT } return TileRenderTesselateResult.CONTINUE } return TileRenderTesselateResult.NO_MATCH } /** * Тесселирует тайлы в заданной позиции в нужном билдере * * [getter] Нужен для получения информации о ближайших блоках * * [layers] содержит текущие программы и их билдеры и их zPos * * Тесселирует тайлы в нужный VertexBuilder с масштабом согласно константе [PIXELS_IN_STARBOUND_UNITf] */ fun tesselate(self: ITileState, getter: ITileChunk, layers: TileLayerList, pos: Vector2i, background: Boolean = false, isModifier: Boolean = false) { // если у нас нет renderTemplate // то мы просто не можем его отрисовать val template = def.renderTemplate.value ?: return val vertexBuilder = layers.computeIfAbsent(if (background) bakedBackgroundProgramState else bakedProgramState, def.renderParameters.zLevel, ::vertexTextureBuilder) for ((_, matcher) in template.matches) { for (matchPiece in matcher) { val matched = tesselatePiece(self, matchPiece, getter, layers, pos, vertexBuilder, background, isModifier) if (matched == TileRenderTesselateResult.HALT) { break } } } } companion object { const val Z_LEVEL = 10f private val LOGGER = LogManager.getLogger() } }