Liquid render!

This commit is contained in:
DBotThePony 2022-09-11 17:55:47 +07:00
parent 598530c4ec
commit 96c88aa725
Signed by: DBot
GPG Key ID: DCC23B5715498507
19 changed files with 339 additions and 20 deletions

View File

@ -123,7 +123,26 @@ fun main() {
chunk.background[x, y]?.setModifierHueShift(modifierHueShift2)
reader.skipBytes(17)
val liquid = reader.readUnsignedByte()
val liquidLevel = reader.readFloat()
val liquidPressure = reader.readFloat()
val liquidIsInfinite = reader.readBoolean()
val collisionMap = reader.readUnsignedByte()
val dungeonId = reader.readUnsignedShort()
val biome = reader.readUnsignedByte()
val envBiome = reader.readUnsignedByte()
val indestructible = reader.readBoolean()
val unknown = reader.readUnsignedByte()
val getLiquid = Starbound.liquidByIDAccess[liquid]
if (getLiquid != null) {
val state = chunk.setLiquid(x, y, getLiquid)!!
state.isInfinite = liquidIsInfinite
state.pressure = liquidPressure
state.level = liquidLevel
}
}
}
@ -144,8 +163,8 @@ fun main() {
//ent.position += Vector2d(y = 14.0, x = -10.0)
ent.position = Vector2d(600.0 + 16.0, 721.0 + 48.0)
client.camera.pos.x = 616f
client.camera.pos.y = 721f
client.camera.pos.x = 578f
client.camera.pos.y = 695f
client.onDrawGUI {
client.gl.font.render("${ent.position}", y = 100f, scale = 0.25f)

View File

@ -1,15 +1,18 @@
package ru.dbotthepony.kstarbound.client
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.client.render.ConfiguredStaticMesh
import ru.dbotthepony.kstarbound.client.render.EntityRenderer
import ru.dbotthepony.kstarbound.client.render.ILayeredRenderer
import ru.dbotthepony.kstarbound.client.render.TileLayerList
import ru.dbotthepony.kstarbound.defs.liquid.LiquidDefinition
import ru.dbotthepony.kstarbound.world.*
import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kvector.matrix.Matrix4fStack
import ru.dbotthepony.kvector.vector.ndouble.Vector2d
import java.io.Closeable
import java.util.IdentityHashMap
import java.util.LinkedList
/**
@ -19,6 +22,7 @@ import java.util.LinkedList
* первыми (на самом дальнем плане)
*/
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
@ -223,7 +227,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
* Должен быть использован только один раз, после выкинут, иначе поведение
* кода невозможно будет предсказать
*/
inner class BakedLayeredRenderer constructor(val origin: ChunkPos = pos) : ILayeredRenderer {
inner class OneShotRenderer constructor(val origin: ChunkPos = pos) : ILayeredRenderer {
private val layerQueue = ArrayDeque<Pair<(Matrix4fStack) -> Unit, Int>>()
init {
@ -241,10 +245,50 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
it.push().translateWithMultiplication(relative.x.toFloat(), relative.y.toFloat())
renderer.render(it)
it.pop()
return@lambda
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
}

View File

@ -101,7 +101,7 @@ class ClientWorld(
val determineRenderers = ArrayList<ILayeredRenderer>()
for (chunk in collectPositionAware(size.encasingChunkPosAABB())) {
determineRenderers.add(chunk.second.BakedLayeredRenderer(chunk.first))
determineRenderers.add(chunk.second.OneShotRenderer(chunk.first))
chunk.second.bake()
}

View File

@ -5,8 +5,9 @@ import org.lwjgl.opengl.GL
import org.lwjgl.opengl.GL46.*
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.freetype.FreeType
import ru.dbotthepony.kstarbound.client.gl.program.GLPrograms
import ru.dbotthepony.kstarbound.client.gl.shader.GLShader
import ru.dbotthepony.kstarbound.client.gl.shader.GLShaderProgram
import ru.dbotthepony.kstarbound.client.gl.program.GLShaderProgram
import ru.dbotthepony.kstarbound.client.gl.shader.GLTransformableColorableProgram
import ru.dbotthepony.kstarbound.client.gl.shader.GLTransformableProgram
import ru.dbotthepony.kstarbound.client.gl.vertex.GLAttributeList
@ -76,6 +77,8 @@ interface GLStreamBuilderList {
class GLStateTracker {
init {
check(TRACKERS.get() == null) { "Already has state tracker existing at this thread!" }
TRACKERS.set(this)
// This line is critical for LWJGL's interoperation with GLFW's
// OpenGL context, or any context that is managed externally.
// LWJGL detects the context that is current in the current thread,
@ -393,13 +396,17 @@ class GLStateTracker {
}
}
val quadWireframe by lazy {
StreamVertexBuilder(this, GLAttributeList.VEC2F, VertexType.QUADS_AS_LINES_WIREFRAME, 16384)
}
val matrixStack = Matrix4fStack()
val freeType = FreeType()
val font = Font(this)
inline fun quadWireframe(color: Color = Color.WHITE, lambda: (StreamVertexBuilder) -> Unit) {
val builder = flat2DQuadWireframe.small
val builder = quadWireframe
builder.begin()
lambda.invoke(builder)
@ -423,5 +430,6 @@ class GLStateTracker {
companion object {
private val LOGGER = LogManager.getLogger(GLStateTracker::class.java)
private val TRACKERS = ThreadLocal<GLStateTracker>()
}
}

View File

@ -0,0 +1,30 @@
package ru.dbotthepony.kstarbound.client.gl.program
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.client.gl.shader.GLShader
private fun loadShaders(
vertex: Collection<String>,
fragment: Collection<String>
): Array<GLShader> {
val result = arrayOfNulls<GLShader>(vertex.size + fragment.size)
var i = 0
for (name in vertex) {
result[i++] = GLShader.internalVertex("shaders/$name.vsh")
}
for (name in fragment) {
result[i++] = GLShader.internalFragment("shaders/$name.fsh")
}
return result as Array<GLShader>
}
open class GLInternalProgram(
state: GLStateTracker,
vertex: Collection<String>,
fragment: Collection<String>
) : GLShaderProgram(state, *loadShaders(vertex, fragment)) {
constructor(state: GLStateTracker, name: String) : this(state, listOf(name), listOf(name))
}

View File

@ -1,10 +1,12 @@
package ru.dbotthepony.kstarbound.client.gl.shader
package ru.dbotthepony.kstarbound.client.gl.program
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import org.lwjgl.opengl.GL46.*
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.client.gl.shader.GLShader
import ru.dbotthepony.kstarbound.client.gl.shader.GLUniformLocation
import ru.dbotthepony.kstarbound.client.gl.shader.ShaderLinkException
import ru.dbotthepony.kvector.api.IFloatMatrix
import ru.dbotthepony.kvector.api.IStruct3f
import ru.dbotthepony.kvector.api.IStruct4f
@ -12,6 +14,10 @@ import java.util.*
import kotlin.collections.HashSet
open class GLShaderProgram(val state: GLStateTracker, vararg shaders: GLShader) {
init {
state.ensureSameThread()
}
val pointer = glCreateProgram()
var linked = false
private set

View File

@ -1,8 +1,13 @@
package ru.dbotthepony.kstarbound.client.gl
package ru.dbotthepony.kstarbound.client.gl.program
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.client.gl.GLType
import ru.dbotthepony.kstarbound.client.gl.shader.GLShader
import ru.dbotthepony.kstarbound.client.gl.shader.GLShaderProgram
import ru.dbotthepony.kstarbound.client.gl.shader.GLTransformableColorableProgram
import ru.dbotthepony.kstarbound.client.gl.shader.GLTransformableProgram
import ru.dbotthepony.kstarbound.client.gl.vertex.GLAttributeList
import ru.dbotthepony.kstarbound.client.gl.vertex.StreamVertexBuilder
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexType
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
@ -10,6 +15,8 @@ private class SimpleProgram<T : GLShaderProgram>(private val name: String, priva
private var value: T? = null
override fun getValue(thisRef: GLPrograms, property: KProperty<*>): T {
thisRef.state.ensureSameThread()
val value = value
if (value != null) {
@ -34,8 +41,26 @@ private class SimpleProgram<T : GLShaderProgram>(private val name: String, priva
}
}
class GLLiquidProgram(state: GLStateTracker) : GLInternalProgram(state, "liquid") {
init {
link()
}
val baselineColor = this["baselineColor"]!!
val transform = this["transform"]!!
val builder by lazy {
StreamVertexBuilder(state, FORMAT, VertexType.QUADS, 16384)
}
companion object {
val FORMAT = GLAttributeList.Builder().push(GLType.VEC2F).build()
}
}
class GLPrograms(val state: GLStateTracker) {
val tile by SimpleProgram("tile", ::GLTransformableColorableProgram)
val font by SimpleProgram("font", ::GLTransformableColorableProgram)
val flat by SimpleProgram("flat", ::GLTransformableColorableProgram)
val liquid by lazy { GLLiquidProgram(state) }
}

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.client.gl.shader
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.client.gl.program.GLShaderProgram
import ru.dbotthepony.kvector.matrix.nfloat.Matrix4f
open class GLTransformableProgram(state: GLStateTracker, vararg shaders: GLShader) : GLShaderProgram(state, *shaders) {

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.client.gl.shader
import org.lwjgl.opengl.GL41.*
import ru.dbotthepony.kstarbound.client.gl.checkForGLError
import ru.dbotthepony.kstarbound.client.gl.program.GLShaderProgram
import ru.dbotthepony.kvector.api.IFloatMatrix
import ru.dbotthepony.kvector.api.IStruct3f
import ru.dbotthepony.kvector.api.IStruct4f
@ -14,6 +15,7 @@ class GLUniformLocation(val program: GLShaderProgram, val name: String, val poin
program.state.ensureSameThread()
val (v0, v1, v2, v3) = value
glProgramUniform4f(program.pointer, pointer, v0, v1, v2, v3)
checkForGLError()
return this
}
@ -21,12 +23,14 @@ class GLUniformLocation(val program: GLShaderProgram, val name: String, val poin
program.state.ensureSameThread()
val (v0, v1, v2) = value
glProgramUniform3f(program.pointer, pointer, v0, v1, v2)
checkForGLError()
return this
}
fun set(value: Int): GLUniformLocation {
program.state.ensureSameThread()
glProgramUniform1i(program.pointer, pointer, value)
checkForGLError()
return this
}
@ -56,4 +60,4 @@ class GLUniformLocation(val program: GLShaderProgram, val name: String, val poin
return this
}
}
}

View File

@ -87,9 +87,8 @@ class GLAttributeList(builder: Builder) {
val VERTEX_TEXTURE = Builder().push(GLType.VEC3F).push(GLType.VEC2F).build()
val VERTEX_HSV_TEXTURE = Builder()
.push(GLType.VEC3F, GLType.VEC2F, GLType.FLOAT).build()
val VERTEX_2D_TEXTURE = Builder().push(GLType.VEC2F).push(GLType.VEC2F).build()
val TILE = Builder().push(GLType.VEC3F, GLType.VEC2F, GLType.FLOAT).build()
}
}

View File

@ -1,7 +1,7 @@
package ru.dbotthepony.kstarbound.client.render
import org.lwjgl.opengl.GL46.*
import ru.dbotthepony.kstarbound.client.gl.shader.GLShaderProgram
import ru.dbotthepony.kstarbound.client.gl.program.GLShaderProgram
import ru.dbotthepony.kstarbound.client.gl.VertexArrayObject
import ru.dbotthepony.kstarbound.client.gl.checkForGLError
import ru.dbotthepony.kstarbound.client.gl.vertex.AbstractVertexBuilder

View File

@ -168,7 +168,7 @@ private enum class TileRenderTesselateResult {
HALT
}
private fun vertexTextureBuilder() = HeapVertexBuilder(GLAttributeList.VERTEX_HSV_TEXTURE, VertexType.QUADS)
private fun vertexTextureBuilder() = HeapVertexBuilder(GLAttributeList.TILE, VertexType.QUADS)
private class TileEqualityTester(val definition: TileDefinition) : EqualityRuleTester {
override fun test(thisTile: TileState?, otherTile: TileState?): Boolean {

View File

@ -0,0 +1,45 @@
package ru.dbotthepony.kstarbound.util
import java.util.Spliterator
import java.util.function.Consumer
class ArraySpliterator<T>(private val source: Array<T>, private val offset: Int = 0, private val size: Int = source.size) : Spliterator<T> {
init {
require(offset + size <= source.size) { "Invalid dimensions for spliterator: offset = $offset, size = $size, source has size of ${source.size}" }
}
private var index = 0
override fun tryAdvance(action: Consumer<in T>): Boolean {
if (index < size) {
action.accept(source[offset + index++])
return true
}
return false
}
override fun trySplit(): Spliterator<T>? {
if (index + 1 >= size) {
return null
}
val splitSize = size / 2
if (splitSize == 0) {
return null
}
val pointer = index + offset
index += splitSize
return ArraySpliterator(source, pointer, splitSize)
}
override fun estimateSize(): Long {
return (size - index).toLong()
}
override fun characteristics(): Int {
return Spliterator.ORDERED or Spliterator.SIZED
}
}

View File

@ -0,0 +1,48 @@
package ru.dbotthepony.kstarbound.util
import java.util.Spliterator
import java.util.function.Consumer
class IndexedArraySpliterator<T>(private val source: Array<T>, private val offset: Int = 0, private val size: Int = source.size) : Spliterator<IndexedArraySpliterator<T>.Entry> {
inner class Entry(val index: Int, val value: T)
init {
require(offset + size <= source.size) { "Invalid dimensions for spliterator: offset = $offset, size = $size, source has size of ${source.size}" }
}
private var index = 0
override fun tryAdvance(action: Consumer<in IndexedArraySpliterator<T>.Entry>): Boolean {
if (index < size) {
val pointer = offset + index++
action.accept(Entry(pointer, source[pointer]))
return true
}
return false
}
override fun trySplit(): Spliterator<IndexedArraySpliterator<T>.Entry>? {
if (index + 1 >= size) {
return null
}
val splitSize = size / 2
if (splitSize == 0) {
return null
}
val pointer = index + offset
index += splitSize
return IndexedArraySpliterator(source, pointer, splitSize)
}
override fun estimateSize(): Long {
return (size - index).toLong()
}
override fun characteristics(): Int {
return Spliterator.ORDERED or Spliterator.SIZED
}
}

View File

@ -1,8 +1,16 @@
package ru.dbotthepony.kstarbound.util
import java.util.stream.Stream
import java.util.stream.StreamSupport
import kotlin.reflect.KClass
class TwoDimensionalArray<T : Any>(clazz: KClass<T>, private val width: Int, private val height: Int) {
data class Entry<out T>(
val x: Int,
val y: Int,
val value: T,
)
private val memory: Array<T?> = java.lang.reflect.Array.newInstance(clazz.java, width * height) as Array<T?>
operator fun get(x: Int, y: Int): T? {
@ -17,7 +25,7 @@ class TwoDimensionalArray<T : Any>(clazz: KClass<T>, private val width: Int, pri
return memory[x + y * width]
}
operator fun set(x: Int, y: Int, value: T): T? {
operator fun set(x: Int, y: Int, value: T?): T? {
if (x !in 0 until width) {
throw IndexOutOfBoundsException("X $x is out of bounds between 0 and $width")
}
@ -30,6 +38,18 @@ class TwoDimensionalArray<T : Any>(clazz: KClass<T>, private val width: Int, pri
memory[x + y * width] = value
return old
}
fun stream(): Stream<out T?> {
return StreamSupport.stream(ArraySpliterator(memory), false)
}
fun indexedStream(): Stream<out Entry<T?>> {
return StreamSupport.stream(IndexedArraySpliterator(memory), false).map {
val x = it.index % width
val y = (it.index - x) / width
Entry(x, y, it.value)
}
}
}
inline fun <reified T : Any> TwoDimensionalArray(width: Int, height: Int) = TwoDimensionalArray(T::class, width, height)

View File

@ -122,7 +122,7 @@ class TileState(val chunk: Chunk<*, *>.TileLayer, val def: TileDefinition) {
}
}
data class LiquidState(val chunk: Chunk<*, *>.TileLayer, val def: LiquidDefinition) {
data class LiquidState(val chunk: Chunk<*, *>, val def: LiquidDefinition) {
var pressure: Float = 0f
var level: Float = 1f
var isInfinite: Boolean = false
@ -414,6 +414,18 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
protected val liquidStates: TwoDimensionalArray<LiquidState> = TwoDimensionalArray(CHUNK_SIZE, CHUNK_SIZE)
fun getLiquid(x: Int, y: Int) = liquidStates[x, y]
fun setLiquid(x: Int, y: Int, value: LiquidDefinition?): LiquidState? {
if (value == null) {
return liquidStates.set(x, y, null)
}
val state = LiquidState(this, value)
liquidStates[x, y] = state
return state
}
protected val entities = HashSet<Entity>()
val entitiesAccess: Set<Entity> = Collections.unmodifiableSet(entities)

View File

@ -0,0 +1,10 @@
#version 460
out vec4 resultColor;
uniform vec4 baselineColor;
void main() {
resultColor = vec4(baselineColor);
}

View File

@ -0,0 +1,10 @@
#version 460
layout (location = 0) in vec2 vertexPos;
uniform mat4 transform;
void main() {
gl_Position = transform * vec4(vertexPos, 5.0, 1.0);
}

View File

@ -0,0 +1,38 @@
package ru.dbotthepony.kstarbound.test
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import ru.dbotthepony.kstarbound.util.ArraySpliterator
import java.rmi.UnexpectedException
object MiscTests {
@Test
@DisplayName("Array Spliterator test")
fun arraySpliterator() {
val base = arrayOf(1, 2, 3, 4)
val spliterator = ArraySpliterator(base)
assertTrue(spliterator.tryAdvance { assertEquals(1, it) })
assertTrue(spliterator.tryAdvance { assertEquals(2, it) })
assertTrue(spliterator.tryAdvance { assertEquals(3, it) })
assertTrue(spliterator.tryAdvance { assertEquals(4, it) })
assertFalse(spliterator.tryAdvance { throw UnexpectedException("Unreachable code") })
}
@Test
@DisplayName("Array Spliterator split test")
fun arraySpliteratorSplit() {
val base = arrayOf(1, 2, 3, 4, 5, 6)
val spliterator = ArraySpliterator(base)
var i = 0
val splitted = spliterator.trySplit()!!
splitted.forEachRemaining { assertEquals(++i, it) }
spliterator.forEachRemaining { assertEquals(++i, it) }
assertEquals(6, i)
}
}