package ru.dbotthepony.kstarbound.client.render import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Scheduler import org.apache.logging.log4j.LogManager import org.lwjgl.opengl.GL45.* import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.gl.* import ru.dbotthepony.kstarbound.client.gl.shader.UberShader import ru.dbotthepony.kstarbound.client.gl.vertex.* import ru.dbotthepony.kstarbound.defs.tile.* import ru.dbotthepony.kstarbound.util.random.staticRandomFloat import ru.dbotthepony.kstarbound.world.api.ITileAccess import ru.dbotthepony.kstarbound.world.api.AbstractTileState import ru.dbotthepony.kstarbound.world.api.TileColor import java.time.Duration import java.util.concurrent.Callable import kotlin.math.roundToInt /** * Хранит в себе программы для отрисовки определённых [TileDefinition] * * Создаётся единожды как потомок [Graphics] */ class TileRenderers(val client: StarboundClient) { private val foreground: Cache = Caffeine.newBuilder() .expireAfterAccess(Duration.ofMinutes(5)) .scheduler(Starbound) .build() private val background: Cache = Caffeine.newBuilder() .expireAfterAccess(Duration.ofMinutes(5)) .scheduler(Starbound) .build() private val matCache: Cache = Caffeine.newBuilder() .expireAfterAccess(Duration.ofMinutes(5)) .scheduler(Starbound) .build() private val modCache: Cache = Caffeine.newBuilder() .expireAfterAccess(Duration.ofMinutes(5)) .scheduler(Starbound) .build() fun getMaterialRenderer(defName: String): TileRenderer { return matCache.get(defName) { val def = Registries.tiles[defName] // TODO: Пустой рендерер client.mailbox.submit(Callable { TileRenderer(this, def!!.value) }).get() } } fun getModifierRenderer(defName: String): TileRenderer { return modCache.get(defName) { val def = Registries.tileModifiers[defName] // TODO: Пустой рендерер client.mailbox.submit(Callable { TileRenderer(this, def!!.value) }).get() } } private inner class Config(private val texture: GLTexture2D, private val color: RGBAColor) : RenderConfig(client.programs.tile) { override val initialBuilderCapacity: Int get() = 1024 override fun setup() { super.setup() client.depthTest = false program.texture0 = 0 client.textures2D[0] = texture texture.textureMagFilter = GL_NEAREST texture.textureMinFilter = GL_NEAREST program.modelMatrix = client.stack.last() program.colorMultiplier = color } override fun uninstall() {} } fun foreground(texture: GLTexture2D): RenderConfig { return foreground.get(texture) { Config(it, FOREGROUND_COLOR) } } fun background(texture: GLTexture2D): RenderConfig { return background.get(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: AbstractTileState?, otherTile: AbstractTileState?): Boolean { return otherTile?.material?.value == definition && thisTile?.hueShift == otherTile.hueShift } } private class ModifierEqualityTester(val definition: TileModifierDefinition) : EqualityRuleTester { override fun test(thisTile: AbstractTileState?, otherTile: AbstractTileState?): Boolean { return otherTile?.modifier?.value == definition } } class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) { private enum class TestResult { NO_MATCH, CONTINUE, HALT } val client get() = renderers.client val texture = def.renderParameters.texture?.image?.texture?.also { it.textureMagFilter = GL_NEAREST } val equalityTester: EqualityRuleTester = when (def) { is TileDefinition -> TileEqualityTester(def) is TileModifierDefinition -> ModifierEqualityTester(def) else -> throw IllegalStateException() } val bakedProgramState = texture?.let { renderers.foreground(it) } val bakedBackgroundProgramState = texture?.let { renderers.background(it) } // private var notifiedDepth = false private fun tesselateAt(self: AbstractTileState, 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.image == null) { val variant = (staticRandomFloat("TileVariant", pos.x, pos.y) * def.renderParameters.variants).roundToInt() 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) val hue = if (isModifier) self.modifierHueShift else self.hueShift // flip uv since in-world coordinates are flipped relative to screen builder.vertex(a, b).uv(u0, v1).hueShift(hue) builder.vertex(c, b).uv(u1, v1).hueShift(hue) builder.vertex(c, d).uv(u1, v0).hueShift(hue) builder.vertex(a, d).uv(u0, v0).hueShift(hue) } private fun tesselatePiece( self: AbstractTileState, matchPiece: RenderMatch, getter: ITileAccess, meshBuilder: LayeredRenderer, pos: Vector2i, thisBuilder: VertexBuilder, isBackground: Boolean, isModifier: Boolean, ): TestResult { if (matchPiece.test(getter, equalityTester, pos)) { for (renderPiece in matchPiece.pieces) { if (renderPiece.piece.image != null) { val program = if (isBackground) { renderers.background(renderPiece.piece.image!!.texture) } else { renderers.foreground(renderPiece.piece.image!!.texture) } tesselateAt( self, renderPiece.piece, getter, meshBuilder.getBuilder(RenderLayer.tileLayer(isBackground, isModifier, self), program).mode(GeometryType.QUADS), 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, isBackground, 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: AbstractTileState, getter: ITileAccess, meshBuilder: LayeredRenderer, pos: Vector2i, isBackground: Boolean = false, isModifier: Boolean = false) { if (texture == null) return // если у нас нет renderTemplate // то мы просто не можем его отрисовать val template = def.renderTemplate.value ?: return val vertexBuilder = meshBuilder .getBuilder(RenderLayer.tileLayer(isBackground, isModifier, self), if (isBackground) bakedBackgroundProgramState!! else bakedProgramState!!) .mode(GeometryType.QUADS) for ((_, matcher) in template.matches) { for (matchPiece in matcher) { val matched = tesselatePiece(self, matchPiece, getter, meshBuilder, pos, vertexBuilder, isBackground, isModifier) if (matched == TestResult.HALT) { break } } } } companion object { private val LOGGER = LogManager.getLogger() } }