276 lines
8.7 KiB
Kotlin
276 lines
8.7 KiB
Kotlin
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<BakedProgramState, ArrayList<TileLayer>>()
|
||
|
||
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<TileLayer> {
|
||
val list = ArrayList<TileLayer>()
|
||
|
||
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<GLTexture2D, ForegroundTileProgram>()
|
||
private val backgroundTilePrograms = HashMap<GLTexture2D, BackgroundTileProgram>()
|
||
private val tileRenderers = HashMap<String, TileRenderer>()
|
||
|
||
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()
|
||
}
|
||
}
|