444 lines
11 KiB
Kotlin
444 lines
11 KiB
Kotlin
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
|
|
import ru.dbotthepony.kstarbound.api.IStruct4f
|
|
import ru.dbotthepony.kstarbound.client.freetype.FreeType
|
|
import ru.dbotthepony.kstarbound.math.Matrix4f
|
|
import ru.dbotthepony.kstarbound.math.Matrix4fStack
|
|
import ru.dbotthepony.kstarbound.client.render.Font
|
|
import ru.dbotthepony.kstarbound.client.render.TileRenderers
|
|
import ru.dbotthepony.kstarbound.math.AABB
|
|
import ru.dbotthepony.kstarbound.util.Color
|
|
import java.io.File
|
|
import java.io.FileNotFoundException
|
|
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) {
|
|
operator fun getValue(glStateTracker: GLStateTracker, property: KProperty<*>): Boolean {
|
|
return value
|
|
}
|
|
|
|
operator fun setValue(glStateTracker: GLStateTracker, property: KProperty<*>, value: Boolean) {
|
|
glStateTracker.ensureSameThread()
|
|
|
|
if (value == this.value)
|
|
return
|
|
|
|
if (value) {
|
|
glEnable(enum)
|
|
} else {
|
|
glDisable(enum)
|
|
}
|
|
|
|
checkForGLError()
|
|
this.value = value
|
|
}
|
|
}
|
|
|
|
private class GLStateGenericTracker<T>(private var value: T, private val lambda: (T) -> Unit) {
|
|
operator fun getValue(glStateTracker: GLStateTracker, property: KProperty<*>): T {
|
|
return value
|
|
}
|
|
|
|
operator fun setValue(glStateTracker: GLStateTracker, property: KProperty<*>, value: T) {
|
|
glStateTracker.ensureSameThread()
|
|
|
|
if (value == this.value)
|
|
return
|
|
|
|
lambda.invoke(value)
|
|
checkForGLError()
|
|
this.value = value
|
|
}
|
|
}
|
|
|
|
open class GLTransformableProgram(state: GLStateTracker, vararg shaders: GLShader) : GLShaderProgram(state, *shaders) {
|
|
init {
|
|
link()
|
|
}
|
|
|
|
val transform = this["_transform"]!!
|
|
|
|
init {
|
|
transform.set(Matrix4f.IDENTITY)
|
|
}
|
|
}
|
|
|
|
open class GLTransformableColorableProgram(state: GLStateTracker, vararg shaders: GLShader) : GLTransformableProgram(state, *shaders) {
|
|
val color = this["_color"]!!
|
|
|
|
init {
|
|
color.set(Color.WHITE)
|
|
}
|
|
}
|
|
|
|
interface GLCleanable : Cleaner.Cleanable {
|
|
/**
|
|
* Выставляет флаг на то, что объект был удалён вручную и вызывает clean()
|
|
*/
|
|
fun cleanManual(): Unit
|
|
}
|
|
|
|
interface GLStreamBuilderList {
|
|
val small: StreamVertexBuilder
|
|
val statefulSmall: StatefulStreamVertexBuilder
|
|
}
|
|
|
|
class GLStateTracker {
|
|
init {
|
|
// 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.
|
|
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) {
|
|
if (!cleanManual)
|
|
LOGGER.error("{} with ID {} got leaked.", name, nativeRef)
|
|
|
|
cleanerHits.add {
|
|
fn(nativeRef)
|
|
checkForGLError()
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
var VBO: GLVertexBufferObject? = null
|
|
set(value) {
|
|
ensureSameThread()
|
|
if (field === value) return
|
|
field = value
|
|
|
|
if (value == null) {
|
|
glBindBuffer(GL_ARRAY_BUFFER, 0)
|
|
checkForGLError()
|
|
return
|
|
}
|
|
|
|
if (!value.isArray) throw IllegalArgumentException("Provided buffer object is not of Array type")
|
|
glBindBuffer(GL_ARRAY_BUFFER, value.pointer)
|
|
checkForGLError()
|
|
}
|
|
|
|
var EBO: GLVertexBufferObject? = null
|
|
set(value) {
|
|
ensureSameThread()
|
|
if (field === value) return
|
|
field = value
|
|
|
|
if (value == null) {
|
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
|
|
checkForGLError()
|
|
return
|
|
}
|
|
|
|
if (!value.isElementArray) throw IllegalArgumentException("Provided buffer object is not of Array type")
|
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, value.pointer)
|
|
checkForGLError()
|
|
}
|
|
|
|
var VAO: GLVertexArrayObject? = null
|
|
set(value) {
|
|
ensureSameThread()
|
|
if (field === value) return
|
|
field = value
|
|
|
|
if (value == null) {
|
|
glBindVertexArray(0)
|
|
checkForGLError()
|
|
return
|
|
}
|
|
|
|
glBindVertexArray(value.pointer)
|
|
checkForGLError()
|
|
}
|
|
|
|
var program: GLShaderProgram? = null
|
|
private set
|
|
|
|
var texture2D: GLTexture2D? = null
|
|
set(value) {
|
|
ensureSameThread()
|
|
if (field === value) return
|
|
field = value
|
|
if (value == null) return
|
|
glBindTexture(GL_TEXTURE_2D, value.pointer)
|
|
checkForGLError()
|
|
}
|
|
|
|
var activeTexture = 0
|
|
set(value) {
|
|
ensureSameThread()
|
|
if (field == value) return
|
|
require(value >= 0) { "Invalid texture block $value" }
|
|
require(value < 80) { "Too big texture block index $value, OpenGL 4.6 guarantee only 80!" }
|
|
field = value
|
|
glActiveTexture(GL_TEXTURE0 + value)
|
|
checkForGLError()
|
|
}
|
|
|
|
var clearColor by GLStateGenericTracker<IStruct4f>(Color.WHITE) {
|
|
val (r, g, b, a) = it
|
|
glClearColor(r, g, b, a)
|
|
}
|
|
|
|
init {
|
|
glActiveTexture(GL_TEXTURE0)
|
|
checkForGLError()
|
|
}
|
|
|
|
val thread = Thread.currentThread()
|
|
val tileRenderers = TileRenderers(this)
|
|
|
|
fun ensureSameThread() {
|
|
if (thread !== Thread.currentThread()) {
|
|
throw IllegalAccessException("Trying to access $this outside of $thread!")
|
|
}
|
|
}
|
|
|
|
fun isSameThread() = thread === Thread.currentThread()
|
|
|
|
fun program(vararg shaders: GLShader): GLShaderProgram {
|
|
return GLShaderProgram(this, *shaders)
|
|
}
|
|
|
|
fun newVBO(type: VBOType = VBOType.ARRAY): GLVertexBufferObject {
|
|
return GLVertexBufferObject(this, type)
|
|
}
|
|
|
|
fun newEBO() = newVBO(VBOType.ELEMENT_ARRAY)
|
|
fun newVAO() = GLVertexArrayObject(this)
|
|
fun newTexture(name: String = "<unknown>") = GLTexture2D(this, name)
|
|
|
|
private val named2DTextures = HashMap<String, GLTexture2D>()
|
|
|
|
fun loadNamedTexture(path: String, memoryFormat: Int = GL_RGBA, fileFormat: Int = GL_RGBA): GLTexture2D {
|
|
return named2DTextures.computeIfAbsent(path) {
|
|
if (!Starbound.pathExists(path)) {
|
|
throw FileNotFoundException("Unable to locate $path")
|
|
}
|
|
|
|
return@computeIfAbsent newTexture(path).upload(Starbound.readDirect(path), memoryFormat, fileFormat).generateMips()
|
|
}
|
|
}
|
|
|
|
private var loadedEmptyTexture = false
|
|
private val missingTexturePath = "/assetmissing.png"
|
|
|
|
fun loadNamedTextureSafe(path: String, memoryFormat: Int = GL_RGBA, fileFormat: Int = GL_RGBA): GLTexture2D {
|
|
if (!loadedEmptyTexture) {
|
|
loadedEmptyTexture = true
|
|
named2DTextures[missingTexturePath] = newTexture(missingTexturePath).upload(Starbound.readDirect(missingTexturePath), memoryFormat, fileFormat).generateMips()
|
|
}
|
|
|
|
return named2DTextures.computeIfAbsent(path) {
|
|
if (!Starbound.pathExists(path)) {
|
|
LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath)
|
|
return@computeIfAbsent named2DTextures[missingTexturePath]!!
|
|
}
|
|
|
|
return@computeIfAbsent newTexture(path).upload(Starbound.readDirect(path), memoryFormat, fileFormat).generateMips()
|
|
}
|
|
}
|
|
|
|
fun bind(obj: GLVertexBufferObject): GLVertexBufferObject {
|
|
if (obj.type == VBOType.ARRAY)
|
|
VBO = obj
|
|
else
|
|
EBO = obj
|
|
|
|
return obj
|
|
}
|
|
|
|
fun unbind(obj: GLVertexBufferObject): GLVertexBufferObject {
|
|
if (obj.type == VBOType.ARRAY)
|
|
if (obj == VBO)
|
|
VBO = null
|
|
else
|
|
if (obj == EBO)
|
|
EBO = null
|
|
|
|
return obj
|
|
}
|
|
|
|
fun bind(obj: GLVertexArrayObject): GLVertexArrayObject {
|
|
VAO = obj
|
|
return obj
|
|
}
|
|
|
|
fun unbind(obj: GLVertexArrayObject): GLVertexArrayObject {
|
|
if (obj == VAO)
|
|
VAO = null
|
|
|
|
return obj
|
|
}
|
|
|
|
fun use(obj: GLShaderProgram): GLShaderProgram {
|
|
ensureSameThread()
|
|
|
|
if (obj == program) {
|
|
return obj
|
|
}
|
|
|
|
check(obj.linked) { "Program is not linked!" }
|
|
|
|
program = obj
|
|
glUseProgram(obj.pointer)
|
|
checkForGLError()
|
|
return obj
|
|
}
|
|
|
|
val shaderVertexTexture: GLTransformableProgram
|
|
val shaderVertexTextureColor: GLTransformableColorableProgram
|
|
|
|
init {
|
|
val textureF = GLShader.internalFragment("shaders/fragment/texture.glsl")
|
|
val textureColorF = GLShader.internalFragment("shaders/fragment/texture_color.glsl")
|
|
val textureV = GLShader.internalVertex("shaders/vertex/texture.glsl")
|
|
|
|
shaderVertexTexture = GLTransformableProgram(this, textureF, textureV)
|
|
shaderVertexTextureColor = GLTransformableColorableProgram(this, textureColorF, textureV)
|
|
|
|
textureF.unlink()
|
|
textureColorF.unlink()
|
|
textureV.unlink()
|
|
}
|
|
|
|
val fontProgram: GLTransformableColorableProgram
|
|
|
|
init {
|
|
val vertex = GLShader.internalVertex("shaders/vertex/font.glsl")
|
|
val fragment = GLShader.internalFragment("shaders/fragment/font.glsl")
|
|
|
|
fontProgram = GLTransformableColorableProgram(this, vertex, fragment)
|
|
|
|
vertex.unlink()
|
|
fragment.unlink()
|
|
}
|
|
|
|
val flatProgram: GLTransformableColorableProgram
|
|
|
|
init {
|
|
val vertex = GLShader.internalVertex("shaders/vertex/flat_vertex_2d.glsl")
|
|
val fragment = GLShader.internalFragment("shaders/fragment/flat_color.glsl")
|
|
|
|
flatProgram = GLTransformableColorableProgram(this, vertex, fragment)
|
|
|
|
vertex.unlink()
|
|
fragment.unlink()
|
|
}
|
|
|
|
val flat2DQuads = object : GLStreamBuilderList {
|
|
override val small by lazy {
|
|
return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.QUADS, 1024)
|
|
}
|
|
|
|
override val statefulSmall by lazy {
|
|
return@lazy StatefulStreamVertexBuilder(this@GLStateTracker, small)
|
|
}
|
|
}
|
|
|
|
val flat2DTexturedQuads = object : GLStreamBuilderList {
|
|
override val small by lazy {
|
|
return@lazy StreamVertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS, 1024)
|
|
}
|
|
|
|
override val statefulSmall by lazy {
|
|
return@lazy StatefulStreamVertexBuilder(this@GLStateTracker, small)
|
|
}
|
|
}
|
|
|
|
val flat2DQuadLines = object : GLStreamBuilderList {
|
|
override val small by lazy {
|
|
return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.QUADS_AS_LINES, 1024)
|
|
}
|
|
|
|
override val statefulSmall by lazy {
|
|
return@lazy StatefulStreamVertexBuilder(this@GLStateTracker, small)
|
|
}
|
|
}
|
|
|
|
val flat2DQuadWireframe = object : GLStreamBuilderList {
|
|
override val small by lazy {
|
|
return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.QUADS_AS_LINES_WIREFRAME, 1024)
|
|
}
|
|
|
|
override val statefulSmall by lazy {
|
|
return@lazy StatefulStreamVertexBuilder(this@GLStateTracker, small)
|
|
}
|
|
}
|
|
|
|
val matrixStack = Matrix4fStack()
|
|
val freeType = FreeType()
|
|
|
|
val font = Font(this)
|
|
|
|
inline fun quadWireframe(color: Color = Color.WHITE, lambda: (StreamVertexBuilder) -> Unit) {
|
|
val stateful = flat2DQuadWireframe.statefulSmall
|
|
val builder = stateful.builder
|
|
|
|
builder.begin()
|
|
|
|
lambda.invoke(builder)
|
|
|
|
stateful.upload()
|
|
|
|
flatProgram.use()
|
|
flatProgram.color.set(color)
|
|
flatProgram.transform.set(matrixStack.last)
|
|
|
|
stateful.draw(GL_LINES)
|
|
}
|
|
|
|
inline fun quadWireframe(value: AABB, color: Color = Color.WHITE, chain: (StreamVertexBuilder) -> Unit = {}) {
|
|
quadWireframe(color) {
|
|
it.quad(value.mins.x.toFloat(), value.mins.y.toFloat(), value.maxs.x.toFloat(), value.maxs.y.toFloat())
|
|
chain(it)
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
private val LOGGER = LogManager.getLogger(GLStateTracker::class.java)
|
|
}
|
|
}
|