318 lines
11 KiB
Kotlin
318 lines
11 KiB
Kotlin
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()
|
||
}
|
||
}
|