KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt

1036 lines
29 KiB
Kotlin

package ru.dbotthepony.kstarbound.client
import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import org.apache.logging.log4j.LogManager
import org.lwjgl.BufferUtils
import org.lwjgl.glfw.Callbacks
import org.lwjgl.glfw.GLFW
import org.lwjgl.glfw.GLFWErrorCallback
import org.lwjgl.opengl.GL
import org.lwjgl.opengl.GL46.*
import org.lwjgl.opengl.GLCapabilities
import org.lwjgl.system.MemoryStack
import org.lwjgl.system.MemoryUtil
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNIT
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.freetype.FreeType
import ru.dbotthepony.kstarbound.client.freetype.InvalidArgumentException
import ru.dbotthepony.kstarbound.client.gl.BlendFunc
import ru.dbotthepony.kstarbound.client.gl.GLFrameBuffer
import ru.dbotthepony.kstarbound.client.gl.GLTexture2D
import ru.dbotthepony.kstarbound.client.gl.ScissorRect
import ru.dbotthepony.kstarbound.client.gl.VBOType
import ru.dbotthepony.kstarbound.client.gl.VertexArrayObject
import ru.dbotthepony.kstarbound.client.gl.VertexBufferObject
import ru.dbotthepony.kstarbound.client.gl.checkForGLError
import ru.dbotthepony.kstarbound.client.gl.properties.GLStateFuncTracker
import ru.dbotthepony.kstarbound.client.gl.properties.GLStateGenericTracker
import ru.dbotthepony.kstarbound.client.gl.properties.GLStateIntTracker
import ru.dbotthepony.kstarbound.client.gl.properties.GLStateSwitchTracker
import ru.dbotthepony.kstarbound.client.gl.properties.TexturesTracker
import ru.dbotthepony.kstarbound.client.gl.shader.GLPrograms
import ru.dbotthepony.kstarbound.client.gl.shader.GLShader
import ru.dbotthepony.kstarbound.client.gl.shader.GLShaderProgram
import ru.dbotthepony.kstarbound.client.gl.vertex.GLAttributeList
import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType
import ru.dbotthepony.kstarbound.client.gl.vertex.QuadTransformers
import ru.dbotthepony.kstarbound.client.gl.vertex.StreamVertexBuilder
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder
import ru.dbotthepony.kstarbound.client.input.UserInput
import ru.dbotthepony.kstarbound.client.render.Box2DRenderer
import ru.dbotthepony.kstarbound.client.render.Camera
import ru.dbotthepony.kstarbound.client.render.Font
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.client.render.TextAlignY
import ru.dbotthepony.kstarbound.client.render.TileRenderers
import ru.dbotthepony.kstarbound.client.world.ClientWorld
import ru.dbotthepony.kstarbound.defs.image.Image
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
import ru.dbotthepony.kstarbound.util.JVMTimeSource
import ru.dbotthepony.kstarbound.util.formatBytesShort
import ru.dbotthepony.kstarbound.world.LightCalculator
import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.IChunkCell
import ru.dbotthepony.kvector.api.IStruct4f
import ru.dbotthepony.kvector.arrays.Matrix4f
import ru.dbotthepony.kvector.arrays.Matrix4fStack
import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.RGBAColor
import ru.dbotthepony.kvector.vector.Vector2d
import ru.dbotthepony.kvector.vector.Vector2f
import ru.dbotthepony.kvector.vector.Vector2i
import ru.dbotthepony.kvector.vector.Vector3f
import java.io.Closeable
import java.io.File
import java.lang.ref.Cleaner
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.time.Duration
import java.util.*
import java.util.concurrent.locks.LockSupport
import java.util.concurrent.locks.ReentrantLock
import kotlin.collections.ArrayList
import kotlin.math.roundToInt
class StarboundClient : Closeable {
val window: Long
val camera = Camera(this)
val input = UserInput()
val thread: Thread = Thread.currentThread()
val capabilities: GLCapabilities
var viewportX: Int = 0
private set
var viewportY: Int = 0
private set
var viewportWidth: Int = 0
private set
var viewportHeight: Int = 0
private set
var viewportCellX = 0
private set
var viewportCellY = 0
private set
var viewportCellWidth = 0
private set
var viewportCellHeight = 0
private set
var viewportRectangle = AABB.rectangle(Vector2d.ZERO, 0.0, 0.0)
private set
var fullbright = true
var gameTerminated = false
private set
/**
* Матрица преобразования экранных координат (в пикселях) в нормализованные координаты
*/
var viewportMatrixScreen: Matrix4f
private set
get() = Matrix4f.unmodifiable(field)
/**
* Матрица преобразования мировых координат в нормализованные координаты
*/
var viewportMatrixWorld: Matrix4f
private set
get() = Matrix4f.unmodifiable(field)
var isRenderingGame = true
private set
private val scissorStack = LinkedList<ScissorRect>()
private val cleanerBacklog = ArrayList<() -> Unit>()
private val onDrawGUI = ArrayList<() -> Unit>()
private val onPreDrawWorld = ArrayList<(LayeredRenderer) -> Unit>()
private val onPostDrawWorld = ArrayList<() -> Unit>()
private val onPostDrawWorldOnce = ArrayList<(LayeredRenderer) -> Unit>()
private val onViewportChanged = ArrayList<(width: Int, height: Int) -> Unit>()
private val terminateCallbacks = ArrayList<() -> Unit>()
private val startupTextList = ArrayList<String>()
private var finishStartupRendering = System.currentTimeMillis() + 4000L
private val cleaner = Cleaner.create { r ->
val thread = Thread(r, "OpenGL Cleaner for '${thread.name}'")
thread.priority = 2
thread
}
@Volatile
var objectsCleaned = 0L
private set
@Volatile
var gcHits = 0L
private set
init {
check(CLIENTS.get() == null) { "Already has OpenGL context existing at ${Thread.currentThread()}!" }
CLIENTS.set(this)
lock.lock()
try {
if (!glfwInitialized) {
check(GLFW.glfwInit()) { "Unable to initialize GLFW" }
glfwInitialized = true
GLFWErrorCallback.create { error, description ->
LOGGER.error("LWJGL error {}: {}", error, description)
}.set()
}
} finally {
lock.unlock()
}
GLFW.glfwDefaultWindowHints()
GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE)
GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_TRUE)
GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 4)
GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 6)
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE)
window = GLFW.glfwCreateWindow(800, 600, "KStarbound", MemoryUtil.NULL, MemoryUtil.NULL)
require(window != MemoryUtil.NULL) { "Unable to create GLFW window" }
startupTextList.add("Created GLFW window")
input.installCallback(window)
GLFW.glfwMakeContextCurrent(window)
// This line is critical for LWJGL's interoperation with GLFW's
// OpenGL context, or any context that is managed externally.
// LWJGL detects the context that is current in the current thread,
// creates the GLCapabilities instance and makes the OpenGL
// bindings available for use.
capabilities = GL.createCapabilities()
GLFW.glfwSetFramebufferSizeCallback(window) { _, w, h ->
if (w == 0 || h == 0) {
isRenderingGame = false
} else {
isRenderingGame = true
setViewport(0, 0, w, h)
viewportMatrixScreen = updateViewportMatrixScreen()
viewportMatrixWorld = updateViewportMatrixWorld()
for (callback in onViewportChanged) {
callback.invoke(w, h)
}
}
}
val stack = MemoryStack.stackPush()
try {
val pWidth = stack.mallocInt(1)
val pHeight = stack.mallocInt(1)
GLFW.glfwGetWindowSize(window, pWidth, pHeight)
val vidmode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor())!!
GLFW.glfwSetWindowPos(
window,
(vidmode.width() - pWidth[0]) / 2,
(vidmode.height() - pHeight[0]) / 2
)
setViewport(0, 0, pWidth[0], pHeight[0])
viewportMatrixScreen = updateViewportMatrixScreen()
viewportMatrixWorld = updateViewportMatrixWorld()
} finally {
stack.close()
}
// vsync
GLFW.glfwSwapInterval(0)
GLFW.glfwShowWindow(window)
putDebugLog("Initialized GLFW window")
}
val programs = GLPrograms()
val flat2DLines by lazy { StreamVertexBuilder(GLAttributeList.VEC2F, GeometryType.LINES) }
val flat2DTriangles by lazy { StreamVertexBuilder(GLAttributeList.VEC2F, GeometryType.TRIANGLES) }
val flat2DTexturedQuads by lazy { StreamVertexBuilder(GLAttributeList.VERTEX_TEXTURE, GeometryType.QUADS) }
val quadWireframe by lazy { StreamVertexBuilder(GLAttributeList.VEC2F, GeometryType.QUADS_AS_LINES_WIREFRAME) }
// минимальное время хранения 5 минут и...
private val named2DTextures0: Cache<String, GLTexture2D> = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(5))
.build()
// ...бесконечное хранение пока кто-то все ещё использует текстуру
private val named2DTextures1: Cache<String, GLTexture2D> = Caffeine.newBuilder()
.weakValues()
.build()
private val missingTexture: GLTexture2D by lazy {
newTexture(missingTexturePath).upload(Starbound.readDirect(missingTexturePath), GL_RGBA, GL_RGBA).generateMips().also {
it.textureMinFilter = GL_NEAREST
it.textureMagFilter = GL_NEAREST
}
}
private val missingTexturePath = "/assetmissing.png"
val matrixStack = Matrix4fStack()
val freeType = FreeType()
val font = Font()
val box2dRenderer = Box2DRenderer()
fun registerCleanable(ref: Any, fn: (Int) -> Unit, nativeRef: Int): Cleaner.Cleanable {
val cleanable = cleaner.register(ref) {
objectsCleaned++
if (isSameThread()) {
fn(nativeRef)
checkForGLError()
} else {
gcHits++
synchronized(cleanerBacklog) {
cleanerBacklog.add {
fn(nativeRef)
checkForGLError()
}
}
}
}
return cleanable
}
fun cleanup() {
synchronized(cleanerBacklog) {
for (lambda in cleanerBacklog) {
lambda.invoke()
}
cleanerBacklog.clear()
}
}
var blend by GLStateSwitchTracker(GL_BLEND)
var scissor by GLStateSwitchTracker(GL_SCISSOR_TEST)
var cull by GLStateSwitchTracker(GL_CULL_FACE)
var cullMode by GLStateFuncTracker(::glCullFace, GL_BACK)
var textureUnpackAlignment by GLStateIntTracker(::glPixelStorei, GL_UNPACK_ALIGNMENT, 4)
var scissorRect by GLStateGenericTracker(ScissorRect(0, 0, 0, 0)) {
// require(it.x >= 0) { "Invalid X ${it.x}"}
// require(it.y >= 0) { "Invalid Y ${it.y}"}
require(it.width >= 0) { "Invalid width ${it.width}"}
require(it.height >= 0) { "Invalid height ${it.height}"}
glScissor(it.x, it.y, it.width, it.height)
}
var depthTest by GLStateSwitchTracker(GL_DEPTH_TEST)
var VBO: VertexBufferObject? = null
set(value) {
ensureSameThread()
if (field !== value) {
isMe(value?.client)
require(value?.isArray != false) { "Provided buffer object is not of Array type" }
glBindBuffer(GL_ARRAY_BUFFER, value?.pointer ?: 0)
checkForGLError("Setting Vertex Buffer Object")
field = value
}
}
var EBO: VertexBufferObject? = null
set(value) {
ensureSameThread()
if (field !== value) {
isMe(value?.client)
require(value?.isElementArray != false) { "Provided buffer object is not of Array type" }
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, value?.pointer ?: 0)
checkForGLError("Setting Element Buffer Object")
field = value
}
}
var VAO: VertexArrayObject? = null
set(value) {
ensureSameThread()
if (field !== value) {
isMe(value?.client)
glBindVertexArray(value?.pointer ?: 0)
checkForGLError("Setting Vertex Array Object")
field = value
}
}
var readFramebuffer: GLFrameBuffer? = null
set(value) {
ensureSameThread()
if (field === value) return
isMe(value?.client)
field = value
if (value == null) {
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0)
checkForGLError()
return
}
glBindFramebuffer(GL_READ_FRAMEBUFFER, value.pointer)
checkForGLError()
}
var writeFramebuffer: GLFrameBuffer? = null
set(value) {
ensureSameThread()
if (field === value) return
isMe(value?.client)
field = value
if (value == null) {
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0)
checkForGLError()
return
}
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, value.pointer)
checkForGLError()
}
var framebuffer: GLFrameBuffer?
get() {
val readFramebuffer = readFramebuffer
val writeFramebuffer = writeFramebuffer
if (readFramebuffer == writeFramebuffer) {
return writeFramebuffer
}
return null
}
set(value) {
readFramebuffer = value
writeFramebuffer = value
}
var program: GLShaderProgram? = null
set(value) {
ensureSameThread()
if (value !== field) {
isMe(value?.client)
glUseProgram(value?.pointer ?: 0)
checkForGLError("Setting shader program")
field = value
}
}
var activeTexture = 0
set(value) {
ensureSameThread()
if (field != value) {
require(value >= 0) { "Invalid texture block $value" }
require(value < 80) { "Too big texture block index $value, OpenGL 4.6 guarantee only 80!" }
glActiveTexture(GL_TEXTURE0 + value)
checkForGLError()
field = value
}
}
var texture2D: GLTexture2D? by TexturesTracker(80)
var clearColor by GLStateGenericTracker<IStruct4f>(RGBAColor.WHITE) {
val (r, g, b, a) = it
glClearColor(r, g, b, a)
}
var blendFunc by GLStateGenericTracker(BlendFunc()) {
glBlendFuncSeparate(it.sourceColor.enum, it.destinationColor.enum, it.sourceAlpha.enum, it.destinationAlpha.enum)
}
init {
glActiveTexture(GL_TEXTURE0)
checkForGLError()
}
fun setViewport(x: Int, y: Int, width: Int, height: Int) {
ensureSameThread()
if (viewportX != x || viewportY != y || viewportWidth != width || viewportHeight != height) {
glViewport(x, y, width, height)
checkForGLError("Setting viewport")
viewportX = x
viewportY = y
viewportWidth = width
viewportHeight = height
}
}
fun pushScissorRect(x: Float, y: Float, width: Float, height: Float) {
return pushScissorRect(x.roundToInt(), y.roundToInt(), width.roundToInt(), height.roundToInt())
}
@Suppress("NAME_SHADOWING")
fun pushScissorRect(x: Int, y: Int, width: Int, height: Int) {
var x = x
var y = y
var width = width
var height = height
val peek = scissorStack.lastOrNull()
if (peek != null) {
x = x.coerceAtLeast(peek.x)
y = y.coerceAtLeast(peek.y)
width = width.coerceAtMost(peek.width)
height = height.coerceAtMost(peek.height)
if (peek.x == x && peek.y == y && peek.width == width && peek.height == height) {
scissorStack.add(peek)
return
}
}
val rect = ScissorRect(x, y, width, height)
scissorStack.add(rect)
scissorRect = rect
scissor = true
}
fun popScissorRect() {
scissorStack.removeLast()
val peek = scissorStack.lastOrNull()
if (peek == null) {
scissor = false
return
}
val y = viewportHeight - peek.y - peek.height
scissorRect = ScissorRect(peek.x, y, peek.width, peek.height)
}
val currentScissorRect get() = scissorStack.lastOrNull()
fun ensureSameThread() {
if (thread !== Thread.currentThread()) {
throw IllegalAccessException("Trying to access $this outside of $thread!")
}
}
fun isSameThread() = thread === Thread.currentThread()
fun newTexture(name: String = "<unknown>") = GLTexture2D(name)
fun loadTexture(path: String): GLTexture2D {
ensureSameThread()
return named2DTextures0.get(path) {
named2DTextures1.get(it) {
val data = Image.get(it)
if (data == null) {
LOGGER.error("Texture {} is missing! Falling back to {}", it, missingTexturePath)
missingTexture
} else {
newTexture(it).upload(data).also {
it.textureMinFilter = GL_NEAREST
it.textureMagFilter = GL_NEAREST
}
}
}
}
}
fun bind(obj: VertexBufferObject): VertexBufferObject {
if (obj.type == VBOType.ARRAY)
VBO = obj
else
EBO = obj
return obj
}
fun unbind(obj: VertexBufferObject): VertexBufferObject {
if (obj.type == VBOType.ARRAY)
if (obj == VBO)
VBO = null
else
if (obj == EBO)
EBO = null
return obj
}
fun bind(obj: VertexArrayObject): VertexArrayObject {
VAO = obj
return obj
}
fun unbind(obj: VertexArrayObject): VertexArrayObject {
if (obj == VAO)
VAO = null
return obj
}
fun newVBO() = VertexBufferObject.vbo()
fun newEBO() = VertexBufferObject.ebo()
fun newVAO() = VertexArrayObject()
inline fun quadWireframe(color: RGBAColor = RGBAColor.WHITE, lambda: (VertexBuilder) -> Unit) {
val builder = quadWireframe
builder.builder.begin()
lambda.invoke(builder.builder)
builder.upload()
programs.flat.use()
programs.flat.color = color
programs.flat.transform = matrixStack.last()
builder.draw(GL_LINES)
}
inline fun quadColor(lambda: (VertexBuilder) -> Unit) {
val builder = programs.flatColor.builder
builder.builder.begin()
lambda.invoke(builder.builder)
builder.upload()
programs.flatColor.use()
programs.flatColor.transform = matrixStack.last()
builder.draw(GL_TRIANGLES)
}
inline fun quadTexture(texture: GLTexture2D, lambda: (VertexBuilder) -> Unit) {
val builder = programs.textured2d.builder
builder.builder.begin()
lambda.invoke(builder.builder)
builder.upload()
activeTexture = 0
texture.bind()
programs.textured2d.use()
programs.textured2d.transform = matrixStack.last()
programs.textured2d.texture = 0
builder.draw(GL_TRIANGLES)
}
inline fun quadWireframe(value: AABB, color: RGBAColor = RGBAColor.WHITE, chain: (VertexBuilder) -> Unit = {}) {
quadWireframe(color) {
it.quad(value.mins.x.toFloat(), value.mins.y.toFloat(), value.maxs.x.toFloat(), value.maxs.y.toFloat())
chain(it)
}
}
fun vertex(file: File) = GLShader(file, GL_VERTEX_SHADER)
fun fragment(file: File) = GLShader(file, GL_FRAGMENT_SHADER)
fun vertex(contents: String) = GLShader(contents, GL_VERTEX_SHADER)
fun fragment(contents: String) = GLShader(contents, GL_FRAGMENT_SHADER)
fun internalVertex(file: String) = GLShader(readInternal(file), GL_VERTEX_SHADER)
fun internalFragment(file: String) = GLShader(readInternal(file), GL_FRAGMENT_SHADER)
fun internalGeometry(file: String) = GLShader(readInternal(file), GL_GEOMETRY_SHADER)
fun putDebugLog(text: String, replace: Boolean = false) {
if (replace) {
if (startupTextList.isEmpty()) {
startupTextList.add(text)
} else {
startupTextList[startupTextList.size - 1] = text
}
} else {
startupTextList.add(text)
}
finishStartupRendering = System.currentTimeMillis() + 4000L
}
private fun isMe(state: StarboundClient?) {
if (state != null && state != this) {
throw InvalidArgumentException("Provided object does not belong to $this state tracker (belongs to $state)")
}
}
private fun updateViewportMatrixScreen(): Matrix4f {
return Matrix4f.ortho(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat(), 0.1f, 100f).translate(Vector3f(z = 2f))
}
private fun updateViewportMatrixWorld(): Matrix4f {
return Matrix4f.orthoDirect(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat(), 1f, 100f)
}
private val xMousePos = ByteBuffer.allocateDirect(8).also { it.order(ByteOrder.LITTLE_ENDIAN) }.asDoubleBuffer()
private val yMousePos = ByteBuffer.allocateDirect(8).also { it.order(ByteOrder.LITTLE_ENDIAN) }.asDoubleBuffer()
val mouseCoordinates: Vector2d get() {
xMousePos.position(0)
yMousePos.position(0)
GLFW.glfwGetCursorPos(window, xMousePos, yMousePos)
xMousePos.position(0)
yMousePos.position(0)
return Vector2d(xMousePos.get(), yMousePos.get())
}
val mouseCoordinatesF: Vector2f get() {
xMousePos.position(0)
yMousePos.position(0)
GLFW.glfwGetCursorPos(window, xMousePos, yMousePos)
xMousePos.position(0)
yMousePos.position(0)
return Vector2f(xMousePos.get().toFloat(), yMousePos.get().toFloat())
}
/**
* Преобразует экранные координаты в мировые
*/
fun screenToWorld(x: Double, y: Double): Vector2d {
val relativeX = camera.pos.x - (viewportWidth / 2.0 - x) / PIXELS_IN_STARBOUND_UNIT / settings.zoom
val relativeY = (viewportHeight / 2.0 - y) / PIXELS_IN_STARBOUND_UNIT / settings.zoom + camera.pos.y
return Vector2d(relativeX, relativeY)
}
/**
* Преобразует экранные координаты в мировые
*/
fun screenToWorld(value: Vector2d): Vector2d {
return screenToWorld(value.x, value.y)
}
/**
* Преобразует экранные координаты в мировые
*/
fun screenToWorld(x: Float, y: Float): Vector2f {
val relativeX = camera.pos.x - (viewportWidth / 2f - x) / PIXELS_IN_STARBOUND_UNITf / settings.zoom
val relativeY = (viewportHeight / 2f - y) / PIXELS_IN_STARBOUND_UNITf / settings.zoom + camera.pos.y
return Vector2f(relativeX, relativeY)
}
/**
* Преобразует экранные координаты в мировые
*/
fun screenToWorld(value: Vector2f): Vector2f {
return screenToWorld(value.x, value.y)
}
/**
* Преобразует мировые координаты в экранные
*/
fun worldToScreen(x: Float, y: Float): Vector2f {
val relativeX = (x - camera.pos.x) * PIXELS_IN_STARBOUND_UNITf * settings.zoom + viewportWidth / 2f
val relativeY = (camera.pos.y - y) * PIXELS_IN_STARBOUND_UNITf * settings.zoom + viewportHeight / 2f
return Vector2f(relativeX, relativeY)
}
val tileRenderers = TileRenderers(this)
var world: ClientWorld? = ClientWorld(this, 0L, Vector2i(3000, 2000), true)
init {
putDebugLog("Initialized OpenGL context")
clearColor = RGBAColor.SLATE_GRAY
blend = true
blendFunc = BlendFunc.MULTIPLY_WITH_ALPHA
}
var frameRenderTime = 0.0
private set
private var lastRender = JVMTimeSource.INSTANCE.seconds
val framesPerSecond get() = if (frameRenderTime == 0.0) 1.0 else 1.0 / frameRenderTime
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
}
if (sum == 0.0)
return 0.0
return frameRenderTimes.size / sum
}
val settings = ClientSettings()
val viewportCells: ICellAccess = object : ICellAccess {
override fun getCell(x: Int, y: Int): IChunkCell? {
return world?.getCell(x + viewportCellX, y + viewportCellY)
}
override fun getCellDirect(x: Int, y: Int): IChunkCell? {
return world?.getCellDirect(x + viewportCellX, y + viewportCellY)
}
}
var viewportLighting = LightCalculator(viewportCells, viewportCellWidth, viewportCellHeight)
private set
val viewportLightingTexture = newTexture("Viewport Lighting")
private var viewportLightingMem: ByteBuffer? = null
fun updateViewportParams() {
viewportRectangle = AABB.rectangle(
camera.pos.toDoubleVector(),
viewportWidth / settings.zoom / PIXELS_IN_STARBOUND_UNIT,
viewportHeight / settings.zoom / PIXELS_IN_STARBOUND_UNIT)
viewportCellX = roundTowardsNegativeInfinity(viewportRectangle.mins.x) - 4
viewportCellY = roundTowardsNegativeInfinity(viewportRectangle.mins.y) - 4
viewportCellWidth = roundTowardsPositiveInfinity(viewportRectangle.width) + 8
viewportCellHeight = roundTowardsPositiveInfinity(viewportRectangle.height) + 8
if (viewportLighting.width != viewportCellWidth || viewportLighting.height != viewportCellHeight) {
viewportLighting = LightCalculator(viewportCells, viewportCellWidth, viewportCellHeight)
viewportLighting.multithreaded = true
if (viewportCellWidth > 0 && viewportCellHeight > 0) {
viewportLightingMem = ByteBuffer.allocateDirect(viewportCellWidth.coerceAtMost(4096) * viewportCellHeight.coerceAtMost(4096) * 3)
} else {
viewportLightingMem = null
}
}
}
fun onViewportChanged(callback: (width: Int, height: Int) -> Unit) {
onViewportChanged.add(callback)
}
fun onDrawGUI(lambda: () -> Unit) {
onDrawGUI.add(lambda)
}
fun onPreDrawWorld(lambda: (LayeredRenderer) -> Unit) {
onPreDrawWorld.add(lambda)
}
fun onPostDrawWorld(lambda: () -> Unit) {
onPostDrawWorld.add(lambda)
}
fun onPostDrawWorldOnce(lambda: (LayeredRenderer) -> Unit) {
onPostDrawWorldOnce.add(lambda)
}
fun renderFrame(): Boolean {
ensureSameThread()
val diff = JVMTimeSource.INSTANCE.seconds - lastRender
if (diff < Starbound.TICK_TIME_ADVANCE)
LockSupport.parkNanos(((Starbound.TICK_TIME_ADVANCE - diff) * 1_000_000_000.0).toLong())
frameRenderTime = JVMTimeSource.INSTANCE.seconds - lastRender
frameRenderTimes[++frameRenderIndex % frameRenderTimes.size] = frameRenderTime
lastRender = JVMTimeSource.INSTANCE.seconds
if (GLFW.glfwWindowShouldClose(window)) {
close()
return false
}
val world = world
if (!isRenderingGame) {
cleanup()
GLFW.glfwPollEvents()
if (world != null) {
if (Starbound.initialized)
world.think()
}
return true
}
if (world != null) {
updateViewportParams()
val layers = LayeredRenderer()
if (Starbound.initialized)
world.think()
clearColor = RGBAColor.SLATE_GRAY
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
matrixStack.clear(viewportMatrixWorld)
matrixStack.push().last()
.translateWithMultiplication(viewportWidth / 2f, viewportHeight / 2f, 2f) // центр экрана + координаты отрисовки мира
.scale(x = settings.zoom * PIXELS_IN_STARBOUND_UNITf, y = settings.zoom * PIXELS_IN_STARBOUND_UNITf) // масштабируем до нужного размера
.translateWithMultiplication(-camera.pos.x, -camera.pos.y) // перемещаем вид к камере
for (lambda in onPreDrawWorld) {
lambda.invoke(layers)
}
for (i in onPostDrawWorldOnce.size - 1 downTo 0) {
onPostDrawWorldOnce[i].invoke(layers)
onPostDrawWorldOnce.removeAt(i)
}
viewportLighting.clear()
world.addLayers(
layers = layers,
size = viewportRectangle)
layers.render(matrixStack)
val viewportLightingMem = viewportLightingMem
if (viewportLightingMem != null && !fullbright) {
viewportLightingMem.position(0)
BufferUtils.zeroBuffer(viewportLightingMem)
viewportLightingMem.position(0)
viewportLighting.calculate(viewportLightingMem, viewportLighting.width.coerceAtMost(4096), viewportLighting.height.coerceAtMost(4096))
viewportLightingMem.position(0)
val old = textureUnpackAlignment
textureUnpackAlignment = if (viewportLighting.width.coerceAtMost(4096) % 4 == 0) 4 else 1
viewportLightingTexture.upload(
GL_RGB,
viewportLighting.width.coerceAtMost(4096),
viewportLighting.height.coerceAtMost(4096),
GL_RGB,
GL_UNSIGNED_BYTE,
viewportLightingMem
)
textureUnpackAlignment = old
viewportLightingTexture.textureMinFilter = GL_LINEAR
//viewportLightingTexture.textureMagFilter = GL_NEAREST
//viewportLightingTexture.generateMips()
blendFunc = BlendFunc.MULTIPLY_BY_SRC
quadTexture(viewportLightingTexture) {
it.quad(
(viewportCellX).toFloat(),
(viewportCellY).toFloat(),
(viewportCellX + viewportCellWidth).toFloat(),
(viewportCellY + viewportCellHeight).toFloat(),
QuadTransformers.uv()
)
}
blendFunc = BlendFunc.MULTIPLY_WITH_ALPHA
}
world.physics.debugDraw()
for (lambda in onPostDrawWorld) {
lambda.invoke()
}
matrixStack.pop()
}
matrixStack.clear(viewportMatrixScreen)
val thisTime = System.currentTimeMillis()
if (startupTextList.isNotEmpty() && thisTime <= finishStartupRendering) {
var alpha = 1f
if (finishStartupRendering - thisTime < 1000L) {
alpha = (finishStartupRendering - thisTime) / 1000f
}
matrixStack.push()
matrixStack.last().translateWithMultiplication(y = viewportHeight.toFloat())
var shade = 255
for (i in startupTextList.size - 1 downTo 0) {
val size = font.render(startupTextList[i], alignY = TextAlignY.BOTTOM, scale = 0.4f, color = RGBAColor(shade / 255f, shade / 255f, shade / 255f, alpha))
matrixStack.last().translateWithMultiplication(y = -size.height * 1.2f)
if (shade > 120) {
shade -= 10
}
}
matrixStack.pop()
}
for (fn in onDrawGUI) {
fn.invoke()
}
val runtime = Runtime.getRuntime()
font.render("FPS: ${(averageFramesPerSecond * 100f).toInt() / 100f}", scale = 0.4f)
font.render("JVM Heap: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", y = font.lineHeight * 0.5f, scale = 0.4f)
GLFW.glfwSwapBuffers(window)
GLFW.glfwPollEvents()
input.think()
camera.think(Starbound.TICK_TIME_ADVANCE)
cleanup()
return true
}
fun onTermination(lambda: () -> Unit) {
terminateCallbacks.add(lambda)
}
override fun close() {
if (gameTerminated)
return
if (window != MemoryUtil.NULL) {
Callbacks.glfwFreeCallbacks(window)
GLFW.glfwDestroyWindow(window)
}
GLFW.glfwTerminate()
GLFW.glfwSetErrorCallback(null)?.free()
gameTerminated = true
for (callback in terminateCallbacks) {
callback.invoke()
}
}
companion object {
private val LOGGER = LogManager.getLogger(StarboundClient::class.java)
private val CLIENTS = ThreadLocal<StarboundClient>()
@JvmStatic
fun current() = checkNotNull(CLIENTS.get()) { "No client registered to current thread (${Thread.currentThread()})" }
@JvmStatic
fun currentOrNull(): StarboundClient? = CLIENTS.get()
private val lock = ReentrantLock()
@Volatile
private var glfwInitialized = false
@JvmStatic
fun readInternal(file: String): String {
return ClassLoader.getSystemClassLoader().getResourceAsStream(file)!!.bufferedReader()
.let {
val read = it.readText()
it.close()
read
}
}
}
}