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.client.StarboundClient import ru.dbotthepony.kstarbound.client.gl.* import ru.dbotthepony.kstarbound.client.gl.shader.GLTileProgram import ru.dbotthepony.kstarbound.client.gl.vertex.* import ru.dbotthepony.kstarbound.defs.tile.* import ru.dbotthepony.kstarbound.world.api.ITileAccess import ru.dbotthepony.kstarbound.world.api.ITileState import ru.dbotthepony.kstarbound.world.api.TileColor import ru.dbotthepony.kvector.arrays.Matrix4f import ru.dbotthepony.kvector.vector.RGBAColor import ru.dbotthepony.kvector.vector.Vector2i import kotlin.collections.HashMap /** * Хранит в себе программы для отрисовки определённых [TileDefinition] * * Создаётся единожды как потомок [GLStateTracker] */ class TileRenderers(val client: StarboundClient) { val state get() = client.gl private val foreground = HashMap() private val background = HashMap() private val matCache = HashMap() private val modCache = HashMap() fun getMaterialRenderer(defName: String): TileRenderer { return matCache.computeIfAbsent(defName) { val def = client.starbound.tiles[defName] // TODO: Пустой рендерер return@computeIfAbsent TileRenderer(this, def!!.value) } } fun getModifierRenderer(defName: String): TileRenderer { return modCache.computeIfAbsent(defName) { val def = client.starbound.tileModifiers[defName] // TODO: Пустой рендерер return@computeIfAbsent TileRenderer(this, def!!.value) } } private inner class Config(private val texture: GLTexture2D, private val color: RGBAColor) : RenderConfig(state, state.programs.tile) { override fun setup(transform: Matrix4f) { super.setup(transform) state.activeTexture = 0 state.depthTest = false program.texture = 0 texture.bind() texture.textureMagFilter = GL_NEAREST texture.textureMinFilter = GL_NEAREST program.transform = transform program.color = color } override fun uninstall() {} } fun foreground(texture: GLTexture2D): RenderConfig { return foreground.computeIfAbsent(texture) { Config(it, FOREGROUND_COLOR) } } fun background(texture: GLTexture2D): RenderConfig { return background.computeIfAbsent(texture) { Config(it, BACKGROUND_COLOR) } } companion object { val BACKGROUND_COLOR = RGBAColor(0.4f, 0.4f, 0.4f) val FOREGROUND_COLOR = RGBAColor(1f, 1f, 1f) } } 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) { private enum class TestResult { NO_MATCH, CONTINUE, HALT } val state get() = renderers.state val texture = state.loadTexture(def.renderParameters.texture.imagePath.value!!).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: ITileAccess, builder: VertexBuilder, 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 != TileColor.DEFAULT) { mins += piece.colorStride * self.color.ordinal maxs += piece.colorStride * self.color.ordinal } 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: ITileAccess, meshBuilder: MultiMeshBuilder, pos: Vector2i, thisBuilder: VertexBuilder, background: Boolean, isModifier: Boolean, ): TestResult { if (matchPiece.test(getter, equalityTester, pos)) { for (renderPiece in matchPiece.pieces) { if (renderPiece.piece.texture != null) { val program = if (background) { renderers.background(state.loadTexture(renderPiece.piece.texture!!)) } else { renderers.foreground(state.loadTexture(renderPiece.piece.texture!!)) } tesselateAt(self, renderPiece.piece, getter, meshBuilder.get(program, def.renderParameters.zLevel), 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, meshBuilder, pos, thisBuilder, background, isModifier) if (matched == TestResult.HALT || matched == TestResult.CONTINUE && matchPiece.haltOnSubMatch) { return TestResult.HALT } } if (matchPiece.haltOnMatch) { return TestResult.HALT } return TestResult.CONTINUE } return TestResult.NO_MATCH } /** * Тесселирует тайлы в заданной позиции в нужном билдере * * [getter] Нужен для получения информации о ближайших блоках * * [meshBuilder] содержит текущие программы и их билдеры и их zPos * * Тесселирует тайлы в нужный VertexBuilder с масштабом согласно константе [PIXELS_IN_STARBOUND_UNITf] */ fun tesselate(self: ITileState, getter: ITileAccess, meshBuilder: MultiMeshBuilder, pos: Vector2i, background: Boolean = false, isModifier: Boolean = false) { // если у нас нет renderTemplate // то мы просто не можем его отрисовать val template = def.renderTemplate.value ?: return val vertexBuilder = meshBuilder.get(if (background) bakedBackgroundProgramState else bakedProgramState, def.renderParameters.zLevel) for ((_, matcher) in template.matches) { for (matchPiece in matcher) { val matched = tesselatePiece(self, matchPiece, getter, meshBuilder, pos, vertexBuilder, background, isModifier) if (matched == TestResult.HALT) { break } } } } companion object { const val Z_LEVEL = 10f private val LOGGER = LogManager.getLogger() } }