Большие изменения в рендере
This commit is contained in:
parent
5b62fe3f09
commit
ff6dba143e
src/main/kotlin/ru/dbotthepony/kstarbound
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -0,0 +1,12 @@
|
||||
package ru.dbotthepony.kstarbound.client
|
||||
|
||||
data class ClientSettings(
|
||||
/**
|
||||
* Масштаб игрового мира
|
||||
*
|
||||
* Масштаб в единицу означает что один Starbound Unit будет равен 8 пикселям на экране
|
||||
*/
|
||||
var scale: Float = 2f
|
||||
) {
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user