461 lines
14 KiB
Kotlin
461 lines
14 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.shader.GLHardLightGeometryProgram
|
||
import ru.dbotthepony.kstarbound.client.gl.shader.GLSoftLightGeometryProgram
|
||
import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType
|
||
import ru.dbotthepony.kstarbound.client.gl.vertex.StreamVertexBuilder
|
||
import ru.dbotthepony.kstarbound.client.render.ConfiguredStaticMesh
|
||
import ru.dbotthepony.kstarbound.client.render.entity.EntityRenderer
|
||
import ru.dbotthepony.kstarbound.client.render.GPULightRenderer
|
||
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
|
||
import ru.dbotthepony.kstarbound.client.render.TileLayerList
|
||
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
|
||
import ru.dbotthepony.kstarbound.world.*
|
||
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE
|
||
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZEd
|
||
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZEf
|
||
import ru.dbotthepony.kstarbound.world.api.ITileChunk
|
||
import ru.dbotthepony.kstarbound.world.entities.Entity
|
||
import ru.dbotthepony.kvector.arrays.Matrix4fStack
|
||
import ru.dbotthepony.kvector.arrays.Matrix4f
|
||
import ru.dbotthepony.kvector.util2d.intersectCircleRectangle
|
||
import ru.dbotthepony.kvector.vector.Vector2d
|
||
import ru.dbotthepony.kvector.vector.Vector2f
|
||
import ru.dbotthepony.kvector.vector.Vector3f
|
||
import java.io.Closeable
|
||
import java.util.LinkedList
|
||
import java.util.function.IntSupplier
|
||
|
||
/**
|
||
* Псевдо 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: IntSupplier, private val view: () -> ITileChunk, private val isBackground: Boolean) : AutoCloseable {
|
||
private val layers = TileLayerList()
|
||
val bakedMeshes = LinkedList<Pair<ConfiguredStaticMesh, Int>>()
|
||
private var changeset = -1
|
||
|
||
fun bake() {
|
||
val view = view()
|
||
|
||
if (state.isSameThread()) {
|
||
for (mesh in bakedMeshes) {
|
||
mesh.first.close()
|
||
}
|
||
}
|
||
|
||
bakedMeshes.clear()
|
||
|
||
layers.clear()
|
||
|
||
for ((pos, tile) in view.iterate()) {
|
||
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() {
|
||
val view = view()
|
||
|
||
for ((_, tile) in view.iterate()) {
|
||
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() {
|
||
if (changeset != layerChangeset.asInt) {
|
||
this.bake()
|
||
changeset = layerChangeset.asInt
|
||
}
|
||
}
|
||
|
||
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() {
|
||
autoBake()
|
||
autoUpload()
|
||
}
|
||
|
||
fun bakeAndRender(transform: Matrix4fStack) {
|
||
autoBakeAndUpload()
|
||
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(::foregroundChangeset, { world.getView(pos).foregroundView }, isBackground = false)
|
||
private val backgroundRenderer = TileLayerRenderer(::backgroundChangeset, { world.getView(pos).backgroundView }, isBackground = true)
|
||
|
||
/**
|
||
* Принудительно подгружает в GLStateTracker все необходимые рендереры (ибо им нужны текстуры и прочее)
|
||
*
|
||
* Вызывается перед tesselateStatic()
|
||
*/
|
||
fun loadRenderers() {
|
||
foregroundRenderer.loadRenderers()
|
||
backgroundRenderer.loadRenderers()
|
||
}
|
||
|
||
/**
|
||
* Тесселирует "статичную" геометрию в builders (к примеру тайлы), с проверкой, изменилось ли что либо,
|
||
* и загружает её в видеопамять.
|
||
*
|
||
* Может быть вызван вне рендер потока (ибо в любом случае он требует некой "стаитичности" данных в чанке)
|
||
* но только если до этого был вызыван loadRenderers() и геометрия чанка не поменялась
|
||
*
|
||
*/
|
||
fun bake() {
|
||
backgroundRenderer.autoBake()
|
||
foregroundRenderer.autoBake()
|
||
|
||
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) { StreamVertexBuilder(state, GPULightRenderer.SHADOW_FORMAT, GeometryType.LINES) }
|
||
private var hardShadowGeometryRev = -1
|
||
private val softShadowGeometry by lazy(LazyThreadSafetyMode.NONE) { StreamVertexBuilder(state, GPULightRenderer.SHADOW_FORMAT_SOFT, GeometryType.QUADS_ALTERNATIVE) }
|
||
private var softShadowGeometryRev = -1
|
||
|
||
private fun buildGeometry(builder: StreamVertexBuilder, line: (StreamVertexBuilder, Float, Float, Float, Float) -> Unit) {
|
||
builder.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 (foregroundView.getTile(x, y).material?.renderParameters?.lightTransparent == false) {
|
||
if (x == 0 || foregroundView.getTile(x - 1, y).material?.renderParameters?.lightTransparent != true) {
|
||
line(builder, x.toFloat(), y + 1f, x.toFloat(), y.toFloat())
|
||
}
|
||
|
||
if (x == CHUNK_SIZE - 1 || foregroundView.getTile(x + 1, y).material?.renderParameters?.lightTransparent != true) {
|
||
line(builder, x + 1f, y.toFloat(), x + 1f, y + 1f)
|
||
}
|
||
|
||
if (y == 0 || foregroundView.getTile(x, y - 1).material?.renderParameters?.lightTransparent != true) {
|
||
line(builder, x.toFloat(), y.toFloat(), x + 1f, y.toFloat())
|
||
}
|
||
|
||
if (y == CHUNK_SIZE - 1 || foregroundView.getTile(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.builder.vertex().pushVec2f(x0, y0)
|
||
it.builder.vertex().pushVec2f(x1, y1)
|
||
}
|
||
}
|
||
|
||
fun buildSoftGeometry() {
|
||
softShadowGeometryRev = tileChangeset
|
||
buildGeometry(softShadowGeometry) { it, x0, y0, x1, y1 ->
|
||
it.builder.shadowLine(x0, y0, x1, y1)
|
||
}
|
||
}
|
||
|
||
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 Renderer(val renderOrigin: ChunkPos = pos) : GPULightRenderer.ShadowGeometryRenderer {
|
||
override fun renderHardGeometry(
|
||
renderer: GPULightRenderer,
|
||
lightPosition: Vector2f,
|
||
lightRadius: Float,
|
||
stack: Matrix4fStack,
|
||
program: GLHardLightGeometryProgram
|
||
) {
|
||
if (!intersectCircleRectangle(lightPosition, lightRadius, renderOrigin.x * CHUNK_SIZEf, renderOrigin.y * CHUNK_SIZEf, (renderOrigin.x + 1) * CHUNK_SIZEf, (renderOrigin.y + 1) * CHUNK_SIZEf)) {
|
||
return
|
||
}
|
||
|
||
var setOnce = false
|
||
|
||
for (geometry in shadowGeometry) {
|
||
if (intersectCircleRectangle(
|
||
lightPosition,
|
||
lightRadius,
|
||
renderOrigin.x * CHUNK_SIZEf + geometry.x * SHADOW_GEOMETRY_SQUARE_SIZE,
|
||
renderOrigin.y * CHUNK_SIZEf + geometry.y * SHADOW_GEOMETRY_SQUARE_SIZE,
|
||
renderOrigin.x * CHUNK_SIZEf + (geometry.x + 1) * SHADOW_GEOMETRY_SQUARE_SIZE,
|
||
renderOrigin.y * CHUNK_SIZEf + (geometry.y + 1) * SHADOW_GEOMETRY_SQUARE_SIZE)
|
||
) {
|
||
if (!setOnce) {
|
||
program.localToWorldTransform =
|
||
Matrix4f.identity().translateWithMultiplication(
|
||
Vector3f(x = renderOrigin.x * CHUNK_SIZEf,
|
||
y = renderOrigin.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, renderOrigin.x * CHUNK_SIZEf, renderOrigin.y * CHUNK_SIZEf, (renderOrigin.x + 1) * CHUNK_SIZEf, (renderOrigin.y + 1) * CHUNK_SIZEf)) {
|
||
return
|
||
}
|
||
|
||
var setOnce = false
|
||
|
||
for (geometry in shadowGeometry) {
|
||
if (intersectCircleRectangle(
|
||
lightPosition,
|
||
lightRadius,
|
||
renderOrigin.x * CHUNK_SIZEf + geometry.x * SHADOW_GEOMETRY_SQUARE_SIZE,
|
||
renderOrigin.y * CHUNK_SIZEf + geometry.y * SHADOW_GEOMETRY_SQUARE_SIZE,
|
||
renderOrigin.x * CHUNK_SIZEf + (geometry.x + 1) * SHADOW_GEOMETRY_SQUARE_SIZE,
|
||
renderOrigin.y * CHUNK_SIZEf + (geometry.y + 1) * SHADOW_GEOMETRY_SQUARE_SIZE)
|
||
) {
|
||
if (!setOnce) {
|
||
program.localToWorldTransform =
|
||
Matrix4f.identity().translateWithMultiplication(
|
||
Vector3f(x = renderOrigin.x * CHUNK_SIZEf,
|
||
y = renderOrigin.y * CHUNK_SIZEf
|
||
))
|
||
|
||
setOnce = true
|
||
}
|
||
|
||
geometry.renderSoftGeometry(renderer, lightPosition, lightRadius, stack, program)
|
||
}
|
||
}
|
||
}
|
||
|
||
fun addLayers(layers: LayeredRenderer) {
|
||
for ((baked, zLevel) in backgroundRenderer.bakedMeshes) {
|
||
layers.add(zLevel + Z_LEVEL_BACKGROUND) {
|
||
it.push().last().translateWithMultiplication(renderOrigin.x * CHUNK_SIZEf, renderOrigin.y * CHUNK_SIZEf)
|
||
baked.renderStacked(it)
|
||
it.pop()
|
||
}
|
||
}
|
||
|
||
for ((baked, zLevel) in foregroundRenderer.bakedMeshes) {
|
||
layers.add(zLevel) {
|
||
it.push().last().translateWithMultiplication(renderOrigin.x * CHUNK_SIZEf, renderOrigin.y * CHUNK_SIZEf)
|
||
baked.renderStacked(it)
|
||
it.pop()
|
||
}
|
||
}
|
||
|
||
for (renderer in entityRenderers.values) {
|
||
layers.add(renderer.layer) {
|
||
val relative = renderer.renderPos - posVector2d
|
||
it.push().last().translateWithMultiplication(renderOrigin.x * CHUNK_SIZEf + relative.x.toFloat(), renderOrigin.y * CHUNK_SIZEf + relative.y.toFloat())
|
||
renderer.render(it)
|
||
it.pop()
|
||
}
|
||
}
|
||
|
||
layers.add(Z_LEVEL_LIQUID) {
|
||
it.push().last().translateWithMultiplication(renderOrigin.x * CHUNK_SIZEf, renderOrigin.y * CHUNK_SIZEf)
|
||
val types = ArrayList<LiquidDefinition>()
|
||
|
||
for (x in 0 until CHUNK_SIZE) {
|
||
for (y in 0 until CHUNK_SIZE) {
|
||
val state = getCell(x, y)
|
||
|
||
if (state.liquid.def != null && !types.any { it === state.liquid.def }) {
|
||
types.add(state.liquid.def!!)
|
||
}
|
||
}
|
||
}
|
||
|
||
val program = state.programs.liquid
|
||
|
||
program.use()
|
||
program.transform = it.last()
|
||
|
||
val builder = program.builder
|
||
|
||
for (type in types) {
|
||
builder.builder.begin()
|
||
|
||
for (x in 0 until CHUNK_SIZE) {
|
||
for (y in 0 until CHUNK_SIZE) {
|
||
val state = getCell(x, y)
|
||
|
||
if (state.liquid.def === type) {
|
||
builder.builder.quad(x.toFloat(), y.toFloat(), x + 1f, y + state.liquid.level)
|
||
}
|
||
}
|
||
}
|
||
|
||
program.baselineColor = type.color
|
||
|
||
builder.upload()
|
||
builder.draw()
|
||
}
|
||
|
||
it.pop()
|
||
}
|
||
}
|
||
}
|
||
|
||
private val entityRenderers = HashMap<Entity, EntityRenderer>()
|
||
|
||
override fun onEntityAdded(entity: Entity) {
|
||
entityRenderers[entity] = EntityRenderer.getRender(world.client, 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
|
||
}
|
||
}
|