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() 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() 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 = Caffeine.newBuilder() .expireAfterAccess(Duration.ofMinutes(5)) .build() // ...бесконечное хранение пока кто-то все ещё использует текстуру private val named2DTextures1: Cache = 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(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 = "") = 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() @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 } } } }