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

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.ChunkRenderer
import ru.dbotthepony.kstarbound.client.render.TextAlignX import ru.dbotthepony.kstarbound.client.render.TextAlignX
import ru.dbotthepony.kstarbound.client.render.TextAlignY import ru.dbotthepony.kstarbound.client.render.TextAlignY
import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.util.Color import ru.dbotthepony.kstarbound.util.Color
import ru.dbotthepony.kstarbound.util.formatBytesShort 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.io.File
import java.util.*
private val LOGGER = LogManager.getLogger() 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() { fun main() {
LOGGER.info("Running LWJGL ${Version.getVersion()}") LOGGER.info("Running LWJGL ${Version.getVersion()}")
@ -54,128 +36,61 @@ fun main() {
Starbound.terminateLoading = true Starbound.terminateLoading = true
} }
while (client.renderFrame()) { var chunkA: Chunk? = null
Starbound.pollCallbacks()
}
}
private var camera: Camera? = null Starbound.onInitialize {
private val startupTextList = ArrayList<String>() chunkA = client.world!!.computeIfAbsent(ChunkPos(0, 0)).chunk
private var finishStartupRendering = Long.MAX_VALUE val chunkB = client.world!!.computeIfAbsent(ChunkPos(-1, 0)).chunk
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))
var x = 0 var x = 0
var y = 0 var y = 0
for (tile in Starbound.tilesAccess.values) { for (tile in Starbound.tilesAccess.values) {
chunk.background[x, y + 1] = tile chunkA!!.background[x, y + 1] = tile
chunk.background[x++, y] = tile chunkA!!.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
if (x >= 32) { if (x >= 31) {
x = 0 x = 0
y += 2 y += 2
} }
} }
val tile = Starbound.getTileDefinition("glass") x = 0
y = 0
for (x in 0 .. 32) { for (tile in Starbound.tilesAccess.values) {
for (y in 0 .. 32) { chunkB.foreground[x, y + 1] = tile
chunk.foreground[x, y] = 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 (x in 4 .. 8) {
for (y 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) val rand = Random()
chunkRenderer!!.tesselateStatic()
chunkRenderer!!.uploadStatic()
}*/
val runtime = Runtime.getRuntime() while (client.renderFrame()) {
// 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()
Starbound.pollCallbacks() 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.File
import java.io.FileNotFoundException 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) class TileDefLoadingException(message: String, cause: Throwable? = null) : IllegalStateException(message, cause)
object Starbound { 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.Camera
import ru.dbotthepony.kstarbound.client.render.TextAlignX import ru.dbotthepony.kstarbound.client.render.TextAlignX
import ru.dbotthepony.kstarbound.client.render.TextAlignY import ru.dbotthepony.kstarbound.client.render.TextAlignY
import ru.dbotthepony.kstarbound.math.Vector2f
import ru.dbotthepony.kstarbound.util.Color import ru.dbotthepony.kstarbound.util.Color
import ru.dbotthepony.kstarbound.util.formatBytesShort import ru.dbotthepony.kstarbound.util.formatBytesShort
import ru.dbotthepony.kstarbound.world.World import kotlin.math.cos
import kotlin.math.sin
class StarboundClient : AutoCloseable { class StarboundClient : AutoCloseable {
val window: Long val window: Long
val camera = Camera() val camera = Camera()
var world: ClientWorld? = ClientWorld(this, 0L)
var gameTerminated = false var gameTerminated = false
private set private set
@ -140,7 +143,20 @@ class StarboundClient : AutoCloseable {
val framesPerSecond get() = 1.0 / frameRenderTime 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 { fun renderFrame(): Boolean {
ensureSameThread() ensureSameThread()
@ -153,9 +169,18 @@ class StarboundClient : AutoCloseable {
val measure = GLFW.glfwGetTime() val measure = GLFW.glfwGetTime()
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
gl.matrixStack.clear(viewportMatrixGame.toMutableMatrix()) 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)) gl.matrixStack.clear(viewportMatrixGUI.toMutableMatrix().translate(z = 2f))
@ -186,13 +211,18 @@ class StarboundClient : AutoCloseable {
val runtime = Runtime.getRuntime() 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) gl.font.render("Mem: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", x = viewportWidth.toFloat(), scale = 0.4f, alignX = TextAlignX.RIGHT)
GLFW.glfwSwapBuffers(window) GLFW.glfwSwapBuffers(window)
GLFW.glfwPollEvents() GLFW.glfwPollEvents()
camera.tick(GLFW.glfwGetTime() - measure)
gl.cleanup()
frameRenderTime = GLFW.glfwGetTime() - measure frameRenderTime = GLFW.glfwGetTime() - measure
frameRenderTimes[++frameRenderIndex % frameRenderTimes.size] = frameRenderTime
return true return true
} }

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.client.gl package ru.dbotthepony.kstarbound.client.gl
import org.apache.logging.log4j.LogManager
import org.lwjgl.opengl.GL import org.lwjgl.opengl.GL
import org.lwjgl.opengl.GL46.* import org.lwjgl.opengl.GL46.*
import ru.dbotthepony.kstarbound.Starbound 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.client.render.TileRenderers
import ru.dbotthepony.kstarbound.util.Color import ru.dbotthepony.kstarbound.util.Color
import java.io.File import java.io.File
import java.lang.ref.Cleaner
import java.util.concurrent.ThreadFactory
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
private class GLStateSwitchTracker(private val enum: Int, private var value: Boolean = false) { 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 { class GLStateTracker {
init { init {
// This line is critical for LWJGL's interoperation with GLFW's // This line is critical for LWJGL's interoperation with GLFW's
@ -83,6 +93,47 @@ class GLStateTracker {
GL.createCapabilities() 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 blend by GLStateSwitchTracker(GL_BLEND)
var depthTest by GLStateSwitchTracker(GL_DEPTH_TEST) var depthTest by GLStateSwitchTracker(GL_DEPTH_TEST)
@ -282,4 +333,8 @@ class GLStateTracker {
val freeType = FreeType() val freeType = FreeType()
val font = Font(this) 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 { class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : AutoCloseable {
val pointer = glGenTextures() val pointer = glGenTextures()
init {
checkForGLError()
}
private val cleanable = state.registerCleanable(this, ::glDeleteTextures, "2D Texture", pointer)
var width = 0 var width = 0
private set private set
@ -154,8 +160,7 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : A
state.texture2D = null state.texture2D = null
} }
glDeleteTextures(pointer) cleanable.cleanManual()
checkForGLError()
isValid = false isValid = false
} }

View File

@ -6,6 +6,12 @@ import java.io.Closeable
class GLVertexArrayObject(val state: GLStateTracker) : Closeable { class GLVertexArrayObject(val state: GLStateTracker) : Closeable {
val pointer = glGenVertexArrays() val pointer = glGenVertexArrays()
init {
checkForGLError()
}
private val cleanable = state.registerCleanable(this, ::glDeleteVertexArrays, "Vertex Array Object", pointer)
fun bind(): GLVertexArrayObject { fun bind(): GLVertexArrayObject {
check(isValid) { "Tried to use NULL GLVertexArrayObject" } check(isValid) { "Tried to use NULL GLVertexArrayObject" }
return state.bind(this) return state.bind(this)
@ -38,13 +44,15 @@ class GLVertexArrayObject(val state: GLStateTracker) : Closeable {
override fun close() { override fun close() {
state.ensureSameThread() state.ensureSameThread()
if (isValid) return
if (!isValid) return
if (state.VAO == this) { if (state.VAO == this) {
state.VAO = null state.VAO = null
} }
glDeleteVertexArrays(pointer) cleanable.cleanManual()
isValid = false 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 { class GLVertexBufferObject(val state: GLStateTracker, val type: VBOType = VBOType.ARRAY) : Closeable {
val pointer = glGenBuffers() val pointer = glGenBuffers()
init {
checkForGLError()
}
private val cleanable = state.registerCleanable(this, ::glDeleteBuffers, "Vertex Buffer Object", pointer)
val isArray get() = type == VBOType.ARRAY val isArray get() = type == VBOType.ARRAY
val isElementArray get() = type == VBOType.ELEMENT_ARRAY val isElementArray get() = type == VBOType.ELEMENT_ARRAY
@ -72,13 +78,15 @@ class GLVertexBufferObject(val state: GLStateTracker, val type: VBOType = VBOTyp
override fun close() { override fun close() {
state.ensureSameThread() state.ensureSameThread()
if (!isValid) return if (!isValid) return
if (state.VBO == this) { if (state.VBO == this) {
state.VBO = null state.VBO = null
} }
glDeleteBuffers(pointer) cleanable.cleanManual()
isValid = false isValid = false
} }
} }

View File

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

View File

@ -1,21 +1,55 @@
package ru.dbotthepony.kstarbound.client.render package ru.dbotthepony.kstarbound.client.render
import org.lwjgl.glfw.GLFW.*
import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.math.*
class Camera { class Camera {
/**
* Позиция этой камеры в Starbound Unit'ах
*/
val pos = MutableVector3f() val pos = MutableVector3f()
var zoom = 1f
fun translate(stack: FloatMatrix<*>) { var pressedLeft = false
stack.translateWithScale(pos) private set
stack.scale(x = zoom, y = zoom)
} 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) { 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 { companion object {
const val FREEVIEW_SENS = 800.0
const val MAX_ZOOM = 4f const val MAX_ZOOM = 4f
const val MIN_ZOOM = 0.1f const val MIN_ZOOM = 0.1f
const val ZOOM_STEP = 0.1f const val ZOOM_STEP = 0.1f

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.math package ru.dbotthepony.kstarbound.math
import com.google.gson.JsonArray import com.google.gson.JsonArray
import ru.dbotthepony.kstarbound.api.IStruct2f
import ru.dbotthepony.kstarbound.api.IStruct2i import ru.dbotthepony.kstarbound.api.IStruct2i
import ru.dbotthepony.kstarbound.api.IStruct3f import ru.dbotthepony.kstarbound.api.IStruct3f
import ru.dbotthepony.kstarbound.api.IStruct4f import ru.dbotthepony.kstarbound.api.IStruct4f
@ -15,13 +16,16 @@ abstract class IVector2i<T : IVector2i<T>> : IMatrixLike, IMatrixLikeInt, IStruc
abstract val y: Int abstract val y: Int
operator fun plus(other: IVector2i<*>) = make(x + other.x, y + other.y) 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: 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: 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: IVector2i<*>) = make(x / other.x, y / other.y)
operator fun div(other: Int) = make(x / other, y / other) 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 left() = make(x - 1, y)
fun right() = 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 { abstract class IVector3f<T : IVector3f<T>> : IMatrixLike, IMatrixLikeFloat, IStruct3f {
override val columns = 1 override val columns = 1
override val rows = 3 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 times(other: Float) = make(x * other, y * other, z * other)
operator fun div(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 { override fun get(row: Int, column: Int): Float {
if (column != 0) { if (column != 0) {
throw IndexOutOfBoundsException("Column must be 0 ($column given)") 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 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 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 columns = 1
override val rows = 4 override val rows = 4

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.api.IStruct2i
import ru.dbotthepony.kstarbound.defs.TileDefinition import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.math.IVector2i import ru.dbotthepony.kstarbound.math.IVector2i
import ru.dbotthepony.kstarbound.math.Vector2i import ru.dbotthepony.kstarbound.math.Vector2i
@ -7,9 +8,18 @@ import ru.dbotthepony.kstarbound.math.Vector2i
/** /**
* Представляет из себя класс, который содержит состояние тайла на заданной позиции * Представляет из себя класс, который содержит состояние тайла на заданной позиции
*/ */
data class ChunkTile(val def: TileDefinition) { data class ChunkTile(val chunk: Chunk.TileLayer, val def: TileDefinition) {
var color = -1 var color = 0
set(value) {
field = value
chunk.incChangeset()
}
var forceVariant = -1 var forceVariant = -1
set(value) {
field = value
chunk.incChangeset()
}
} }
interface ITileMap { interface ITileMap {
@ -134,16 +144,23 @@ interface ITileGetterSetter : ITileGetter, ITileSetter
interface ITileChunk : ITileGetter, IChunkPositionable interface ITileChunk : ITileGetter, IChunkPositionable
interface IMutableTileChunk : ITileChunk, ITileSetter interface IMutableTileChunk : ITileChunk, ITileSetter
const val CHUNK_SHIFT = 6 const val CHUNK_SHIFT = 5
const val CHUNK_SIZE = 1 shl CHUNK_SHIFT // 64 const val CHUNK_SIZE = 1 shl CHUNK_SHIFT // 32
const val CHUNK_SIZE_FF = CHUNK_SIZE - 1 const val CHUNK_SIZE_FF = CHUNK_SIZE - 1
data class ChunkPos(override val x: Int, override val y: Int) : IVector2i<ChunkPos>() { 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) override fun make(x: Int, y: Int) = ChunkPos(x, y)
val firstBlock get() = Vector2i(x shl CHUNK_SHIFT, y shl CHUNK_SHIFT) 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) 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 override val pos: ChunkPos
get() = this@Chunk.pos get() = this@Chunk.pos
@ -277,6 +325,7 @@ open class Chunk(val world: World, val pos: ChunkPos) {
if (isOutside(x, y)) if (isOutside(x, y))
throw IndexOutOfBoundsException("Trying to set tile ${tile?.def?.materialName} at $x $y, but that is outside of chunk's range") 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 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)) if (isOutside(x, y))
throw IndexOutOfBoundsException("Trying to set tile ${tile?.materialName} at $x $y, but that is outside of chunk's range") 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 this[x, y] = chunkTile
changeset++
return chunkTile return chunkTile
} }
override fun randomLongFor(x: Int, y: Int): Long { 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 foreground = TileLayer()
val background = ChunkTileLayer() val background = TileLayer()
companion object { companion object {
val EMPTY = object : IMutableTileChunk { 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.defs.TileDefinition
import ru.dbotthepony.kstarbound.math.Vector2i 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? { interface IMutableWorldChunkTuple : IWorldChunkTuple {
if (lastAccessedChunk?.pos == pos) { 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 return lastAccessedChunk
} }
for ((k, v) in chunkMap) { return chunkMap[pos]
if (k == pos) { }
lastAccessedChunk = v
return v 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 return null
} }
fun getOrMakeChunk(pos: ChunkPos): Chunk { protected open fun computeIfAbsentInternal(pos: ChunkPos): T {
if (lastAccessedChunk?.pos == pos) { if (lastAccessedChunk?.chunk?.pos == pos) {
return lastAccessedChunk!! return lastAccessedChunk!!
} }
for ((k, v) in chunkMap) { return chunkMap.computeIfAbsent(pos) lazy@{
if (k == pos) { val chunk = Chunk(this, pos)
return v
} 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? { open fun computeIfAbsent(pos: ChunkPos): IWorldChunkTuple {
val get = getChunk(pos) ?: return null val getTuple = computeIfAbsentInternal(pos)
return TileChunkView( return WorldChunkTuple(
center = get.foreground, world = getTuple.world,
left = getChunk(pos.left())?.foreground, chunk = getTuple.chunk,
top = getChunk(pos.up())?.foreground, top = getTuple.top,
topLeft = getChunk(pos.up().left())?.foreground, left = getTuple.left,
topRight = getChunk(pos.up().right())?.foreground, right = getTuple.right,
right = getChunk(pos.right())?.foreground, bottom = getTuple.bottom,
bottom = getChunk(pos.down())?.foreground,
bottomLeft = getChunk(pos.down().left())?.foreground,
bottomRight = getChunk(pos.down().right())?.foreground,
) )
} }
fun getBackgroundView(pos: ChunkPos): TileChunkView? { open fun getForegroundView(pos: ChunkPos): TileChunkView? {
val get = getChunk(pos) ?: return null val get = getChunkInternal(pos) ?: return null
return TileChunkView( return TileChunkView(
center = get.background, center = get.chunk.foreground,
left = getChunk(pos.left())?.background, left = get.left?.chunk?.foreground,
top = getChunk(pos.up())?.background, top = get.top?.chunk?.foreground,
topLeft = getChunk(pos.up().left())?.background, topLeft = getChunkInternal(pos.up().left())?.chunk?.foreground,
topRight = getChunk(pos.up().right())?.background, topRight = getChunkInternal(pos.up().right())?.chunk?.foreground,
right = getChunk(pos.right())?.background, right = get.right?.chunk?.foreground,
bottom = getChunk(pos.down())?.background, bottom = get.bottom?.chunk?.foreground,
bottomLeft = getChunk(pos.down().left())?.background, bottomLeft = getChunkInternal(pos.down().left())?.chunk?.foreground,
bottomRight = getChunk(pos.down().right())?.background, bottomRight = getChunkInternal(pos.down().right())?.chunk?.foreground,
) )
} }
/** open fun getBackgroundView(pos: ChunkPos): TileChunkView? {
* Считается, что [pos] это абсолютные координаты ТАЙЛА в мире, поэтому они val get = getChunkInternal(pos) ?: return null
* трансформируются в координаты чанка
*/
fun getChunk(pos: Vector2i) = getChunk(ChunkPos(pos))
/** return TileChunkView(
* Считается, что [pos] это абсолютные координаты ТАЙЛА в мире, поэтому они center = get.chunk.background,
* трансформируются в координаты чанка left = get.left?.chunk?.background,
*/ top = get.top?.chunk?.background,
fun getOrMakeChunk(pos: Vector2i) = getOrMakeChunk(ChunkPos(pos)) topLeft = getChunkInternal(pos.up().left())?.chunk?.background,
topRight = getChunkInternal(pos.up().right())?.chunk?.background,
/** right = get.right?.chunk?.background,
* Считается, что [pos] это абсолютные координаты ТАЙЛА в мире, поэтому они bottom = get.bottom?.chunk?.background,
* трансформируются в координаты чанка bottomLeft = getChunkInternal(pos.down().left())?.chunk?.background,
*/ bottomRight = getChunkInternal(pos.down().right())?.chunk?.background,
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))
fun getTile(pos: Vector2i): ChunkTile? { 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 { fun setTile(pos: Vector2i, tile: TileDefinition?): IWorldChunkTuple {
val chunk = getOrMakeChunk(pos) val chunk = computeIfAbsentInternal(ChunkPos.fromTilePosition(pos))
chunk.foreground[pos.x and CHUNK_SIZE_FF, pos.y and CHUNK_SIZE_FF] = tile chunk.chunk.foreground[pos.x and CHUNK_SIZE_FF, pos.y and CHUNK_SIZE_FF] = tile
return chunk return chunk
} }
fun getBackgroundTile(pos: Vector2i): ChunkTile? { 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 { fun setBackgroundTile(pos: Vector2i, tile: TileDefinition?): IWorldChunkTuple {
val chunk = getOrMakeChunk(pos) val chunk = computeIfAbsentInternal(ChunkPos.fromTilePosition(pos))
chunk.background[pos.x and CHUNK_SIZE_FF, pos.y and CHUNK_SIZE_FF] = tile chunk.chunk.background[pos.x and CHUNK_SIZE_FF, pos.y and CHUNK_SIZE_FF] = tile
return chunk return chunk
} }
} }