Большие изменения в рендере

This commit is contained in:
DBotThePony 2022-02-04 19:34:48 +07:00
parent 5b62fe3f09
commit ff6dba143e
Signed by: DBot
GPG Key ID: DCC23B5715498507
17 changed files with 687 additions and 343 deletions

View File

@ -10,35 +10,17 @@ import ru.dbotthepony.kstarbound.client.render.Camera
import ru.dbotthepony.kstarbound.client.render.ChunkRenderer
import ru.dbotthepony.kstarbound.client.render.TextAlignX
import ru.dbotthepony.kstarbound.client.render.TextAlignY
import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.util.Color
import ru.dbotthepony.kstarbound.util.formatBytesShort
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos
import java.io.File
import java.util.*
private val LOGGER = LogManager.getLogger()
var viewportWidth = 800
private set
var viewportHeight = 600
private set
var viewportMatrixGUI = updateViewportMatrixA()
private set
var viewportMatrixGame = updateViewportMatrixB()
private set
private fun updateViewportMatrixA(): Matrix4f {
return Matrix4f.ortho(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat(), 0.1f, 100f)
}
private fun updateViewportMatrixB(): Matrix4f {
return Matrix4f.orthoDirect(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat(), 1f, 100f)
}
var window = 0L
private set
fun main() {
LOGGER.info("Running LWJGL ${Version.getVersion()}")
@ -54,128 +36,61 @@ fun main() {
Starbound.terminateLoading = true
}
while (client.renderFrame()) {
Starbound.pollCallbacks()
}
}
var chunkA: Chunk? = null
private var camera: Camera? = null
private val startupTextList = ArrayList<String>()
private var finishStartupRendering = Long.MAX_VALUE
var frameRenderTime = 1.0
private set
val framesPerSecond get() = 1.0 / frameRenderTime
private fun loop() {
val client = StarboundClient()
val state = client.gl
startupTextList.add("Initialized OpenGL context")
camera = Camera()
// Set the clear color
glClearColor(0.2f, 0.2f, 0.2f, 0.2f)
state.blend = true
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
var chunkRenderer: ChunkRenderer? = null
/*Starbound.onInitialize {
val chunk = Starbound.world.getOrMakeChunk(Vector2i(2, 2))
Starbound.onInitialize {
chunkA = client.world!!.computeIfAbsent(ChunkPos(0, 0)).chunk
val chunkB = client.world!!.computeIfAbsent(ChunkPos(-1, 0)).chunk
var x = 0
var y = 0
for (tile in Starbound.tilesAccess.values) {
chunk.background[x, y + 1] = tile
chunk.background[x++, y] = tile
chunk.background[x, y + 1] = tile
chunk.background[x++, y] = tile
chunk.background[x, y + 1] = tile
chunk.background[x++, y] = tile
chunk.background[x, y + 1] = tile
chunk.background[x++, y] = tile
chunk.background[x, y + 1] = tile
chunk.background[x++, y] = tile
chunk.background[x, y + 1] = tile
chunkA!!.background[x, y + 1] = tile
chunkA!!.background[x++, y] = tile
if (x >= 32) {
if (x >= 31) {
x = 0
y += 2
}
}
val tile = Starbound.getTileDefinition("glass")
x = 0
y = 0
for (x in 0 .. 32) {
for (y in 0 .. 32) {
chunk.foreground[x, y] = tile
for (tile in Starbound.tilesAccess.values) {
chunkB.foreground[x, y + 1] = tile
chunkB.foreground[x++, y] = tile
if (x > 31) {
x = 0
y += 2
}
}
val tile = Starbound.getTileDefinition("alienrock")
for (x in 0 .. 31) {
for (y in 0 .. 31) {
chunkA!!.foreground[x, y] = tile
}
}
for (x in 4 .. 8) {
for (y in 4 .. 8) {
chunk.foreground[x, y] = null as TileDefinition?
chunkA!!.foreground[x, y] = null as TileDefinition?
}
}
}
chunkRenderer = ChunkRenderer(state, chunk, Starbound.world)
chunkRenderer!!.tesselateStatic()
chunkRenderer!!.uploadStatic()
}*/
val rand = Random()
val runtime = Runtime.getRuntime()
// Run the rendering loop until the user has attempted to close
// the window or has pressed the ESCAPE key.
while (!glfwWindowShouldClose(window)) {
val measure = glfwGetTime()
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) // clear the framebuffer
state.matrixStack.clear(viewportMatrixGame.toMutableMatrix())
camera?.translate(state.matrixStack.last)
state.matrixStack.push().scale(x = 20f, y = 20f).translateWithScale(0f, 0f)
chunkRenderer?.render()
state.matrixStack.clear(viewportMatrixGUI.toMutableMatrix().translate(z = 2f))
state.font.render("FPS: %.2f".format(framesPerSecond), scale = 0.4f)
state.font.render("Mem: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", x = viewportWidth.toFloat(), scale = 0.4f, alignX = TextAlignX.RIGHT)
val thisTime = System.currentTimeMillis()
if (startupTextList.isNotEmpty() && thisTime <= finishStartupRendering) {
var alpha = 1f
if (finishStartupRendering - thisTime < 1000L) {
alpha = (finishStartupRendering - thisTime) / 1000f
}
state.matrixStack.push()
state.matrixStack.translateWithScale(y = viewportHeight.toFloat())
var shade = 255
for (i in startupTextList.size - 1 downTo 0) {
val size = state.font.render(startupTextList[i], alignY = TextAlignY.BOTTOM, scale = 0.4f, color = Color.SHADES_OF_GRAY[shade].copy(alpha = alpha))
state.matrixStack.translateWithScale(y = -size.height * 1.2f)
if (shade > 120) {
shade -= 10
}
}
state.matrixStack.pop()
}
glfwSwapBuffers(window) // swap the color buffers
// Poll for window events. The key callback above will only be
// invoked during this call.
glfwPollEvents()
while (client.renderFrame()) {
Starbound.pollCallbacks()
frameRenderTime = glfwGetTime() - measure
if (chunkA != null && glfwGetTime() < 10.0) {
val tile = Starbound.getTileDefinition("alienrock")
//chunkA!!.foreground[rand.nextInt(0, CHUNK_SIZE_FF), rand.nextInt(0, CHUNK_SIZE_FF)] = tile
}
}
}

View File

@ -9,6 +9,12 @@ import ru.dbotthepony.kstarbound.world.World
import java.io.File
import java.io.FileNotFoundException
const val METRES_IN_STARBOUND_UNIT = 0.5
const val METRES_IN_STARBOUND_UNITf = 0.5f
const val PIXELS_IN_STARBOUND_UNIT = 8.0
const val PIXELS_IN_STARBOUND_UNITf = 8.0f
class TileDefLoadingException(message: String, cause: Throwable? = null) : IllegalStateException(message, cause)
object Starbound {

View File

@ -0,0 +1,12 @@
package ru.dbotthepony.kstarbound.client
data class ClientSettings(
/**
* Масштаб игрового мира
*
* Масштаб в единицу означает что один Starbound Unit будет равен 8 пикселям на экране
*/
var scale: Float = 2f
) {
}

View File

@ -0,0 +1,75 @@
package ru.dbotthepony.kstarbound.client
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.api.IStruct2f
import ru.dbotthepony.kstarbound.client.render.ChunkRenderer
import ru.dbotthepony.kstarbound.world.*
class ClientWorldChunkTuple(
world: World<*>,
chunk: Chunk,
top: IWorldChunkTuple?,
left: IWorldChunkTuple?,
right: IWorldChunkTuple?,
bottom: IWorldChunkTuple?,
val renderer: ChunkRenderer
) : MutableWorldChunkTuple(
world,
chunk,
top,
left,
right,
bottom,
)
class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWorldChunkTuple>(seed) {
override fun tupleFactory(
chunk: Chunk,
top: IWorldChunkTuple?,
left: IWorldChunkTuple?,
right: IWorldChunkTuple?,
bottom: IWorldChunkTuple?
): ClientWorldChunkTuple {
return ClientWorldChunkTuple(
world = this,
chunk = chunk,
top = top,
left = left,
right = right,
bottom = bottom,
renderer = ChunkRenderer(client.gl, chunk)
)
}
/**
* Отрисовывает этот мир с точки зрения [pos] в Starbound Units
*
* Все координаты "местности" сохраняются, поэтому, если отрисовывать слишком далеко от 0, 0
* то геометрия может начать искажаться из-за погрешности плавающей запятой
*
* Обрезает всю заведомо невидимую геометрию на основе аргументов mins и maxs (в пикселях)
*/
fun render(
pos: IStruct2f,
scale: Float = 1f,
mins: IStruct2f,
maxs: IStruct2f,
) {
val determineRenderers = ArrayList<ChunkRenderer>()
for (chunk in chunkMap.values) {
determineRenderers.add(chunk.renderer)
}
for (renderer in determineRenderers) {
val (x, y) = renderer.chunk.pos
client.gl.matrixStack.push().translateWithScale(x = x * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf, y = y * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf)
renderer.bakeAndRender()
client.gl.matrixStack.pop()
}
}
}

View File

@ -12,13 +12,16 @@ import ru.dbotthepony.kstarbound.math.Matrix4f
import ru.dbotthepony.kstarbound.client.render.Camera
import ru.dbotthepony.kstarbound.client.render.TextAlignX
import ru.dbotthepony.kstarbound.client.render.TextAlignY
import ru.dbotthepony.kstarbound.math.Vector2f
import ru.dbotthepony.kstarbound.util.Color
import ru.dbotthepony.kstarbound.util.formatBytesShort
import ru.dbotthepony.kstarbound.world.World
import kotlin.math.cos
import kotlin.math.sin
class StarboundClient : AutoCloseable {
val window: Long
val camera = Camera()
var world: ClientWorld? = ClientWorld(this, 0L)
var gameTerminated = false
private set
@ -140,7 +143,20 @@ class StarboundClient : AutoCloseable {
val framesPerSecond get() = 1.0 / frameRenderTime
var world: World? = World()
private val frameRenderTimes = DoubleArray(60) { 1.0 }
private var frameRenderIndex = 0
val averageFramesPerSecond: Double get() {
var sum = 0.0
for (value in frameRenderTimes) {
sum += value
}
return frameRenderTimes.size / sum
}
val settings = ClientSettings()
fun renderFrame(): Boolean {
ensureSameThread()
@ -153,9 +169,18 @@ class StarboundClient : AutoCloseable {
val measure = GLFW.glfwGetTime()
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
gl.matrixStack.clear(viewportMatrixGame.toMutableMatrix())
camera.translate(gl.matrixStack.last)
val mins = Vector2f((-viewportWidth / 2f) / settings.scale, (-viewportHeight / 2f) / settings.scale)
val maxs = -mins
gl.matrixStack.push()
.translateWithScale(viewportWidth / 2f - camera.pos.x, viewportHeight / 2f - camera.pos.y) // центр экрана + координаты отрисовки мира
.scale(x = settings.scale, y = settings.scale) // масштабируем до нужного размера
world?.render(Vector2f.ZERO, mins = mins, maxs = maxs)
gl.matrixStack.pop()
gl.matrixStack.clear(viewportMatrixGUI.toMutableMatrix().translate(z = 2f))
@ -186,13 +211,18 @@ class StarboundClient : AutoCloseable {
val runtime = Runtime.getRuntime()
gl.font.render("FPS: %.2f".format(framesPerSecond), scale = 0.4f)
gl.font.render("FPS: %.2f".format(averageFramesPerSecond), scale = 0.4f)
gl.font.render("Mem: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", x = viewportWidth.toFloat(), scale = 0.4f, alignX = TextAlignX.RIGHT)
GLFW.glfwSwapBuffers(window)
GLFW.glfwPollEvents()
camera.tick(GLFW.glfwGetTime() - measure)
gl.cleanup()
frameRenderTime = GLFW.glfwGetTime() - measure
frameRenderTimes[++frameRenderIndex % frameRenderTimes.size] = frameRenderTime
return true
}

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.client.gl
import org.apache.logging.log4j.LogManager
import org.lwjgl.opengl.GL
import org.lwjgl.opengl.GL46.*
import ru.dbotthepony.kstarbound.Starbound
@ -12,6 +13,8 @@ import ru.dbotthepony.kstarbound.client.render.TileRenderer
import ru.dbotthepony.kstarbound.client.render.TileRenderers
import ru.dbotthepony.kstarbound.util.Color
import java.io.File
import java.lang.ref.Cleaner
import java.util.concurrent.ThreadFactory
import kotlin.reflect.KProperty
private class GLStateSwitchTracker(private val enum: Int, private var value: Boolean = false) {
@ -73,6 +76,13 @@ open class GLTransformableColorableProgram(state: GLStateTracker, vararg shaders
}
}
interface GLCleanable : Cleaner.Cleanable {
/**
* Выставляет флаг на то, что объект был удалён вручную и вызывает clean()
*/
fun cleanManual(): Unit
}
class GLStateTracker {
init {
// This line is critical for LWJGL's interoperation with GLFW's
@ -83,6 +93,47 @@ class GLStateTracker {
GL.createCapabilities()
}
private var cleanerHits = ArrayList<() -> Unit>()
private val cleaner = Cleaner.create(object : ThreadFactory {
override fun newThread(r: Runnable): Thread {
val thread = Thread(r, "OpenGL Object Cleaner@" + System.identityHashCode(this))
thread.priority = 2
return thread
}
})
fun registerCleanable(ref: Any, fn: (Int) -> Unit, name: String, nativeRef: Int): GLCleanable {
var cleanManual = false
val cleanable = cleaner.register(ref) {
cleanerHits.add {
fn(nativeRef)
checkForGLError()
if (!cleanManual)
LOGGER.error("{} with ID {} got leaked.", name, nativeRef)
}
}
return object : GLCleanable {
override fun cleanManual() {
cleanManual = true
clean()
}
override fun clean() = cleanable.clean()
}
}
fun cleanup() {
val copy = cleanerHits
cleanerHits = ArrayList()
for (lambda in copy) {
lambda.invoke()
}
}
var blend by GLStateSwitchTracker(GL_BLEND)
var depthTest by GLStateSwitchTracker(GL_DEPTH_TEST)
@ -282,4 +333,8 @@ class GLStateTracker {
val freeType = FreeType()
val font = Font(this)
companion object {
private val LOGGER = LogManager.getLogger(GLStateTracker::class.java)
}
}

View File

@ -31,6 +31,12 @@ class GLTexturePropertyTracker(private val flag: Int, var value: Int) {
class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : AutoCloseable {
val pointer = glGenTextures()
init {
checkForGLError()
}
private val cleanable = state.registerCleanable(this, ::glDeleteTextures, "2D Texture", pointer)
var width = 0
private set
@ -154,8 +160,7 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : A
state.texture2D = null
}
glDeleteTextures(pointer)
checkForGLError()
cleanable.cleanManual()
isValid = false
}

View File

@ -6,6 +6,12 @@ import java.io.Closeable
class GLVertexArrayObject(val state: GLStateTracker) : Closeable {
val pointer = glGenVertexArrays()
init {
checkForGLError()
}
private val cleanable = state.registerCleanable(this, ::glDeleteVertexArrays, "Vertex Array Object", pointer)
fun bind(): GLVertexArrayObject {
check(isValid) { "Tried to use NULL GLVertexArrayObject" }
return state.bind(this)
@ -38,13 +44,15 @@ class GLVertexArrayObject(val state: GLStateTracker) : Closeable {
override fun close() {
state.ensureSameThread()
if (isValid) return
if (!isValid) return
if (state.VAO == this) {
state.VAO = null
}
glDeleteVertexArrays(pointer)
cleanable.cleanManual()
isValid = false
}
}

View File

@ -12,6 +12,12 @@ enum class VBOType(val value: Int) {
class GLVertexBufferObject(val state: GLStateTracker, val type: VBOType = VBOType.ARRAY) : Closeable {
val pointer = glGenBuffers()
init {
checkForGLError()
}
private val cleanable = state.registerCleanable(this, ::glDeleteBuffers, "Vertex Buffer Object", pointer)
val isArray get() = type == VBOType.ARRAY
val isElementArray get() = type == VBOType.ELEMENT_ARRAY
@ -72,13 +78,15 @@ class GLVertexBufferObject(val state: GLStateTracker, val type: VBOType = VBOTyp
override fun close() {
state.ensureSameThread()
if (!isValid) return
if (state.VBO == this) {
state.VBO = null
}
glDeleteBuffers(pointer)
cleanable.cleanManual()
isValid = false
}
}

View File

@ -1,14 +1,11 @@
package ru.dbotthepony.kstarbound.client.render
import org.lwjgl.opengl.GL11
import org.lwjgl.opengl.GL46.*
import ru.dbotthepony.kstarbound.client.gl.GLShaderProgram
import ru.dbotthepony.kstarbound.client.gl.GLVertexArrayObject
import ru.dbotthepony.kstarbound.client.gl.VertexBuilder
import ru.dbotthepony.kstarbound.client.gl.checkForGLError
import ru.dbotthepony.kstarbound.gl.*
import ru.dbotthepony.kstarbound.math.FloatMatrix
import ru.dbotthepony.kstarbound.math.Matrix4f
/**
* Служит для быстрой настройки состояния для будущей отрисовки
@ -47,6 +44,7 @@ class BakedStaticMesh(
val vao: GLVertexArrayObject,
) : AutoCloseable {
private var onClose = {}
constructor(programState: BakedProgramState, builder: VertexBuilder) : this(
programState,
builder.indexCount,

View File

@ -1,21 +1,55 @@
package ru.dbotthepony.kstarbound.client.render
import org.lwjgl.glfw.GLFW.*
import ru.dbotthepony.kstarbound.math.*
class Camera {
/**
* Позиция этой камеры в Starbound Unit'ах
*/
val pos = MutableVector3f()
var zoom = 1f
fun translate(stack: FloatMatrix<*>) {
stack.translateWithScale(pos)
stack.scale(x = zoom, y = zoom)
}
var pressedLeft = false
private set
var pressedRight = false
private set
var pressedUp = false
private set
var pressedDown = false
private set
fun userInput(key: Int, scancode: Int, action: Int, mods: Int) {
when (key) {
GLFW_KEY_LEFT -> pressedLeft = action > 0
GLFW_KEY_RIGHT -> pressedRight = action > 0
GLFW_KEY_UP -> pressedUp = action > 0
GLFW_KEY_DOWN -> pressedDown = action > 0
}
}
fun tick(delta: Double) {
if (pressedLeft) {
pos.x -= (delta * FREEVIEW_SENS).toFloat()
}
if (pressedRight) {
pos.x += (delta * FREEVIEW_SENS).toFloat()
}
if (pressedUp) {
pos.y += (delta * FREEVIEW_SENS).toFloat()
}
if (pressedDown) {
pos.y -= (delta * FREEVIEW_SENS).toFloat()
}
}
companion object {
const val FREEVIEW_SENS = 800.0
const val MAX_ZOOM = 4f
const val MIN_ZOOM = 0.1f
const val ZOOM_STEP = 0.1f

View File

@ -1,19 +1,88 @@
package ru.dbotthepony.kstarbound.client.render
import ru.dbotthepony.kstarbound.client.ClientWorld
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.gl.*
import ru.dbotthepony.kstarbound.math.FloatMatrix
import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ITileChunk
import ru.dbotthepony.kstarbound.world.World
import kotlin.collections.ArrayList
class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: World? = null) : AutoCloseable {
private val foregroundLayers = TileLayerList()
private val backgroundLayers = TileLayerList()
private val bakedMeshes = ArrayList<BakedStaticMesh>()
class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: ClientWorld? = null) : AutoCloseable {
private inner class TileLayerRenderer(private val layerChangeset: () -> Int, private val isBackground: Boolean) : AutoCloseable {
private val layers = TileLayerList()
private val bakedMeshes = ArrayList<BakedStaticMesh>()
private var changeset = -1
fun tesselateStatic(view: ITileChunk) {
if (state.isSameThread()) {
for (mesh in bakedMeshes) {
mesh.close()
}
bakedMeshes.clear()
} else {
unloadableBakedMeshes.addAll(bakedMeshes)
bakedMeshes.clear()
}
layers.clear()
for ((pos, tile) in view.posToTile) {
if (tile != null) {
val renderer = state.tileRenderers.get(tile.def.materialName)
renderer.tesselate(view, layers, pos, background = isBackground)
}
}
}
fun loadRenderers(view: ITileChunk) {
for ((_, tile) in view.posToTile) {
if (tile != null) {
state.tileRenderers.get(tile.def.materialName)
}
}
}
fun uploadStatic(clear: Boolean = true) {
for ((baked, builder) in layers.buildList()) {
bakedMeshes.add(BakedStaticMesh(baked, builder))
}
if (clear) {
layers.clear()
}
}
fun render(transform: FloatMatrix<*>) {
for (mesh in bakedMeshes) {
mesh.render(transform)
}
}
fun bakeAndRender(transform: FloatMatrix<*>, provider: () -> ITileChunk) {
if (changeset != layerChangeset.invoke()) {
this.tesselateStatic(provider.invoke())
this.uploadStatic()
changeset = layerChangeset.invoke()
}
render(transform)
}
override fun close() {
for (mesh in bakedMeshes) {
mesh.close()
}
}
}
private val bakedMeshesForeground = ArrayList<BakedStaticMesh>()
private val unloadableBakedMeshes = ArrayList<BakedStaticMesh>()
private val foreground = TileLayerRenderer(chunk.foreground::changeset, isBackground = false)
private val background = TileLayerRenderer(chunk.background::changeset, isBackground = true)
private fun getForeground(): ITileChunk {
return world?.getForegroundView(chunk.pos) ?: chunk.foreground
}
@ -29,39 +98,8 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Worl
* но только если до этого был вызыван loadRenderers() и геометрия чанка не поменялась
*/
fun tesselateStatic() {
if (state.isSameThread()) {
for (mesh in bakedMeshes) {
mesh.close()
}
bakedMeshes.clear()
} else {
unloadableBakedMeshes.addAll(bakedMeshes)
bakedMeshes.clear()
}
foregroundLayers.clear()
val foreground = getForeground()
// TODO: Синхронизация (ибо обновления игровой логики будут в потоке вне рендер потока)
for ((pos, tile) in foreground.posToTile) {
if (tile != null) {
val renderer = state.tileRenderers.get(tile.def.materialName)
renderer.tesselate(foreground, foregroundLayers, pos)
}
}
backgroundLayers.clear()
val background = getBackground()
for ((pos, tile) in background.posToTile) {
if (tile != null) {
val renderer = state.tileRenderers.get(tile.def.materialName)
renderer.tesselate(background, backgroundLayers, pos, background = true)
}
}
foreground.tesselateStatic(getForeground())
background.tesselateStatic(getBackground())
}
/**
@ -72,18 +110,8 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Worl
fun loadRenderers() {
unloadUnused()
// TODO: Синхронизация (ибо обновления игровой логики будут в потоке вне рендер потока)
for ((_, tile) in getForeground().posToTile) {
if (tile != null) {
state.tileRenderers.get(tile.def.materialName)
}
}
for ((_, tile) in getBackground().posToTile) {
if (tile != null) {
state.tileRenderers.get(tile.def.materialName)
}
}
foreground.loadRenderers(getForeground())
background.loadRenderers(getBackground())
}
private fun unloadUnused() {
@ -99,35 +127,26 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Worl
fun uploadStatic(clear: Boolean = true) {
unloadUnused()
for ((baked, builder) in backgroundLayers.buildList()) {
bakedMeshes.add(BakedStaticMesh(baked, builder))
}
for ((baked, builder) in foregroundLayers.buildList()) {
bakedMeshes.add(BakedStaticMesh(baked, builder))
}
if (clear) {
backgroundLayers.clear()
foregroundLayers.clear()
}
foreground.uploadStatic(clear)
background.uploadStatic(clear)
}
fun render(transform: FloatMatrix<*> = state.matrixStack.last) {
unloadUnused()
for (mesh in bakedMeshes) {
mesh.render(transform)
}
background.render(transform)
foreground.render(transform)
}
fun bakeAndRender(transform: FloatMatrix<*> = state.matrixStack.last) {
unloadUnused()
background.bakeAndRender(transform, this::getBackground)
foreground.bakeAndRender(transform, this::getForeground)
}
override fun close() {
for (mesh in bakedMeshes) {
mesh.close()
}
for (mesh in unloadableBakedMeshes) {
mesh.close()
}
background.close()
foreground.close()
}
}

View File

@ -253,9 +253,9 @@ class Font(
val advanceX: Float
val advanceY: Float
private val vbo: GLVertexBufferObject?
private val ebo: GLVertexBufferObject?
private val vao: GLVertexArrayObject?
private val vbo: GLVertexBufferObject? // все три указателя должны хранится во избежание утечки
private val ebo: GLVertexBufferObject? // все три указателя должны хранится во избежание утечки
private val vao: GLVertexArrayObject? // все три указателя должны хранится во избежание утечки
private val indexCount: Int

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.client.render
import org.apache.logging.log4j.LogManager
import org.lwjgl.opengl.GL46.*
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.gl.*
import ru.dbotthepony.kstarbound.defs.TileDefinition
@ -169,34 +170,36 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) {
if (offset != Vector2i.ZERO) {
a += offset.x / BASELINE_TEXTURE_SIZE
// в json файлах y указан как положительный вверх
// в json файлах y указан как положительный вверх,
// что соответствует нашему миру
b += offset.y / BASELINE_TEXTURE_SIZE
c += offset.x / BASELINE_TEXTURE_SIZE
d += offset.y / BASELINE_TEXTURE_SIZE
}
/*
if (!notifiedDepth && tile.render.zLevel >= 5900) {
LOGGER.warn("Tile {} has out of bounds zLevel of {}", tile.materialName, tile.render.zLevel)
notifiedDepth = true
}
*/
if (tile.render.variants == 0 || piece.texture != null || piece.variantStride == null) {
val (u0, v0) = texture.pixelToUV(piece.texturePosition)
val (u1, v1) = texture.pixelToUV(piece.texturePosition + piece.textureSize)
//builder.quadZ(a, b, c, d, tile.render.zLevel.toFloat() + 200f, VertexTransformers.uv(u0, v1, u1, v0))
builder.quadZ(a, b, c, d, Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0))
builder.quadZ(
a * PIXELS_IN_STARBOUND_UNITf,
b * PIXELS_IN_STARBOUND_UNITf,
c * PIXELS_IN_STARBOUND_UNITf,
d * PIXELS_IN_STARBOUND_UNITf,
Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0))
} else {
val variant = (getter.randomDoubleFor(pos) * tile.render.variants).toInt()
val (u0, v0) = texture.pixelToUV(piece.texturePosition + piece.variantStride * variant)
val (u1, v1) = texture.pixelToUV(piece.texturePosition + piece.textureSize + piece.variantStride * variant)
//builder.quadZ(a, b, c, d, tile.render.zLevel.toFloat() + 200f, VertexTransformers.uv(u0, v1, u1, v0))
builder.quadZ(a, b, c, d, Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0))
builder.quadZ(
a * PIXELS_IN_STARBOUND_UNITf,
b * PIXELS_IN_STARBOUND_UNITf,
c * PIXELS_IN_STARBOUND_UNITf,
d * PIXELS_IN_STARBOUND_UNITf,
Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0))
}
}
@ -245,7 +248,7 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) {
*
* [layers] содержит текущие программы и их билдеры и их zPos
*
* Тесселирует тайлы в границы -1f .. CHUNK_SIZEf + 1f на основе [pos]
* Тесселирует тайлы в нужный VertexBuilder с масштабом согласно константе [PIXELS_IN_STARBOUND_UNITf]
*/
fun tesselate(getter: ITileChunk, layers: TileLayerList, pos: Vector2i, background: Boolean = false) {
// если у нас нет renderTemplate

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.math
import com.google.gson.JsonArray
import ru.dbotthepony.kstarbound.api.IStruct2f
import ru.dbotthepony.kstarbound.api.IStruct2i
import ru.dbotthepony.kstarbound.api.IStruct3f
import ru.dbotthepony.kstarbound.api.IStruct4f
@ -15,13 +16,16 @@ abstract class IVector2i<T : IVector2i<T>> : IMatrixLike, IMatrixLikeInt, IStruc
abstract val y: Int
operator fun plus(other: IVector2i<*>) = make(x + other.x, y + other.y)
operator fun plus(other: Int) = make(x + other, y + other)
operator fun minus(other: IVector2i<*>) = make(x - other.x, y - other.y)
operator fun minus(other: Int) = make(x - other, y - other)
operator fun times(other: IVector2i<*>) = make(x * other.x, y * other.y)
operator fun times(other: Int) = make(x * other, y * other)
operator fun div(other: IVector2i<*>) = make(x / other.x, y / other.y)
operator fun div(other: Int) = make(x / other, y / other)
operator fun times(other: Int) = make(x * other, y * other)
operator fun minus(other: Int) = make(x - other, y - other)
operator fun plus(other: Int) = make(x + other, y + other)
operator fun unaryMinus() = make(-x, -y)
fun left() = make(x - 1, y)
fun right() = make(x + 1, y)
@ -59,6 +63,89 @@ data class Vector2i(override val x: Int = 0, override val y: Int = 0) : IVector2
}
}
data class MutableVector2i(override var x: Int = 0, override var y: Int = 0) : IVector2i<MutableVector2i>() {
override fun make(x: Int, y: Int): MutableVector2i {
this.x = x
this.y = y
return this
}
companion object {
fun fromJson(input: JsonArray): MutableVector2i {
return MutableVector2i(input[0].asInt, input[1].asInt)
}
}
}
abstract class IVector2f<T : IVector2f<T>> : IMatrixLike, IMatrixLikeFloat, IStruct2f {
override val columns = 1
override val rows = 2
abstract val x: Float
abstract val y: Float
operator fun plus(other: IVector2f<*>) = make(x + other.x, y + other.y)
operator fun minus(other: IVector2f<*>) = make(x - other.x, y - other.y)
operator fun times(other: IVector2f<*>) = make(x * other.x, y * other.y)
operator fun div(other: IVector2f<*>) = make(x / other.x, y / other.y)
operator fun plus(other: Float) = make(x + other, y + other)
operator fun minus(other: Float) = make(x - other, y - other)
operator fun times(other: Float) = make(x * other, y * other)
operator fun div(other: Float) = make(x / other, y / other)
operator fun unaryMinus() = make(-x, -y)
fun left() = make(x - 1, y)
fun right() = make(x + 1, y)
fun up() = make(x, y + 1)
fun down() = make(x, y - 1)
override fun get(row: Int, column: Int): Float {
if (column != 0) {
throw IndexOutOfBoundsException("Column must be 0 ($column given)")
}
return when (row) {
0 -> x
1 -> y
else -> throw IndexOutOfBoundsException("Row out of bounds: $row")
}
}
protected abstract fun make(x: Float, y: Float): T
}
data class Vector2f(override val x: Float = 0f, override val y: Float = 0f) : IVector2f<Vector2f>() {
override fun make(x: Float, y: Float) = Vector2f(x, y)
companion object {
fun fromJson(input: JsonArray): Vector2f {
return Vector2f(input[0].asFloat, input[1].asFloat)
}
val ZERO = Vector2f()
val LEFT = Vector2f().left()
val RIGHT = Vector2f().right()
val UP = Vector2f().up()
val DOWN = Vector2f().down()
}
}
data class MutableVector2f(override var x: Float = 0f, override var y: Float = 0f) : IVector2f<MutableVector2f>() {
override fun make(x: Float, y: Float): MutableVector2f {
this.x = x
this.y = y
return this
}
companion object {
fun fromJson(input: JsonArray): MutableVector2f {
return MutableVector2f(input[0].asFloat, input[1].asFloat)
}
}
}
abstract class IVector3f<T : IVector3f<T>> : IMatrixLike, IMatrixLikeFloat, IStruct3f {
override val columns = 1
override val rows = 3
@ -77,6 +164,8 @@ abstract class IVector3f<T : IVector3f<T>> : IMatrixLike, IMatrixLikeFloat, IStr
operator fun times(other: Float) = make(x * other, y * other, z * other)
operator fun div(other: Float) = make(x / other, y / other, z / other)
operator fun unaryMinus() = make(-x, -y, -z)
override fun get(row: Int, column: Int): Float {
if (column != 0) {
throw IndexOutOfBoundsException("Column must be 0 ($column given)")
@ -190,6 +279,8 @@ abstract class IVector4f<T : IVector4f<T>> : IMatrixLike, IMatrixLikeFloat, IStr
operator fun times(other: Float) = make(x * other, y * other, z * other, w * other)
operator fun div(other: Float) = make(x / other, y / other, z / other, w / other)
operator fun unaryMinus() = make(-x, -y, -z, -w)
override val columns = 1
override val rows = 4

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.api.IStruct2i
import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.math.IVector2i
import ru.dbotthepony.kstarbound.math.Vector2i
@ -7,9 +8,18 @@ import ru.dbotthepony.kstarbound.math.Vector2i
/**
* Представляет из себя класс, который содержит состояние тайла на заданной позиции
*/
data class ChunkTile(val def: TileDefinition) {
var color = -1
data class ChunkTile(val chunk: Chunk.TileLayer, val def: TileDefinition) {
var color = 0
set(value) {
field = value
chunk.incChangeset()
}
var forceVariant = -1
set(value) {
field = value
chunk.incChangeset()
}
}
interface ITileMap {
@ -134,16 +144,23 @@ interface ITileGetterSetter : ITileGetter, ITileSetter
interface ITileChunk : ITileGetter, IChunkPositionable
interface IMutableTileChunk : ITileChunk, ITileSetter
const val CHUNK_SHIFT = 6
const val CHUNK_SIZE = 1 shl CHUNK_SHIFT // 64
const val CHUNK_SHIFT = 5
const val CHUNK_SIZE = 1 shl CHUNK_SHIFT // 32
const val CHUNK_SIZE_FF = CHUNK_SIZE - 1
data class ChunkPos(override val x: Int, override val y: Int) : IVector2i<ChunkPos>() {
constructor(pos: Vector2i) : this(pos.x shr CHUNK_SHIFT, pos.y shr CHUNK_SHIFT)
constructor(pos: IStruct2i) : this(pos.component1(), pos.component2())
override fun make(x: Int, y: Int) = ChunkPos(x, y)
val firstBlock get() = Vector2i(x shl CHUNK_SHIFT, y shl CHUNK_SHIFT)
val lastBlock get() = Vector2i(((x + 1) shl CHUNK_SHIFT) - 1, ((y + 1) shl CHUNK_SHIFT) - 1)
companion object {
fun fromTilePosition(input: IStruct2i): ChunkPos {
val (x, y) = input
return ChunkPos(x shr CHUNK_SHIFT, y shr CHUNK_SHIFT)
}
}
}
/**
@ -256,8 +273,39 @@ class MutableTileChunkView(
}
}
open class Chunk(val world: World, val pos: ChunkPos) {
inner class ChunkTileLayer : IMutableTileChunk {
/**
* Чанк мира
*
* Хранит в себе тайлы и ентити внутри себя
*
* Считается, что один тайл имеет форму квадрата и сторона квадрата примерно равна полуметру,
* что будет называться Starbound Unit
*
* Весь игровой мир будет измеряться в Starbound Unit'ах
*/
open class Chunk(val world: World<*>?, val pos: ChunkPos) {
/**
* Возвращает счётчик изменений чанка
*/
var changeset = 0
private set
fun incChangeset() {
changeset++
}
inner class TileLayer : IMutableTileChunk {
/**
* Возвращает счётчик изменений этого слоя
*/
var changeset = 0
private set
fun incChangeset() {
changeset++
this@Chunk.changeset++
}
override val pos: ChunkPos
get() = this@Chunk.pos
@ -277,6 +325,7 @@ open class Chunk(val world: World, val pos: ChunkPos) {
if (isOutside(x, y))
throw IndexOutOfBoundsException("Trying to set tile ${tile?.def?.materialName} at $x $y, but that is outside of chunk's range")
changeset++
tiles[x or (y shl CHUNK_SHIFT)] = tile
}
@ -284,18 +333,19 @@ open class Chunk(val world: World, val pos: ChunkPos) {
if (isOutside(x, y))
throw IndexOutOfBoundsException("Trying to set tile ${tile?.materialName} at $x $y, but that is outside of chunk's range")
val chunkTile = if (tile != null) ChunkTile(tile) else null
val chunkTile = if (tile != null) ChunkTile(this, tile) else null
this[x, y] = chunkTile
changeset++
return chunkTile
}
override fun randomLongFor(x: Int, y: Int): Long {
return super.randomLongFor(x, y) xor world.seed
return super.randomLongFor(x, y) xor (world?.seed ?: 0L)
}
}
val foreground = ChunkTileLayer()
val background = ChunkTileLayer()
val foreground = TileLayer()
val background = TileLayer()
companion object {
val EMPTY = object : IMutableTileChunk {

View File

@ -3,139 +3,174 @@ package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.math.Vector2i
class World(val seed: Long = 0L) {
private val chunkMap = ArrayList<Pair<ChunkPos, Chunk>>()
private var lastAccessedChunk: Chunk? = null
/**
* Возвращает кортеж чанка, который содержит родителя (мир) и соседей (кортежи чанков)
*/
interface IWorldChunkTuple {
val world: World<*>
val chunk: Chunk
val top: IWorldChunkTuple?
val left: IWorldChunkTuple?
val right: IWorldChunkTuple?
val bottom: IWorldChunkTuple?
}
fun getChunk(pos: ChunkPos): Chunk? {
if (lastAccessedChunk?.pos == pos) {
interface IMutableWorldChunkTuple : IWorldChunkTuple {
override var top: IWorldChunkTuple?
override var left: IWorldChunkTuple?
override var right: IWorldChunkTuple?
override var bottom: IWorldChunkTuple?
}
data class WorldChunkTuple(
override val world: World<*>,
override val chunk: Chunk,
override val top: IWorldChunkTuple?,
override val left: IWorldChunkTuple?,
override val right: IWorldChunkTuple?,
override val bottom: IWorldChunkTuple?,
) : IWorldChunkTuple
open class MutableWorldChunkTuple(
override val world: World<*>,
override val chunk: Chunk,
override var top: IWorldChunkTuple?,
override var left: IWorldChunkTuple?,
override var right: IWorldChunkTuple?,
override var bottom: IWorldChunkTuple?,
) : IMutableWorldChunkTuple
@Suppress("WeakerAccess")
abstract class World<T : IMutableWorldChunkTuple>(val seed: Long = 0L) {
protected val chunkMap = HashMap<ChunkPos, T>()
protected var lastAccessedChunk: T? = null
protected abstract fun tupleFactory(
chunk: Chunk,
top: IWorldChunkTuple?,
left: IWorldChunkTuple?,
right: IWorldChunkTuple?,
bottom: IWorldChunkTuple?,
): T
protected fun getChunkInternal(pos: ChunkPos): T? {
if (lastAccessedChunk?.chunk?.pos == pos) {
return lastAccessedChunk
}
for ((k, v) in chunkMap) {
if (k == pos) {
lastAccessedChunk = v
return v
}
}
return chunkMap[pos]
}
open fun getChunk(pos: ChunkPos): IWorldChunkTuple? {
val getTuple = getChunkInternal(pos)
if (getTuple != null)
return WorldChunkTuple(
world = getTuple.world,
chunk = getTuple.chunk,
top = getTuple.top,
left = getTuple.left,
right = getTuple.right,
bottom = getTuple.bottom,
)
return null
}
fun getOrMakeChunk(pos: ChunkPos): Chunk {
if (lastAccessedChunk?.pos == pos) {
protected open fun computeIfAbsentInternal(pos: ChunkPos): T {
if (lastAccessedChunk?.chunk?.pos == pos) {
return lastAccessedChunk!!
}
for ((k, v) in chunkMap) {
if (k == pos) {
return v
}
return chunkMap.computeIfAbsent(pos) lazy@{
val chunk = Chunk(this, pos)
val top = getChunkInternal(pos.up())
val left = getChunkInternal(pos.left())
val right = getChunkInternal(pos.right())
val bottom = getChunkInternal(pos.down())
val tuple = tupleFactory(
chunk = chunk,
top = top,
left = left,
right = right,
bottom = bottom,
)
top?.bottom = tuple
left?.right = tuple
right?.left = tuple
bottom?.top = tuple
lastAccessedChunk = tuple
return@lazy tuple
}
val chunk = Chunk(this, pos)
lastAccessedChunk = chunk
chunkMap.add(pos to chunk)
return chunk
}
fun getForegroundView(pos: ChunkPos): TileChunkView? {
val get = getChunk(pos) ?: return null
open fun computeIfAbsent(pos: ChunkPos): IWorldChunkTuple {
val getTuple = computeIfAbsentInternal(pos)
return TileChunkView(
center = get.foreground,
left = getChunk(pos.left())?.foreground,
top = getChunk(pos.up())?.foreground,
topLeft = getChunk(pos.up().left())?.foreground,
topRight = getChunk(pos.up().right())?.foreground,
right = getChunk(pos.right())?.foreground,
bottom = getChunk(pos.down())?.foreground,
bottomLeft = getChunk(pos.down().left())?.foreground,
bottomRight = getChunk(pos.down().right())?.foreground,
return WorldChunkTuple(
world = getTuple.world,
chunk = getTuple.chunk,
top = getTuple.top,
left = getTuple.left,
right = getTuple.right,
bottom = getTuple.bottom,
)
}
fun getBackgroundView(pos: ChunkPos): TileChunkView? {
val get = getChunk(pos) ?: return null
open fun getForegroundView(pos: ChunkPos): TileChunkView? {
val get = getChunkInternal(pos) ?: return null
return TileChunkView(
center = get.background,
left = getChunk(pos.left())?.background,
top = getChunk(pos.up())?.background,
topLeft = getChunk(pos.up().left())?.background,
topRight = getChunk(pos.up().right())?.background,
right = getChunk(pos.right())?.background,
bottom = getChunk(pos.down())?.background,
bottomLeft = getChunk(pos.down().left())?.background,
bottomRight = getChunk(pos.down().right())?.background,
center = get.chunk.foreground,
left = get.left?.chunk?.foreground,
top = get.top?.chunk?.foreground,
topLeft = getChunkInternal(pos.up().left())?.chunk?.foreground,
topRight = getChunkInternal(pos.up().right())?.chunk?.foreground,
right = get.right?.chunk?.foreground,
bottom = get.bottom?.chunk?.foreground,
bottomLeft = getChunkInternal(pos.down().left())?.chunk?.foreground,
bottomRight = getChunkInternal(pos.down().right())?.chunk?.foreground,
)
}
/**
* Считается, что [pos] это абсолютные координаты ТАЙЛА в мире, поэтому они
* трансформируются в координаты чанка
*/
fun getChunk(pos: Vector2i) = getChunk(ChunkPos(pos))
open fun getBackgroundView(pos: ChunkPos): TileChunkView? {
val get = getChunkInternal(pos) ?: return null
/**
* Считается, что [pos] это абсолютные координаты ТАЙЛА в мире, поэтому они
* трансформируются в координаты чанка
*/
fun getOrMakeChunk(pos: Vector2i) = getOrMakeChunk(ChunkPos(pos))
/**
* Считается, что [pos] это абсолютные координаты ТАЙЛА в мире, поэтому они
* трансформируются в координаты чанка
*/
fun getForegroundView(pos: Vector2i) = getForegroundView(ChunkPos(pos))
/**
* Считается, что [pos] это абсолютные координаты ТАЙЛА в мире, поэтому они
* трансформируются в координаты чанка
*/
fun getBackgroundView(pos: Vector2i) = getBackgroundView(ChunkPos(pos))
/**
* Считается, что [x] и [y] это абсолютные координаты ЧАНКА в мире, поэтому они
* НЕ трансформируются в координаты чанка, а используются напрямую
*/
fun getChunk(x: Int, y: Int) = getChunk(ChunkPos(x, y))
/**
* Считается, что [x] и [y] это абсолютные координаты ЧАНКА в мире, поэтому они
* НЕ трансформируются в координаты чанка, а используются напрямую
*/
fun getOrMakeChunk(x: Int, y: Int) = getOrMakeChunk(ChunkPos(x, y))
/**
* Считается, что [x] и [y] это абсолютные координаты ЧАНКА в мире, поэтому они
* НЕ трансформируются в координаты чанка, а используются напрямую
*/
fun getForegroundView(x: Int, y: Int) = getForegroundView(ChunkPos(x, y))
/**
* Считается, что [x] и [y] это абсолютные координаты ЧАНКА в мире, поэтому они
* НЕ трансформируются в координаты чанка, а используются напрямую
*/
fun getBackgroundView(x: Int, y: Int) = getBackgroundView(ChunkPos(x, y))
return TileChunkView(
center = get.chunk.background,
left = get.left?.chunk?.background,
top = get.top?.chunk?.background,
topLeft = getChunkInternal(pos.up().left())?.chunk?.background,
topRight = getChunkInternal(pos.up().right())?.chunk?.background,
right = get.right?.chunk?.background,
bottom = get.bottom?.chunk?.background,
bottomLeft = getChunkInternal(pos.down().left())?.chunk?.background,
bottomRight = getChunkInternal(pos.down().right())?.chunk?.background,
)
}
fun getTile(pos: Vector2i): ChunkTile? {
return getChunk(pos)?.foreground?.get(pos.x, pos.y)
return getChunkInternal(ChunkPos.fromTilePosition(pos))?.chunk?.foreground?.get(pos.x, pos.y)
}
fun setTile(pos: Vector2i, tile: TileDefinition?): Chunk {
val chunk = getOrMakeChunk(pos)
chunk.foreground[pos.x and CHUNK_SIZE_FF, pos.y and CHUNK_SIZE_FF] = tile
fun setTile(pos: Vector2i, tile: TileDefinition?): IWorldChunkTuple {
val chunk = computeIfAbsentInternal(ChunkPos.fromTilePosition(pos))
chunk.chunk.foreground[pos.x and CHUNK_SIZE_FF, pos.y and CHUNK_SIZE_FF] = tile
return chunk
}
fun getBackgroundTile(pos: Vector2i): ChunkTile? {
return getChunk(pos)?.background?.get(pos.x, pos.y)
return getChunkInternal(ChunkPos.fromTilePosition(pos))?.chunk?.background?.get(pos.x, pos.y)
}
fun setBackgroundTile(pos: Vector2i, tile: TileDefinition?): Chunk {
val chunk = getOrMakeChunk(pos)
chunk.background[pos.x and CHUNK_SIZE_FF, pos.y and CHUNK_SIZE_FF] = tile
fun setBackgroundTile(pos: Vector2i, tile: TileDefinition?): IWorldChunkTuple {
val chunk = computeIfAbsentInternal(ChunkPos.fromTilePosition(pos))
chunk.chunk.background[pos.x and CHUNK_SIZE_FF, pos.y and CHUNK_SIZE_FF] = tile
return chunk
}
}