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

508 lines
15 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
import org.lwjgl.opengl.GL11.GL_LINES
import org.lwjgl.opengl.GL11.GL_TRIANGLES
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.client.gl.program.GLHardLightGeometryProgram
import ru.dbotthepony.kstarbound.client.gl.program.GLSoftLightGeometryProgram
import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType
import ru.dbotthepony.kstarbound.client.gl.vertex.StatefulVertexBuilder
import ru.dbotthepony.kstarbound.client.gl.vertex.quad
import ru.dbotthepony.kstarbound.client.gl.vertex.shadowLine
import ru.dbotthepony.kstarbound.client.render.ConfiguredStaticMesh
import ru.dbotthepony.kstarbound.client.render.entity.EntityRenderer
import ru.dbotthepony.kstarbound.client.render.ILayeredRenderer
import ru.dbotthepony.kstarbound.client.render.GPULightRenderer
import ru.dbotthepony.kstarbound.client.render.TileLayerList
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.world.*
import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kvector.matrix.Matrix4fStack
import ru.dbotthepony.kvector.matrix.nfloat.Matrix4f
import ru.dbotthepony.kvector.util2d.intersectCircleRectangle
import ru.dbotthepony.kvector.vector.ndouble.Vector2d
import ru.dbotthepony.kvector.vector.nfloat.Vector2f
import ru.dbotthepony.kvector.vector.nfloat.Vector3f
import java.io.Closeable
import java.util.LinkedList
/**
* Псевдо zPos у фоновых тайлов
*
* Добавление этого числа к zPos гарантирует, что фоновые тайлы будут отрисованы
* первыми (на самом дальнем плане)
*/
const val Z_LEVEL_BACKGROUND = 60000
const val Z_LEVEL_LIQUID = 10000
class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, ClientChunk>(world, pos), Closeable {
val state: GLStateTracker get() = world.client.gl
private inner class TileLayerRenderer(private val layerChangeset: () -> Int, private val isBackground: Boolean) : AutoCloseable {
private val layers = TileLayerList()
val bakedMeshes = LinkedList<Pair<ConfiguredStaticMesh, Int>>()
private var changeset = -1
fun bake(view: ITileChunk) {
if (state.isSameThread()) {
for (mesh in bakedMeshes) {
mesh.first.close()
}
}
bakedMeshes.clear()
layers.clear()
for ((pos, tile) in view.posToTile) {
val material = tile.material
if (material != null) {
world.client.tileRenderers.getTileRenderer(material.materialName).tesselate(tile, view, layers, pos, background = isBackground)
}
val modifier = tile.modifier
if (modifier != null) {
world.client.tileRenderers.getModifierRenderer(modifier.modName).tesselate(tile, view, layers, pos, background = isBackground, isModifier = true)
}
}
}
fun loadRenderers(view: ITileChunk) {
for ((_, tile) in view.posToTile) {
val material = tile.material
val modifier = tile.modifier
if (material != null) {
world.client.tileRenderers.getTileRenderer(material.materialName)
}
if (modifier != null) {
world.client.tileRenderers.getModifierRenderer(modifier.modName)
}
}
}
fun uploadStatic(clear: Boolean = true) {
for ((baked, builder, zLevel) in layers.buildSortedLayerList()) {
bakedMeshes.add(ConfiguredStaticMesh(baked, builder) to zLevel)
}
if (clear) {
layers.clear()
}
}
fun render(stack: Matrix4fStack) {
val transform = stack.last
for (mesh in bakedMeshes) {
mesh.first.render(transform)
}
}
fun autoBake(provider: () -> ITileChunk) {
if (changeset != layerChangeset.invoke()) {
this.bake(provider.invoke())
changeset = layerChangeset.invoke()
}
}
fun autoUpload() {
if (layers.isNotEmpty) {
for (mesh in bakedMeshes) {
mesh.first.close()
}
bakedMeshes.clear()
for ((baked, builder, zLevel) in layers.buildSortedLayerList()) {
bakedMeshes.add(ConfiguredStaticMesh(baked, builder) to zLevel)
}
layers.clear()
}
}
fun autoBakeAndUpload(provider: () -> ITileChunk) {
autoBake(provider)
autoUpload()
}
fun bakeAndRender(transform: Matrix4fStack, provider: () -> ITileChunk) {
autoBakeAndUpload(provider)
render(transform)
}
override fun close() {
for (mesh in bakedMeshes) {
mesh.first.close()
}
}
}
val debugCollisions get() = world.client.settings.debugCollisions
val posVector2d = Vector2d(x = pos.x * CHUNK_SIZEd, y = pos.y * CHUNK_SIZEd)
private val foregroundRenderer = TileLayerRenderer(foreground::changeset, isBackground = false)
private val backgroundRenderer = TileLayerRenderer(background::changeset, isBackground = true)
private fun getForegroundView(): ITileChunk {
return world.getForegroundView(pos)!!
}
private fun getBackgroundView(): ITileChunk {
return world.getBackgroundView(pos)!!
}
/**
* Принудительно подгружает в GLStateTracker все необходимые рендереры (ибо им нужны текстуры и прочее)
*
* Вызывается перед tesselateStatic()
*/
fun loadRenderers() {
foregroundRenderer.loadRenderers(getForegroundView())
backgroundRenderer.loadRenderers(getBackgroundView())
}
/**
* Отрисовывает всю геометрию напрямую
*/
fun render(stack: Matrix4fStack) {
backgroundRenderer.render(stack)
foregroundRenderer.render(stack)
}
/**
* Отрисовывает всю геометрию напрямую, с проверкой, изменился ли чанк
*/
fun bakeAndRender(stack: Matrix4fStack) {
backgroundRenderer.bakeAndRender(stack, this::getBackgroundView)
foregroundRenderer.bakeAndRender(stack, this::getForegroundView)
}
/**
* Тесселирует "статичную" геометрию в builders (к примеру тайлы), с проверкой, изменилось ли что либо,
* и загружает её в видеопамять.
*
* Может быть вызван вне рендер потока (ибо в любом случае он требует некой "стаитичности" данных в чанке)
* но только если до этого был вызыван loadRenderers() и геометрия чанка не поменялась
*
*/
fun bake() {
backgroundRenderer.autoBake(this::getBackgroundView)
foregroundRenderer.autoBake(this::getForegroundView)
if (state.isSameThread())
upload()
}
/**
* Загружает в видеопамять всю геометрию напрямую, если есть что загружать
*/
fun upload() {
backgroundRenderer.autoUpload()
foregroundRenderer.autoUpload()
}
private inner class ShadowGeometryTracker(val x: Int, val y: Int) : GPULightRenderer.ShadowGeometryRenderer {
private val hardShadowGeometry by lazy(LazyThreadSafetyMode.NONE) { StatefulVertexBuilder(state, GPULightRenderer.SHADOW_FORMAT, GeometryType.LINES) }
private var hardShadowGeometryRev = -1
private val softShadowGeometry by lazy(LazyThreadSafetyMode.NONE) { StatefulVertexBuilder(state, GPULightRenderer.SHADOW_FORMAT_SOFT, GeometryType.QUADS_ALTERNATIVE) }
private var softShadowGeometryRev = -1
private fun buildGeometry(builder: StatefulVertexBuilder, line: (StatefulVertexBuilder, Float, Float, Float, Float) -> Unit) {
builder.begin()
for (x in this.x * SHADOW_GEOMETRY_SQUARE_SIZE until (this.x + 1) * SHADOW_GEOMETRY_SQUARE_SIZE) {
for (y in this.y * SHADOW_GEOMETRY_SQUARE_SIZE until (this.y + 1) * SHADOW_GEOMETRY_SQUARE_SIZE) {
if (foreground[x, y].material?.renderParameters?.lightTransparent == false) {
if (x == 0 || foreground[x - 1, y].material?.renderParameters?.lightTransparent != true) {
line(builder, x.toFloat(), y + 1f, x.toFloat(), y.toFloat())
}
if (x == CHUNK_SIZE - 1 || foreground[x + 1, y].material?.renderParameters?.lightTransparent != true) {
line(builder, x + 1f, y.toFloat(), x + 1f, y + 1f)
}
if (y == 0 || foreground[x, y - 1].material?.renderParameters?.lightTransparent != true) {
line(builder, x.toFloat(), y.toFloat(), x + 1f, y.toFloat())
}
if (y == CHUNK_SIZE - 1 || foreground[x, y + 1].material?.renderParameters?.lightTransparent != true) {
line(builder, x + 1f, y + 1f, x.toFloat(), y + 1f)
}
}
}
}
builder.upload()
}
fun buildHardGeometry() {
hardShadowGeometryRev = tileChangeset
buildGeometry(hardShadowGeometry) { it, x0, y0, x1, y1 ->
it.vertex().pushVec2f(x0, y0)
it.vertex().pushVec2f(x1, y1)
}
}
fun buildSoftGeometry() {
softShadowGeometryRev = tileChangeset
buildGeometry(softShadowGeometry, StatefulVertexBuilder::shadowLine)
}
override fun renderHardGeometry(
renderer: GPULightRenderer,
lightPosition: Vector2f,
lightRadius: Float,
stack: Matrix4fStack,
program: GLHardLightGeometryProgram
) {
if (hardShadowGeometryRev != tileChangeset) {
buildHardGeometry()
}
hardShadowGeometry.draw(GL_LINES)
}
override fun renderSoftGeometry(
renderer: GPULightRenderer,
lightPosition: Vector2f,
lightRadius: Float,
stack: Matrix4fStack,
program: GLSoftLightGeometryProgram
) {
if (softShadowGeometryRev != tileChangeset) {
buildSoftGeometry()
}
softShadowGeometry.draw(GL_TRIANGLES)
}
}
private val shadowGeometry = ArrayList<ShadowGeometryTracker>()
init {
for (x in 0 until SHADOW_GEOMETRY_SUBDIVISION) {
for (y in 0 until SHADOW_GEOMETRY_SUBDIVISION) {
shadowGeometry.add(ShadowGeometryTracker(x, y))
}
}
}
/**
* Хранит состояние отрисовки этого чанка
*
* Должен быть использован только один раз, после выкинут, иначе поведение
* кода невозможно будет предсказать
*/
inner class OneShotRenderer constructor(val origin: ChunkPos = pos) : ILayeredRenderer, GPULightRenderer.ShadowGeometryRenderer {
private val layerQueue = ArrayDeque<Pair<(Matrix4fStack) -> Unit, Int>>()
override fun renderHardGeometry(
renderer: GPULightRenderer,
lightPosition: Vector2f,
lightRadius: Float,
stack: Matrix4fStack,
program: GLHardLightGeometryProgram
) {
if (!intersectCircleRectangle(lightPosition, lightRadius, origin.x * CHUNK_SIZEf, origin.y * CHUNK_SIZEf, (origin.x + 1) * CHUNK_SIZEf, (origin.y + 1) * CHUNK_SIZEf)) {
return
}
var setOnce = false
for (geometry in shadowGeometry) {
if (intersectCircleRectangle(
lightPosition,
lightRadius,
origin.x * CHUNK_SIZEf + geometry.x * SHADOW_GEOMETRY_SQUARE_SIZE,
origin.y * CHUNK_SIZEf + geometry.y * SHADOW_GEOMETRY_SQUARE_SIZE,
origin.x * CHUNK_SIZEf + (geometry.x + 1) * SHADOW_GEOMETRY_SQUARE_SIZE,
origin.y * CHUNK_SIZEf + (geometry.y + 1) * SHADOW_GEOMETRY_SQUARE_SIZE)
) {
if (!setOnce) {
program.localToWorldTransform.set(
Matrix4f.IDENTITY.translateWithMultiplication(
Vector3f(x = origin.x * CHUNK_SIZEf,
y = origin.y * CHUNK_SIZEf)))
setOnce = true
}
geometry.renderHardGeometry(renderer, lightPosition, lightRadius, stack, program)
}
}
}
override fun renderSoftGeometry(
renderer: GPULightRenderer,
lightPosition: Vector2f,
lightRadius: Float,
stack: Matrix4fStack,
program: GLSoftLightGeometryProgram
) {
if (!intersectCircleRectangle(lightPosition, lightRadius, origin.x * CHUNK_SIZEf, origin.y * CHUNK_SIZEf, (origin.x + 1) * CHUNK_SIZEf, (origin.y + 1) * CHUNK_SIZEf)) {
return
}
var setOnce = false
for (geometry in shadowGeometry) {
if (intersectCircleRectangle(
lightPosition,
lightRadius,
origin.x * CHUNK_SIZEf + geometry.x * SHADOW_GEOMETRY_SQUARE_SIZE,
origin.y * CHUNK_SIZEf + geometry.y * SHADOW_GEOMETRY_SQUARE_SIZE,
origin.x * CHUNK_SIZEf + (geometry.x + 1) * SHADOW_GEOMETRY_SQUARE_SIZE,
origin.y * CHUNK_SIZEf + (geometry.y + 1) * SHADOW_GEOMETRY_SQUARE_SIZE)
) {
if (!setOnce) {
program.localToWorldTransform.set(
Matrix4f.IDENTITY.translateWithMultiplication(
Vector3f(x = origin.x * CHUNK_SIZEf,
y = origin.y * CHUNK_SIZEf)))
setOnce = true
}
geometry.renderSoftGeometry(renderer, lightPosition, lightRadius, stack, program)
}
}
}
init {
for ((baked, zLevel) in backgroundRenderer.bakedMeshes) {
layerQueue.add(baked::renderStacked to (zLevel + Z_LEVEL_BACKGROUND))
}
for ((baked, zLevel) in foregroundRenderer.bakedMeshes) {
layerQueue.add(baked::renderStacked to zLevel)
}
for (renderer in entityRenderers.values) {
layerQueue.add(lambda@{ it: Matrix4fStack ->
val relative = renderer.renderPos - posVector2d
it.push().translateWithMultiplication(relative.x.toFloat(), relative.y.toFloat())
renderer.render(it)
it.pop()
Unit
} to renderer.layer)
}
layerQueue.add({ it: Matrix4fStack ->
val types = ArrayList<LiquidDefinition>()
for (x in 0 until CHUNK_SIZE) {
for (y in 0 until CHUNK_SIZE) {
val state = getLiquid(x, y)
if (state != null && !types.any { it === state.def }) {
types.add(state.def)
}
}
}
val program = state.programs.liquid
program.use()
program.transform.set(it.last)
val builder = program.builder
for (type in types) {
builder.begin()
for (x in 0 until CHUNK_SIZE) {
for (y in 0 until CHUNK_SIZE) {
val state = getLiquid(x, y)
if (state != null && state.def === type) {
builder.quad(x.toFloat(), y.toFloat(), x + 1f, y + state.level)
}
}
}
program.baselineColor.set(type.color)
builder.upload()
builder.draw()
}
} to Z_LEVEL_LIQUID)
layerQueue.sortBy {
return@sortBy it.second
}
}
override fun renderLayerFromStack(zPos: Int, stack: Matrix4fStack): Int {
if (layerQueue.isEmpty())
return Int.MIN_VALUE
stack.push().translateWithMultiplication(x = origin.x * CHUNK_SIZEf, y = origin.y * CHUNK_SIZEf)
var pair = layerQueue.last()
while (pair.second >= zPos) {
pair.first.invoke(stack)
layerQueue.removeLast()
if (layerQueue.isEmpty()) {
stack.pop()
return Int.MIN_VALUE
}
pair = layerQueue.last()
}
stack.pop()
return layerQueue.last().second
}
override fun bottomMostZLevel(): Int {
if (layerQueue.isEmpty()) {
return Int.MIN_VALUE
}
return layerQueue.last().second
}
}
private val entityRenderers = HashMap<Entity, EntityRenderer>()
override fun onEntityAdded(entity: Entity) {
entityRenderers[entity] = EntityRenderer.getRender(state, entity, this)
}
override fun onEntityTransferedToThis(entity: Entity, otherChunk: ClientChunk) {
val renderer = otherChunk.entityRenderers[entity] ?: throw IllegalStateException("$otherChunk has no renderer for $entity!")
entityRenderers[entity] = renderer
renderer.chunk = this
}
override fun onEntityTransferedFromThis(entity: Entity, otherChunk: ClientChunk) {
entityRenderers.remove(entity)
}
override fun onEntityRemoved(entity: Entity) {
entityRenderers.remove(entity)!!.close()
}
override fun close() {
backgroundRenderer.close()
foregroundRenderer.close()
for (renderer in entityRenderers.values) {
renderer.close()
}
}
companion object {
const val SHADOW_GEOMETRY_SUBDIVISION = 4
const val SHADOW_GEOMETRY_SQUARE_SIZE = CHUNK_SIZE / SHADOW_GEOMETRY_SUBDIVISION
}
}