KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt

259 lines
9.0 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 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<GLTexture2D, Config> = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(5))
.scheduler(Starbound)
.build()
private val background: Cache<GLTexture2D, Config> = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(5))
.scheduler(Starbound)
.build()
private val matCache: Cache<String, TileRenderer> = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(5))
.scheduler(Starbound)
.build()
private val modCache: Cache<String, TileRenderer> = 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<UberShader>(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<UberShader> {
return foreground.get(texture) { Config(it, FOREGROUND_COLOR) }
}
fun background(texture: GLTexture2D): RenderConfig<UberShader> {
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()
}
}