package ru.dbotthepony.kstarbound.client.render import org.apache.logging.log4j.LogManager import org.lwjgl.opengl.GL46.* import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.gl.* import ru.dbotthepony.kstarbound.defs.tile.RenderMatch import ru.dbotthepony.kstarbound.defs.tile.RenderPiece import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.world.ITileChunk import ru.dbotthepony.kvector.vector.Color import ru.dbotthepony.kvector.vector.nint.Vector2i import kotlin.collections.HashMap data class TileLayer( val bakedProgramState: BakedProgramState, val vertexBuilder: DynamicVertexBuilder, val zPos: Int) class TileLayerList { private val layers = HashMap>() fun getLayer(programState: BakedProgramState, zLevel: Int, compute: () -> DynamicVertexBuilder): DynamicVertexBuilder { val list = layers.computeIfAbsent(programState) {ArrayList()} for (layer in list) { if (layer.zPos == zLevel) { return layer.vertexBuilder } } val computed = TileLayer(programState, compute.invoke(), zLevel) list.add(computed) return computed.vertexBuilder } fun buildList(): List { val list = ArrayList() for (getList in layers.values) { list.addAll(getList) } list.sortBy { // унарный минус для инвентирования порядка (сначала большие, потом маленькие) return@sortBy -it.zPos } return list } fun clear() = layers.clear() val isEmpty get() = layers.isEmpty() val isNotEmpty get() = layers.isNotEmpty() } class TileRenderers(val state: GLStateTracker) { private val foregroundTilePrograms = HashMap() private val backgroundTilePrograms = HashMap() private val tileRenderers = HashMap() operator fun get(tile: String): TileRenderer { return tileRenderers.computeIfAbsent(tile) { val def = Starbound.getTileDefinition(tile) // TODO: Пустой рендерер return@computeIfAbsent TileRenderer(state, def!!) } } private inner class ForegroundTileProgram(private val texture: GLTexture2D) : BakedProgramState(state.shaderVertexTexture) { override fun setup() { super.setup() state.activeTexture = 0 state.depthTest = false program["_texture"] = 0 texture.bind() texture.textureMagFilter = GL_NEAREST texture.textureMinFilter = GL_NEAREST } 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) : BakedProgramState(state.shaderVertexTextureColor) { 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): BakedProgramState { return foregroundTilePrograms.computeIfAbsent(texture, ::ForegroundTileProgram) } /** * Возвращает запечённое состояние shaderVertexTextureColor с данной текстурой */ fun background(texture: GLTexture2D): BakedProgramState { return backgroundTilePrograms.computeIfAbsent(texture, ::BackgroundTileProgram) } companion object { val BACKGROUND_COLOR = Color(0.4f, 0.4f, 0.4f) } } private enum class TileRenderTesselateResult { NO_MATCH, CONTINUE, HALT } class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) { val texture = state.loadNamedTexture(tile.renderParameters.absoluteTexturePath).also { it.textureMagFilter = GL_NEAREST } val bakedProgramState = state.tileRenderers.foreground(texture) val bakedBackgroundProgramState = state.tileRenderers.background(texture) // private var notifiedDepth = false private fun tesselateAt(piece: RenderPiece, getter: ITileChunk, builder: DynamicVertexBuilder, pos: Vector2i, offset: Vector2i = Vector2i.ZERO) { 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 } if (tile.renderParameters.variants == 0 || piece.texture != null || piece.variantStride == null) { val (u0, v0) = texture.pixelToUV(piece.texturePosition) val (u1, v1) = texture.pixelToUV(piece.texturePosition + piece.textureSize) builder.quadZ( a, b, c, d, Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0)) } else { val variant = (getter.randomDoubleFor(pos) * tile.renderParameters.variants).toInt() val (u0, v0) = texture.pixelToUV(piece.texturePosition + piece.variantStride * variant) val (u1, v1) = texture.pixelToUV(piece.texturePosition + piece.textureSize + piece.variantStride * variant) builder.quadZ( a, b, c, d, Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0)) } } private fun tesselatePiece(matchPiece: RenderMatch, getter: ITileChunk, layers: TileLayerList, pos: Vector2i, thisBuilder: DynamicVertexBuilder, background: Boolean): TileRenderTesselateResult { if (matchPiece.test(getter, tile, pos)) { for (renderPiece in matchPiece.pieces) { if (renderPiece.piece.texture != null) { val program: BakedProgramState if (background) { program = state.tileRenderers.background(state.loadNamedTexture(renderPiece.piece.texture!!)) } else { program = state.tileRenderers.foreground(state.loadNamedTexture(renderPiece.piece.texture!!)) } tesselateAt(renderPiece.piece, getter, layers.getLayer(program, tile.renderParameters.zLevel) { return@getLayer DynamicVertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS) }, pos, renderPiece.offset) } else { tesselateAt(renderPiece.piece, getter, thisBuilder, pos, renderPiece.offset) } } for (subPiece in matchPiece.subMatches) { val matched = tesselatePiece(subPiece, getter, layers, pos, thisBuilder, background) 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(getter: ITileChunk, layers: TileLayerList, pos: Vector2i, background: Boolean = false) { // если у нас нет renderTemplate // то мы просто не можем его отрисовать val template = tile.renderTemplate val builder = layers.getLayer(if (background) bakedBackgroundProgramState else bakedProgramState, tile.renderParameters.zLevel) { return@getLayer DynamicVertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS) } for ((_, matcher) in template.matches) { for (matchPiece in matcher) { val matched = tesselatePiece(matchPiece, getter, layers, pos, builder, background) if (matched == TileRenderTesselateResult.HALT) { break } } } } companion object { const val Z_LEVEL = 10f private val LOGGER = LogManager.getLogger() } }