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

1003 lines
30 KiB
Kotlin

package ru.dbotthepony.kstarbound.client
import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.Scheduler
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.GL45.*
import org.lwjgl.opengl.GLCapabilities
import org.lwjgl.system.MemoryStack
import org.lwjgl.system.MemoryUtil
import ru.dbotthepony.kstarbound.LoadingLog
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT
import ru.dbotthepony.kstarbound.world.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.VertexArrayObject
import ru.dbotthepony.kstarbound.client.gl.BufferObject
import ru.dbotthepony.kstarbound.client.gl.checkForGLError
import ru.dbotthepony.kstarbound.client.gl.properties.GLStateFuncTracker
import ru.dbotthepony.kstarbound.client.gl.properties.GLGenericTracker
import ru.dbotthepony.kstarbound.client.gl.properties.GLObjectTracker
import ru.dbotthepony.kstarbound.client.gl.properties.GLStateIntTracker
import ru.dbotthepony.kstarbound.client.gl.properties.GLStateSwitchTracker
import ru.dbotthepony.kstarbound.client.gl.properties.GLTexturesTracker
import ru.dbotthepony.kstarbound.client.gl.shader.FontProgram
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.shader.UberShader
import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder
import ru.dbotthepony.kstarbound.client.input.UserInput
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.forEachValid
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.AbstractCell
import ru.dbotthepony.kvector.api.IStruct4f
import ru.dbotthepony.kvector.arrays.Matrix3f
import ru.dbotthepony.kvector.arrays.Matrix3fStack
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.Vector4f
import java.io.Closeable
import java.io.File
import java.lang.ref.PhantomReference
import java.lang.ref.ReferenceQueue
import java.lang.ref.WeakReference
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.time.Duration
import java.util.*
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinWorkerThread
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport
import java.util.concurrent.locks.ReentrantLock
import java.util.function.IntConsumer
import kotlin.collections.ArrayList
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
class StarboundClient : Closeable {
val window: Long
val camera = Camera(this)
val input = UserInput()
val thread: Thread = Thread.currentThread()
private val threadCounter = AtomicInteger()
// client specific executor which will accept tasks which involve probable
// callback to foreground executor to initialize thread-unsafe data
// In above case too many threads will introduce big congestion for resources, stalling entire workload; wasting cpu resources
val executor = ForkJoinPool(Runtime.getRuntime().availableProcessors().coerceAtMost(4), {
object : ForkJoinWorkerThread(it) {
init {
name = "Background Executor for '${thread.name}'-${threadCounter.incrementAndGet()}"
}
override fun onTermination(exception: Throwable?) {
super.onTermination(exception)
if (exception != null) {
LOGGER.error("$this encountered an exception while executing task", exception)
}
}
}
}, null, false)
val mailbox = MailboxExecutorService(thread)
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
// potentially visible cells
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 viewportBottomLeft = Vector2d()
private set
var viewportTopRight = Vector2d()
private set
var fullbright = true
var preciseWait = false
var clientTerminated = false
private set
var viewportMatrixScreen: Matrix3f
private set
get() = Matrix3f.unmodifiable(field)
var viewportMatrixWorld: Matrix3f
private set
get() = Matrix3f.unmodifiable(field)
var isRenderingGame = true
private set
private val scissorStack = LinkedList<ScissorRect>()
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>()
val loadingLog = LoadingLog()
private val openglCleanQueue = ReferenceQueue<Any>()
private var openglCleanQueueHead: CleanRef? = null
private class CleanRef(ref: Any, queue: ReferenceQueue<Any>, val fn: IntConsumer, val value: Int) : PhantomReference<Any>(ref, queue) {
var next: CleanRef? = null
var prev: CleanRef? = null
}
var openglObjectsCreated = 0L
private set
var openglObjectsCleaned = 0L
private set
init {
check(CLIENTS.get() == null) { "Already has OpenGL context existing at ${Thread.currentThread()}!" }
CLIENTS.set(this)
lock.lock()
try {
clients++
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, 5)
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE)
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE)
window = GLFW.glfwCreateWindow(800, 600, "KStarbound", MemoryUtil.NULL, MemoryUtil.NULL)
require(window != MemoryUtil.NULL) { "Unable to create GLFW window" }
loadingLog.line("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)
loadingLog.line("Initialized GLFW window")
}
val maxTextureBlocks = glGetInteger(GL_MAX_TEXTURE_IMAGE_UNITS)
val maxUserTextureBlocks = maxTextureBlocks - 1 // available textures blocks for generic use
val maxVertexAttribBindPoints = glGetInteger(GL_MAX_VERTEX_ATTRIB_BINDINGS)
init {
LOGGER.info("OpenGL Version: ${glGetString(GL_VERSION)}")
LOGGER.info("OpenGL Vendor: ${glGetString(GL_VENDOR)}")
LOGGER.info("OpenGL Renderer: ${glGetString(GL_RENDERER)}")
LOGGER.debug("Max supported texture image units: $maxTextureBlocks")
LOGGER.debug("Max supported vertex attribute bind points: $maxVertexAttribBindPoints")
}
val stack = Matrix3fStack()
// минимальное время хранения 5 минут и...
val named2DTextures0: Cache<Image, GLTexture2D> = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(1))
.scheduler(Scheduler.systemScheduler())
.build()
// ...бесконечное хранение пока кто-то все ещё использует текстуру
val named2DTextures1: Cache<Image, GLTexture2D> = Caffeine.newBuilder()
.weakValues()
.weakKeys()
.build()
private val fontShaderPrograms = ArrayList<WeakReference<FontProgram>>()
private val uberShaderPrograms = ArrayList<WeakReference<UberShader>>()
val lightMapLocation = maxTextureBlocks - 1
fun addShaderProgram(program: GLShaderProgram) {
if (program is UberShader) {
uberShaderPrograms.add(WeakReference(program))
}
if (program is FontProgram) {
fontShaderPrograms.add(WeakReference(program))
}
}
fun registerCleanable(ref: Any, fn: IntConsumer, nativeRef: Int) {
openglObjectsCreated++
val ref0 = CleanRef(ref, openglCleanQueue, fn, nativeRef)
if (openglCleanQueueHead == null) {
openglCleanQueueHead = ref0
} else {
ref0.next = openglCleanQueueHead
openglCleanQueueHead!!.prev = ref0
openglCleanQueueHead = ref0
}
}
private fun executeQueuedTasks() {
mailbox.executeQueuedTasks()
var next = openglCleanQueue.poll() as CleanRef?
while (next != null) {
openglObjectsCleaned++
next.fn.accept(next.value)
checkForGLError("Removing unreachable OpenGL object")
val head = openglCleanQueueHead
if (next === head) {
openglCleanQueueHead = head.next
} else {
next.prev?.next = next.next
next.next?.prev = next.prev
}
next = openglCleanQueue.poll() as CleanRef?
}
}
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 GLGenericTracker(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 vao by GLObjectTracker<VertexArrayObject>(::glBindVertexArray)
var framebuffer by GLObjectTracker<GLFrameBuffer>(::glBindFramebuffer, GL_FRAMEBUFFER)
var program by GLObjectTracker<GLShaderProgram>(::glUseProgram)
val textures2D = GLTexturesTracker<GLTexture2D>(maxTextureBlocks)
var clearColor by GLGenericTracker<IStruct4f>(RGBAColor.WHITE) {
val (r, g, b, a) = it
glClearColor(r, g, b, a)
}
var blendFunc by GLGenericTracker(BlendFunc()) {
glBlendFuncSeparate(it.sourceColor.enum, it.destinationColor.enum, it.sourceAlpha.enum, it.destinationAlpha.enum)
}
val freeType = FreeType()
val font = Font()
val programs = GLPrograms()
init {
glActiveTexture(GL_TEXTURE0)
checkForGLError()
}
val whiteTexture = GLTexture2D(1, 1, GL_RGB8)
val missingTexture = GLTexture2D(8, 8, GL_RGB8)
init {
val buffer = ByteBuffer.allocateDirect(3)
buffer.put(0xFF.toByte())
buffer.put(0xFF.toByte())
buffer.put(0xFF.toByte())
buffer.position(0)
whiteTexture.upload(GL_RGB, GL_UNSIGNED_BYTE, buffer)
}
init {
val buffer = ByteBuffer.allocateDirect(3 * 8 * 8)
for (row in 0 until 4) {
for (x in 0 until 4) {
buffer.put(0x80.toByte())
buffer.put(0x0.toByte())
buffer.put(0x80.toByte())
}
for (x in 0 until 4) {
buffer.put(0x0.toByte())
buffer.put(0x0.toByte())
buffer.put(0x0.toByte())
}
}
for (row in 0 until 4) {
for (x in 0 until 4) {
buffer.put(0x0.toByte())
buffer.put(0x0.toByte())
buffer.put(0x0.toByte())
}
for (x in 0 until 4) {
buffer.put(0x80.toByte())
buffer.put(0x0.toByte())
buffer.put(0x80.toByte())
}
}
buffer.position(0)
missingTexture.upload(GL_RGB, GL_UNSIGNED_BYTE, buffer)
}
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 newEBO() = BufferObject.EBO()
fun newVBO() = BufferObject.VBO()
fun newVAO() = VertexArrayObject()
inline fun quadWireframe(color: RGBAColor = RGBAColor.WHITE, lambda: (VertexBuilder) -> Unit) {
val builder = programs.position.builder
builder.builder.begin(GeometryType.QUADS_AS_LINES_WIREFRAME)
lambda.invoke(builder.builder)
builder.upload()
programs.position.use()
programs.position.colorMultiplier = color
programs.position.modelMatrix = stack.last()
builder.draw(GL_LINES)
}
inline fun quadColor(color: RGBAColor = RGBAColor.WHITE, lambda: (VertexBuilder) -> Unit) {
val builder = programs.position.builder
builder.builder.begin(GeometryType.QUADS)
lambda.invoke(builder.builder)
builder.upload()
programs.position.use()
programs.position.colorMultiplier = color
programs.position.modelMatrix = stack.last()
builder.draw(GL_TRIANGLES)
}
inline fun lines(color: RGBAColor = RGBAColor.WHITE, lambda: (VertexBuilder) -> Unit) {
val builder = programs.position.builder
builder.builder.begin(GeometryType.LINES)
lambda.invoke(builder.builder)
builder.upload()
programs.position.use()
programs.position.colorMultiplier = color
programs.position.modelMatrix = stack.last()
builder.draw(GL_LINES)
}
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)
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(): Matrix3f {
return Matrix3f.ortho(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat())
}
private fun updateViewportMatrixWorld(): Matrix3f {
return Matrix3f.orthoDirect(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat())
}
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(), viewportHeight - 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(), viewportHeight - yMousePos.get().toFloat())
}
fun screenToWorld(x: Double, y: Double): Vector2d {
val relativeX = (-viewportWidth / 2.0 + x) / settings.zoom / PIXELS_IN_STARBOUND_UNIT + camera.pos.x
val relativeY = (-viewportHeight / 2.0 + y) / settings.zoom / PIXELS_IN_STARBOUND_UNIT + camera.pos.y
return Vector2d(relativeX, relativeY)
}
fun screenToWorld(x: Int, y: Int): Vector2d {
return screenToWorld(x.toDouble(), y.toDouble())
}
fun screenToWorld(value: Vector2d): Vector2d {
return screenToWorld(value.x, value.y)
}
val tileRenderers = TileRenderers(this)
var world: ClientWorld? = ClientWorld(this, 0L, Vector2i(3000, 2000), true)
init {
clearColor = RGBAColor.SLATE_GRAY
blend = true
blendFunc = BlendFunc.MULTIPLY_WITH_ALPHA
}
// nanoseconds
var frameRenderTime = 0L
private set
private var nextRender = System.nanoTime()
private val frameRenderTimes = LongArray(60) { 1L }
private var frameRenderIndex = 0
private val renderWaitTimes = LongArray(60) { 1L }
private var renderWaitIndex = 0
private var lastRender = System.nanoTime()
val averageRenderWait: Double get() {
var sum = 0.0
for (value in renderWaitTimes)
sum += value
if (sum == 0.0)
return 0.0
sum /= 1_000_000_000.0
return sum / renderWaitTimes.size
}
val averageRenderTime: Double get() {
var sum = 0.0
for (value in frameRenderTimes)
sum += value
if (sum == 0.0)
return 0.0
sum /= 1_000_000_000.0
return sum / frameRenderTimes.size
}
val settings = ClientSettings()
val viewportCells: ICellAccess = object : ICellAccess {
override fun getCell(x: Int, y: Int): AbstractCell {
return world!!.getCell(x + viewportCellX, y + viewportCellY)
}
override fun getCellDirect(x: Int, y: Int): AbstractCell {
return world!!.getCellDirect(x + viewportCellX, y + viewportCellY)
}
override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {
return world!!.setCell(x + viewportCellX, y + viewportCellY, cell)
}
}
var viewportLighting = LightCalculator(viewportCells, viewportCellWidth, viewportCellHeight)
private set
var viewportLightingTexture = GLTexture2D(1, 1, GL_RGB8)
private set
private var viewportLightingMem: ByteBuffer? = null
fun updateViewportParams() {
viewportRectangle = AABB.rectangle(
camera.pos,
viewportWidth / settings.zoom / PIXELS_IN_STARBOUND_UNIT,
viewportHeight / settings.zoom / PIXELS_IN_STARBOUND_UNIT
)
viewportCellX = roundTowardsNegativeInfinity(viewportRectangle.mins.x) - 16
viewportCellY = roundTowardsNegativeInfinity(viewportRectangle.mins.y) - 16
val viewportCellWidth0 = roundTowardsPositiveInfinity(viewportRectangle.width) + 32
val viewportCellHeight0 = roundTowardsPositiveInfinity(viewportRectangle.height) + 32
if ((viewportCellWidth0 - viewportCellWidth).absoluteValue > 1)
viewportCellWidth = viewportCellWidth0
if ((viewportCellHeight0 - viewportCellHeight).absoluteValue > 1)
viewportCellHeight = viewportCellHeight0
viewportTopRight = screenToWorld(viewportWidth, viewportHeight)
viewportBottomLeft = screenToWorld(0, 0)
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)
viewportLightingTexture = GLTexture2D(viewportCellWidth.coerceAtMost(4096), viewportCellHeight.coerceAtMost(4096), GL_RGB8)
} 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)
}
private val layers = LayeredRenderer(this)
fun renderFrame(): Boolean {
ensureSameThread()
var diff = nextRender - System.nanoTime()
// try to sleep until next frame as precise as possible
while (diff > 0L) {
executeQueuedTasks()
diff = nextRender - System.nanoTime()
if (preciseWait) {
if (diff >= 1_500_000L) {
LockSupport.parkNanos(1_000_000L)
} else {
Thread.yield()
}
} else {
LockSupport.parkNanos(diff)
}
}
val mark = System.nanoTime()
try {
if (GLFW.glfwWindowShouldClose(window)) {
close()
return false
}
val world = world
if (!isRenderingGame) {
executeQueuedTasks()
GLFW.glfwPollEvents()
if (world != null && Starbound.initialized)
world.think()
return true
}
layers.clear()
uberShaderPrograms.forEachValid {
if (it.flags.contains(UberShader.Flag.NEEDS_SCREEN_SIZE)) {
it.screenSize = Vector2f(viewportWidth.toFloat(), viewportHeight.toFloat())
}
}
if (world != null) {
updateViewportParams()
if (Starbound.initialized)
world.think()
clearColor = RGBAColor.SLATE_GRAY
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
stack.clear(Matrix3f.identity())
val viewMatrix = viewportMatrixWorld.copy()
.translate(viewportWidth / 2f, viewportHeight / 2f) // центр экрана + координаты отрисовки мира
.scale(x = settings.zoom * PIXELS_IN_STARBOUND_UNITf, y = settings.zoom * PIXELS_IN_STARBOUND_UNITf) // масштабируем до нужного размера
.translate(-camera.pos.x.toFloat(), -camera.pos.y.toFloat()) // перемещаем вид к камере
uberShaderPrograms.forEachValid { it.viewMatrix = viewMatrix }
fontShaderPrograms.forEachValid { it.viewMatrix = viewMatrix }
for (lambda in onPreDrawWorld) {
lambda.invoke(layers)
}
for (i in onPostDrawWorldOnce.size - 1 downTo 0) {
onPostDrawWorldOnce[i].invoke(layers)
onPostDrawWorldOnce.removeAt(i)
}
viewportLighting.clear()
val viewportLightingMem = viewportLightingMem
world.addLayers(
layers = layers,
size = viewportRectangle)
if (viewportLightingMem != null && !fullbright) {
val spos = screenToWorld(mouseCoordinates)
viewportLighting.addPointLight(roundTowardsPositiveInfinity(spos.x - viewportCellX), roundTowardsPositiveInfinity(spos.y - viewportCellY), 1f, 1f, 1f)
viewportLightingMem.position(0)
BufferUtils.zeroBuffer(viewportLightingMem)
viewportLightingMem.position(0)
viewportLighting.calculate(viewportLightingMem, viewportLightingTexture.width, viewportLightingTexture.height)
viewportLightingMem.position(0)
val old = textureUnpackAlignment
textureUnpackAlignment = if (viewportLightingTexture.width.coerceAtMost(4096) % 4 == 0) 4 else 1
viewportLightingTexture.upload(
GL_RGB,
GL_UNSIGNED_BYTE,
viewportLightingMem
)
textureUnpackAlignment = old
} else {
viewportLightingTexture = whiteTexture
}
viewportLightingTexture.textureMinFilter = GL_LINEAR
textures2D[lightMapLocation] = viewportLightingTexture
val lightmapUV = if (fullbright) Vector4f.ZERO else Vector4f(
((viewportBottomLeft.x - viewportCellX) / viewportLighting.width).toFloat(),
((viewportBottomLeft.y - viewportCellY) / viewportLighting.height).toFloat(),
(1f - (viewportCellX + viewportCellWidth - viewportTopRight.x) / viewportLighting.width).toFloat(),
(1f - (viewportCellY + viewportCellHeight - viewportTopRight.y) / viewportLighting.height).toFloat())
uberShaderPrograms.forEachValid {
it.lightmapTexture = lightMapLocation
it.lightmapUV = lightmapUV
}
layers.render()
for (lambda in onPostDrawWorld) {
lambda.invoke()
}
}
uberShaderPrograms.forEachValid { it.viewMatrix = viewportMatrixScreen }
fontShaderPrograms.forEachValid { it.viewMatrix = viewportMatrixScreen }
if (System.nanoTime() - loadingLog.lastActivity <= 4_000_000_000L) {
var alpha = 1f
if (System.nanoTime() - loadingLog.lastActivity >= 3_000_000_000L) {
alpha = 1f - (System.nanoTime() - loadingLog.lastActivity - 3_000_000_000L) / 1_000_000_000f
}
stack.push()
stack.last().translate(y = viewportHeight.toFloat())
var shade = 255
for (line in loadingLog) {
if (line.progress in 0.01f ..< 1f) {
quadColor(RGBAColor(0f, shade / 400f * line.progress, 0f, alpha * 0.8f)) {
it.vertex(0f, font.lineHeight * -0.4f)
it.vertex(viewportWidth * line.progress, font.lineHeight * -0.4f)
it.vertex(viewportWidth * line.progress, 0f)
it.vertex(0f, 0f)
}
}
val size = font.render(line.text, alignY = TextAlignY.BOTTOM, scale = 0.4f, color = RGBAColor(shade / 255f, shade / 255f, shade / 255f, alpha))
stack.last().translate(y = -size.height * 1.2f)
if (shade > 120) {
shade -= 10
}
}
stack.pop()
}
stack.clear(Matrix3f.identity())
for (fn in onDrawGUI) {
fn.invoke()
}
val runtime = Runtime.getRuntime()
font.render("Latency: ${(averageRenderWait * 1_00000.0).toInt() / 100f}ms", scale = 0.4f)
font.render("Frame: ${(averageRenderTime * 1_00000.0).toInt() / 100f}ms", y = font.lineHeight * 0.6f, scale = 0.4f)
font.render("JVM Heap: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", y = font.lineHeight * 1.2f, scale = 0.4f)
font.render("OGL C: $openglObjectsCreated D: $openglObjectsCleaned A: ${openglObjectsCreated - openglObjectsCleaned}", y = font.lineHeight * 1.8f, scale = 0.4f)
GLFW.glfwSwapBuffers(window)
GLFW.glfwPollEvents()
input.think()
camera.think(Starbound.TICK_TIME_ADVANCE)
executeQueuedTasks()
return true
} finally {
frameRenderTime = System.nanoTime() - mark
frameRenderTimes[++frameRenderIndex % frameRenderTimes.size] = frameRenderTime
renderWaitTimes[++renderWaitIndex % renderWaitTimes.size] = System.nanoTime() - lastRender
lastRender = System.nanoTime()
nextRender = mark + Starbound.TICK_TIME_ADVANCE_NANOS
}
}
fun onTermination(lambda: () -> Unit) {
terminateCallbacks.add(lambda)
}
override fun close() {
if (clientTerminated)
return
lock.lock()
try {
if (window != MemoryUtil.NULL) {
Callbacks.glfwFreeCallbacks(window)
GLFW.glfwDestroyWindow(window)
}
if (--clients == 0) {
GLFW.glfwTerminate()
GLFW.glfwSetErrorCallback(null)?.free()
glfwInitialized = false
}
clientTerminated = true
for (callback in terminateCallbacks) {
callback.invoke()
}
} finally {
lock.unlock()
}
}
companion object {
private val LOGGER = LogManager.getLogger(StarboundClient::class.java)
private val CLIENTS = ThreadLocal<StarboundClient>()
private val WHITE = ByteBuffer.allocateDirect(4).also {
it.put(0xFF.toByte())
it.put(0xFF.toByte())
it.put(0xFF.toByte())
it.put(0xFF.toByte())
it.position(0)
}
@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
@Volatile
private var clients = 0
@JvmStatic
fun readInternal(file: String): String {
return ClassLoader.getSystemClassLoader().getResourceAsStream(file)!!.bufferedReader()
.let {
val read = it.readText()
it.close()
read
}
}
}
}