KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt
DBotThePony 15abdba2c5
Starbound теперь более не синглтон, а настоящий класс
удалил кучу устаревших классов ибо они совсем не имеют смысла
2023-02-06 17:17:42 +07:00

318 lines
11 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<ConfiguredShaderProgram, Int2ObjectAVLTreeMap<TileLayer>>()
/**
* Получает геометрию слоя ([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<TileLayer> {
val list = ArrayList<TileLayer>()
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<GLTexture2D, ForegroundTileProgram>()
private val backgroundTilePrograms = HashMap<GLTexture2D, BackgroundTileProgram>()
private val tileRenderersCache = HashMap<String, TileRenderer>()
private val modifierRenderersCache = HashMap<String, TileRenderer>()
fun getTileRenderer(defName: String): TileRenderer {
return tileRenderersCache.computeIfAbsent(defName) {
val def = client.starbound.tiles[defName] // TODO: Пустой рендерер
return@computeIfAbsent TileRenderer(this, def!!)
}
}
fun getModifierRenderer(defName: String): TileRenderer {
return modifierRenderersCache.computeIfAbsent(defName) {
val def = client.starbound.tileModifiers[defName] // TODO: Пустой рендерер
return@computeIfAbsent TileRenderer(this, def!!)
}
}
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()
}
}