508 lines
15 KiB
Kotlin
508 lines
15 KiB
Kotlin
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
|
||
}
|
||
}
|