package ru.dbotthepony.kstarbound.render import org.apache.logging.log4j.LogManager import org.lwjgl.opengl.GL46.* import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.TileDefinition import ru.dbotthepony.kstarbound.defs.TileRenderMatchPiece import ru.dbotthepony.kstarbound.defs.TileRenderPiece import ru.dbotthepony.kstarbound.gl.* import ru.dbotthepony.kstarbound.math.Vector2i import ru.dbotthepony.kstarbound.util.Color import ru.dbotthepony.kstarbound.world.ITileChunk import kotlin.collections.HashMap data class TileLayer( val bakedProgramState: BakedProgramState, val vertexBuilder: VertexBuilder, val zPos: Int) class TileLayerList { private val layers = HashMap>() fun getLayer(programState: BakedProgramState, zLevel: Int, compute: () -> VertexBuilder): VertexBuilder { 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() } 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.render.texture).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: TileRenderPiece, getter: ITileChunk, builder: VertexBuilder, 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 / BASELINE_TEXTURE_SIZE var d = fy + piece.textureSize.y / BASELINE_TEXTURE_SIZE if (offset != Vector2i.ZERO) { a += offset.x / BASELINE_TEXTURE_SIZE // в json файлах y указан как положительный вверх b += offset.y / BASELINE_TEXTURE_SIZE c += offset.x / BASELINE_TEXTURE_SIZE d += offset.y / BASELINE_TEXTURE_SIZE } /* if (!notifiedDepth && tile.render.zLevel >= 5900) { LOGGER.warn("Tile {} has out of bounds zLevel of {}", tile.materialName, tile.render.zLevel) notifiedDepth = true } */ if (tile.render.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, tile.render.zLevel.toFloat() + 200f, VertexTransformers.uv(u0, v1, u1, v0)) builder.quadZ(a, b, c, d, Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0)) } else { val variant = (getter.randomDoubleFor(pos) * tile.render.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, tile.render.zLevel.toFloat() + 200f, VertexTransformers.uv(u0, v1, u1, v0)) builder.quadZ(a, b, c, d, Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0)) } } private fun tesselatePiece(matchPiece: TileRenderMatchPiece, getter: ITileChunk, layers: TileLayerList, pos: Vector2i, thisBuilder: VertexBuilder, 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.render.zLevel) { return@getLayer VertexBuilder(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 * * Тесселирует тайлы в границы -1f .. CHUNK_SIZEf + 1f на основе [pos] */ fun tesselate(getter: ITileChunk, layers: TileLayerList, pos: Vector2i, background: Boolean = false) { // если у нас нет renderTemplate // то мы просто не можем его отрисовать tile.render.renderTemplate ?: return val builder = layers.getLayer(if (background) bakedBackgroundProgramState else bakedProgramState, tile.render.zLevel) { return@getLayer VertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS) } for ((_, matcher) in tile.render.renderTemplate.matches) { for (matchPiece in matcher.pieces) { val matched = tesselatePiece(matchPiece, getter, layers, pos, builder, background) if (matched == TileRenderTesselateResult.HALT) { break } } } } companion object { const val BASELINE_TEXTURE_SIZE = 8f const val Z_LEVEL = 10f private val LOGGER = LogManager.getLogger() } }