568 lines
15 KiB
Kotlin
568 lines
15 KiB
Kotlin
package ru.dbotthepony.kstarbound.client.gl
|
|
|
|
import org.apache.logging.log4j.LogManager
|
|
import org.lwjgl.opengl.GL
|
|
import org.lwjgl.opengl.GL14
|
|
import org.lwjgl.opengl.GL46.*
|
|
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.program.GLPrograms
|
|
import ru.dbotthepony.kstarbound.client.gl.shader.GLShader
|
|
import ru.dbotthepony.kstarbound.client.gl.program.GLShaderProgram
|
|
import ru.dbotthepony.kstarbound.client.gl.shader.GLTransformableColorableProgram
|
|
import ru.dbotthepony.kstarbound.client.gl.shader.GLTransformableProgram
|
|
import ru.dbotthepony.kstarbound.client.gl.vertex.GLAttributeList
|
|
import ru.dbotthepony.kstarbound.client.gl.vertex.StreamVertexBuilder
|
|
import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType
|
|
import ru.dbotthepony.kstarbound.client.gl.vertex.quad
|
|
import ru.dbotthepony.kstarbound.client.render.Box2DRenderer
|
|
import ru.dbotthepony.kstarbound.client.render.Font
|
|
import ru.dbotthepony.kstarbound.client.render.TileRenderers
|
|
import ru.dbotthepony.kvector.api.IStruct4f
|
|
import ru.dbotthepony.kvector.matrix.Matrix4fStack
|
|
import ru.dbotthepony.kvector.util2d.AABB
|
|
import ru.dbotthepony.kvector.vector.Color
|
|
import java.io.FileNotFoundException
|
|
import java.lang.ref.Cleaner
|
|
import java.util.*
|
|
import java.util.concurrent.ThreadFactory
|
|
import kotlin.collections.ArrayList
|
|
import kotlin.collections.HashMap
|
|
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 callback: (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
|
|
|
|
callback.invoke(value)
|
|
checkForGLError()
|
|
this.value = value
|
|
}
|
|
}
|
|
|
|
@Suppress("unused")
|
|
data class BlendFunc(
|
|
val sourceColor: Func,
|
|
val destinationColor: Func,
|
|
val sourceAlpha: Func,
|
|
val destinationAlpha: Func
|
|
) {
|
|
constructor() : this(
|
|
Func.ONE,
|
|
Func.ZERO,
|
|
Func.ONE,
|
|
Func.ZERO
|
|
)
|
|
|
|
constructor(
|
|
source: Func, destination: Func
|
|
) : this(
|
|
source,
|
|
destination,
|
|
source,
|
|
destination
|
|
)
|
|
|
|
enum class Func(val enum: Int) {
|
|
ZERO(GL_ZERO),
|
|
ONE(GL_ONE),
|
|
SRC_COLOR(GL_SRC_COLOR),
|
|
ONE_MINUS_SRC_COLOR(GL_ONE_MINUS_SRC_COLOR),
|
|
SRC_ALPHA(GL_SRC_ALPHA),
|
|
ONE_MINUS_SRC_ALPHA(GL_ONE_MINUS_SRC_ALPHA),
|
|
DST_ALPHA(GL_DST_ALPHA),
|
|
ONE_MINUS_DST_ALPHA(GL_ONE_MINUS_DST_ALPHA),
|
|
DST_COLOR(GL_DST_COLOR),
|
|
ONE_MINUS_DST_COLOR(GL_ONE_MINUS_DST_COLOR),
|
|
SRC_ALPHA_SATURATE(GL_SRC_ALPHA_SATURATE);
|
|
}
|
|
|
|
companion object {
|
|
val DEFAULT = BlendFunc()
|
|
val ALPHA_TEST = BlendFunc(Func.SRC_ALPHA, Func.ONE_MINUS_SRC_ALPHA)
|
|
|
|
val ONLY_ALPHA = BlendFunc(
|
|
Func.ZERO,
|
|
Func.ONE,
|
|
Func.ONE,
|
|
Func.ZERO
|
|
)
|
|
|
|
val ONLY_COLOR = BlendFunc(
|
|
Func.ONE,
|
|
Func.ZERO,
|
|
Func.ZERO,
|
|
Func.ONE
|
|
)
|
|
}
|
|
}
|
|
|
|
interface GLCleanable : Cleaner.Cleanable {
|
|
/**
|
|
* Выставляет флаг на то, что объект был удалён вручную и вызывает clean()
|
|
*/
|
|
fun cleanManual()
|
|
}
|
|
|
|
interface GLStreamBuilderList {
|
|
val small: StreamVertexBuilder
|
|
}
|
|
|
|
@Suppress("PropertyName", "unused")
|
|
class GLStateTracker {
|
|
private fun isMe(state: GLStateTracker?) {
|
|
if (state != null && state != this) {
|
|
throw InvalidArgumentException("Provided object does not belong to $this state tracker (belongs to $state)")
|
|
}
|
|
}
|
|
|
|
init {
|
|
check(TRACKERS.get() == null) { "Already has state tracker existing at this thread!" }
|
|
TRACKERS.set(this)
|
|
// 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 val 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 {} was GC'd by JVM, but it should have been removed manually.", name, nativeRef)
|
|
|
|
synchronized(cleanerHits) {
|
|
cleanerHits.add {
|
|
fn(nativeRef)
|
|
checkForGLError()
|
|
}
|
|
}
|
|
}
|
|
|
|
return object : GLCleanable {
|
|
override fun cleanManual() {
|
|
cleanManual = true
|
|
clean()
|
|
}
|
|
|
|
override fun clean() = cleanable.clean()
|
|
}
|
|
}
|
|
|
|
fun cleanup() {
|
|
synchronized(cleanerHits) {
|
|
for (lambda in cleanerHits) {
|
|
lambda.invoke()
|
|
}
|
|
|
|
cleanerHits.clear()
|
|
}
|
|
}
|
|
|
|
var blend by GLStateSwitchTracker(GL_BLEND)
|
|
var depthTest by GLStateSwitchTracker(GL_DEPTH_TEST)
|
|
|
|
var VBO: VertexBufferObject? = null
|
|
set(value) {
|
|
ensureSameThread()
|
|
if (field === value) return
|
|
isMe(value?.state)
|
|
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: VertexBufferObject? = null
|
|
set(value) {
|
|
ensureSameThread()
|
|
if (field === value) return
|
|
isMe(value?.state)
|
|
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: VertexArrayObject? = null
|
|
set(value) {
|
|
ensureSameThread()
|
|
if (field === value) return
|
|
isMe(value?.state)
|
|
field = value
|
|
|
|
if (value == null) {
|
|
glBindVertexArray(0)
|
|
checkForGLError()
|
|
return
|
|
}
|
|
|
|
glBindVertexArray(value.pointer)
|
|
checkForGLError()
|
|
}
|
|
|
|
var readFramebuffer: GLFrameBuffer? = null
|
|
set(value) {
|
|
ensureSameThread()
|
|
if (field === value) return
|
|
isMe(value?.state)
|
|
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?.state)
|
|
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
|
|
private set
|
|
|
|
var texture2D: GLTexture2D? = null
|
|
set(value) {
|
|
ensureSameThread()
|
|
if (field === value) return
|
|
isMe(value?.state)
|
|
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)
|
|
}
|
|
|
|
var blendFunc by GLStateGenericTracker(BlendFunc()) {
|
|
glBlendFuncSeparate(it.sourceColor.enum, it.destinationColor.enum, it.sourceAlpha.enum, it.destinationAlpha.enum)
|
|
}
|
|
|
|
init {
|
|
glActiveTexture(GL_TEXTURE0)
|
|
checkForGLError()
|
|
}
|
|
|
|
val thread: 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): VertexBufferObject {
|
|
return VertexBufferObject(this, type)
|
|
}
|
|
|
|
fun newEBO() = newVBO(VBOType.ELEMENT_ARRAY)
|
|
fun newVAO() = VertexArrayObject(this)
|
|
fun newTexture(name: String = "<unknown>") = GLTexture2D(this, name)
|
|
|
|
private val named2DTextures = HashMap<String, GLTexture2D>()
|
|
|
|
fun loadNamedTexture(path: String, memoryFormat: Int, fileFormat: Int): 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()
|
|
}
|
|
}
|
|
|
|
fun loadNamedTexture(path: String): GLTexture2D {
|
|
return named2DTextures.computeIfAbsent(path) {
|
|
if (!Starbound.pathExists(path)) {
|
|
throw FileNotFoundException("Unable to locate $path")
|
|
}
|
|
|
|
return@computeIfAbsent newTexture(path).upload(Starbound.readDirect(path)).generateMips()
|
|
}
|
|
}
|
|
|
|
private var loadedEmptyTexture = false
|
|
private val missingTexturePath = "/assetmissing.png"
|
|
|
|
fun loadNamedTextureSafe(path: String, memoryFormat: Int, fileFormat: Int): 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 loadNamedTextureSafe(path: String): GLTexture2D {
|
|
if (!loadedEmptyTexture) {
|
|
loadedEmptyTexture = true
|
|
named2DTextures[missingTexturePath] = newTexture(missingTexturePath).upload(Starbound.readDirect(missingTexturePath), GL_RGBA, GL_RGBA).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)).generateMips()
|
|
}
|
|
}
|
|
|
|
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 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 programs = GLPrograms(this)
|
|
|
|
val flat2DLines = object : GLStreamBuilderList {
|
|
override val small by lazy {
|
|
return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, GeometryType.LINES, 1024)
|
|
}
|
|
}
|
|
|
|
val flat2DTriangles = object : GLStreamBuilderList {
|
|
override val small by lazy {
|
|
return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, GeometryType.TRIANGLES, 1024)
|
|
}
|
|
}
|
|
|
|
val flat2DQuads = object : GLStreamBuilderList {
|
|
override val small by lazy {
|
|
return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, GeometryType.QUADS, 1024)
|
|
}
|
|
}
|
|
|
|
val flat2DTexturedQuads = object : GLStreamBuilderList {
|
|
override val small by lazy {
|
|
return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VERTEX_TEXTURE, GeometryType.QUADS, 1024)
|
|
}
|
|
}
|
|
|
|
val flat2DQuadLines = object : GLStreamBuilderList {
|
|
override val small by lazy {
|
|
return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, GeometryType.QUADS_AS_LINES, 1024)
|
|
}
|
|
}
|
|
|
|
val flat2DQuadWireframe = object : GLStreamBuilderList {
|
|
override val small by lazy {
|
|
return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, GeometryType.QUADS_AS_LINES_WIREFRAME, 1024)
|
|
}
|
|
}
|
|
|
|
val quadWireframe by lazy {
|
|
StreamVertexBuilder(this, GLAttributeList.VEC2F, GeometryType.QUADS_AS_LINES_WIREFRAME, 16384)
|
|
}
|
|
|
|
val matrixStack = Matrix4fStack()
|
|
val freeType = FreeType()
|
|
|
|
val font = Font(this)
|
|
|
|
inline fun quadWireframe(color: Color = Color.WHITE, lambda: (StreamVertexBuilder) -> Unit) {
|
|
val builder = quadWireframe
|
|
|
|
builder.begin()
|
|
lambda.invoke(builder)
|
|
builder.upload()
|
|
|
|
programs.flat.use()
|
|
programs.flat.color.set(color)
|
|
programs.flat.transform.set(matrixStack.last)
|
|
|
|
builder.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)
|
|
}
|
|
}
|
|
|
|
val box2dRenderer = Box2DRenderer(this)
|
|
|
|
companion object {
|
|
private val LOGGER = LogManager.getLogger(GLStateTracker::class.java)
|
|
private val TRACKERS = ThreadLocal<GLStateTracker>()
|
|
}
|
|
}
|