graphics.scenery.backends.opengl.OpenGLRenderer.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of scenery Show documentation
Show all versions of scenery Show documentation
flexible scenegraphing and rendering for scientific visualisation
package graphics.scenery.backends.opengl
import cleargl.*
import com.jogamp.nativewindow.WindowClosingProtocol
import com.jogamp.newt.event.WindowAdapter
import com.jogamp.newt.event.WindowEvent
import com.jogamp.opengl.*
import com.jogamp.opengl.util.FPSAnimator
import com.jogamp.opengl.util.awt.AWTGLReadBufferUtil
import graphics.scenery.*
import graphics.scenery.backends.*
import graphics.scenery.spirvcrossj.Loader
import graphics.scenery.spirvcrossj.libspirvcrossj
import graphics.scenery.utils.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.lwjgl.system.MemoryUtil
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.lang.reflect.Field
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
import java.nio.IntBuffer
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import javax.imageio.ImageIO
import javax.swing.JFrame
import javax.swing.SwingUtilities
import kotlin.collections.LinkedHashMap
import kotlin.concurrent.withLock
import kotlin.math.max
import kotlin.math.roundToInt
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberProperties
/**
* Deferred Lighting Renderer for scenery
*
* This is the main class of scenery's Deferred Lighting Renderer. Currently,
* a rendering strategy using a 32bit position, 16bit normal, 32bit RGBA diffuse/albedo,
* and 24bit depth buffer is employed. The renderer supports HDR rendering and does that
* by default. By deactivating the `hdr.Active` [Settings], HDR can be programmatically
* deactivated. The renderer also supports drawing to HMDs via OpenVR. If this is intended,
* make sure the `vr.Active` [Settings] is set to `true`, and that the `Hub` has a HMD
* instance attached.
*
* @param[hub] Hub instance to use and attach to.
* @param[applicationName] The name of this application.
* @param[scene] The [Scene] instance to initialize first.
* @param[width] Horizontal window size.
* @param[height] Vertical window size.
* @param[embedIn] An optional [SceneryFXPanel] in which to embed the renderer instance.
* @param[renderConfigFile] The file to create a [RenderConfigReader.RenderConfig] from.
*
* @author Ulrik Günther
*/
@Suppress("MemberVisibilityCanBePrivate")
open class OpenGLRenderer(hub: Hub,
applicationName: String,
scene: Scene,
width: Int,
height: Int,
renderConfigFile: String,
final override var embedIn: SceneryPanel? = null,
var embedInDrawable: GLAutoDrawable? = null) : Renderer(), Hubable, ClearGLEventListener {
/** slf4j logger */
private val logger by LazyLogger()
private val className = this.javaClass.simpleName
/** [GL4] instance handed over, coming from [ClearGLDefaultEventListener]*/
private lateinit var gl: GL4
/** should the window close on next looping? */
override var shouldClose = false
/** the scenery window */
final override var window: SceneryWindow = SceneryWindow.UninitializedWindow()
/** separately stored ClearGLWindow */
var cglWindow: ClearGLWindow? = null
/** drawble for offscreen rendering */
var drawable: GLAutoDrawable? = null
/** Whether the renderer manages its own event loop, which is the case for this one. */
override var managesRenderLoop = true
/** The currently active scene */
var scene: Scene = Scene()
/** Cache of [Node]s, needed e.g. for fullscreen quad rendering */
private var nodeStore = ConcurrentHashMap()
/** [Settings] for the renderer */
final override var settings: Settings = Settings()
/** The hub used for communication between the components */
final override var hub: Hub? = null
private var textureCache = HashMap()
private var shaderPropertyCache = HashMap, List>()
private var uboCache = ConcurrentHashMap()
private var joglDrawable: GLAutoDrawable? = null
private var screenshotRequested = false
private var screenshotOverwriteExisting = false
private var screenshotFilename = ""
private var encoder: H264Encoder? = null
private var recordMovie = false
/**
* Activate or deactivate push-based rendering mode (render only on scene changes
* or input events). Push mode is activated if [pushMode] is true.
*/
override var pushMode: Boolean = false
private var updateLatch: CountDownLatch? = null
private var lastResizeTimer = Timer()
@Volatile private var mustRecreateFramebuffers = false
private var framebufferRecreateHook: () -> Unit = {}
private var gpuStats: GPUStats? = null
private var maxTextureUnits = 8
/** heartbeat timer */
private var heartbeatTimer = Timer()
override var lastFrameTime = System.nanoTime() * 1.0f
private var currentTime = System.nanoTime()
override var initialized = false
override var firstImageReady: Boolean = false
protected set
protected var frames = 0L
var fps = 0
protected set
protected var framesPerSec = 0
val pboCount = 2
@Volatile private var pbos: IntArray = IntArray(pboCount) { 0 }
private var pboBuffers: Array = Array(pboCount) { null }
private var readIndex = 0
private var updateIndex = 1
private var renderConfig: RenderConfigReader.RenderConfig
final override var renderConfigFile = ""
set(config) {
field = config
this.renderConfig = RenderConfigReader().loadFromFile(renderConfigFile)
mustRecreateFramebuffers = true
}
private var renderpasses = LinkedHashMap()
private var flow: List
/**
* Extension function of Boolean to use Booleans in GLSL
*
* This function converts a Boolean to Int 0, if false, and to 1, if true
*/
fun Boolean.toInt(): Int {
return if (this) {
1
} else {
0
}
}
var applicationName = ""
inner class ResizeHandler {
@Volatile var lastResize = -1L
var lastWidth = 0
var lastHeight = 0
@Synchronized fun queryResize() {
if (lastWidth <= 0 || lastHeight <= 0) {
lastWidth = Math.max(1, lastWidth)
lastHeight = Math.max(1, lastHeight)
return
}
if (lastResize > 0L && lastResize + WINDOW_RESIZE_TIMEOUT < System.nanoTime()) {
lastResize = System.nanoTime()
return
}
if (lastWidth == window.width && lastHeight == window.height) {
return
}
mustRecreateFramebuffers = true
gl.glDeleteBuffers(pboCount, pbos, 0)
pbos = IntArray(pboCount) { 0 }
lastWidth = window.width
lastHeight = window.height
if(drawable is GLOffscreenAutoDrawable) {
(drawable as? GLOffscreenAutoDrawable)?.setSurfaceSize(window.width, window.height)
}
lastResize = -1L
}
}
/**
* OpenGL Buffer class, creates a buffer associated with the context [gl] and size [size] in bytes.
*
* @author Ulrik Guenther
*/
class OpenGLBuffer(var gl: GL4, var size: Int) {
/** Temporary buffer for data before it is sent to the GPU. */
var buffer: ByteBuffer
private set
/** OpenGL id of the buffer. */
var id = intArrayOf(-1)
private set
/** Required buffer offset alignment for uniform buffers, determined from [GL4.GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT] */
var alignment = 256L
private set
init {
val tmp = intArrayOf(0, 0)
gl.glGetIntegerv(GL4.GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, tmp, 0)
alignment = tmp[0].toLong()
gl.glGenBuffers(1, id, 0)
buffer = MemoryUtil.memAlloc(maxOf(tmp[0], size))
gl.glBindBuffer(GL4.GL_UNIFORM_BUFFER, id[0])
gl.glBufferData(GL4.GL_UNIFORM_BUFFER, size * 1L, null, GL4.GL_DYNAMIC_DRAW)
gl.glBindBuffer(GL4.GL_UNIFORM_BUFFER, 0)
}
/** Copies the [buffer] from main memory to GPU memory. */
fun copyFromStagingBuffer() {
buffer.flip()
gl.glBindBuffer(GL4.GL_UNIFORM_BUFFER, id[0])
gl.glBufferSubData(GL4.GL_UNIFORM_BUFFER, 0, buffer.remaining() * 1L, buffer)
gl.glBindBuffer(GL4.GL_UNIFORM_BUFFER, 0)
}
/** Resets staging buffer position and limit */
fun reset() {
buffer.position(0)
buffer.limit(size)
}
/**
* Resizes the backing buffer to [newSize], which is 1.5x the original size by default,
* and returns the staging buffer.
*/
fun resize(newSize: Int = (buffer.capacity() * 1.5f).roundToInt()): ByteBuffer {
logger.debug("Resizing backing buffer of $this from ${buffer.capacity()} to $newSize")
// resize main memory-backed buffer
buffer = MemoryUtil.memRealloc(buffer, newSize) ?: throw IllegalStateException("Could not resize buffer")
size = buffer.capacity()
// resize OpenGL buffer as well
gl.glBindBuffer(GL4.GL_UNIFORM_BUFFER, id[0])
gl.glBufferData(GL4.GL_UNIFORM_BUFFER, size * 1L, null, GL4.GL_DYNAMIC_DRAW)
gl.glBindBuffer(GL4.GL_UNIFORM_BUFFER, 0)
return buffer
}
/**
* Returns the [buffer]'s remaining bytes.
*/
fun remaining() = buffer.remaining()
/**
* Advances the backing buffer for population, aligning it by [alignment], or any given value
* that overrides it (not recommended), returns the buffers new position.
*/
fun advance(align: Long = this.alignment): Int {
val pos = buffer.position()
val rem = pos.rem(align)
if (rem != 0L) {
val newpos = pos + align.toInt() - rem.toInt()
buffer.position(newpos)
}
return buffer.position()
}
}
protected val buffers = HashMap()
protected val sceneUBOs = CopyOnWriteArrayList()
protected val resizeHandler = ResizeHandler()
companion object {
private const val WINDOW_RESIZE_TIMEOUT = 200L
private const val MATERIAL_HAS_DIFFUSE = 0x0001
private const val MATERIAL_HAS_AMBIENT = 0x0002
private const val MATERIAL_HAS_SPECULAR = 0x0004
private const val MATERIAL_HAS_NORMAL = 0x0008
private const val MATERIAL_HAS_ALPHAMASK = 0x0010
init {
Loader.loadNatives()
libspirvcrossj.initializeProcess()
Runtime.getRuntime().addShutdownHook(object: Thread() {
override fun run() {
logger.debug("Finalizing libspirvcrossj")
libspirvcrossj.finalizeProcess()
}
})
}
}
/**
* Constructor for OpenGLRenderer, initialises geometry buffers
* according to eye configuration. Also initialises different rendering passes.
*
*/
init {
logger.info("Initializing OpenGL Renderer...")
this.hub = hub
this.settings = loadDefaultRendererSettings(hub.get(SceneryElement.Settings) as Settings)
this.window.width = width
this.window.height = height
this.renderConfigFile = renderConfigFile
this.renderConfig = RenderConfigReader().loadFromFile(renderConfigFile)
this.flow = this.renderConfig.createRenderpassFlow()
logger.info("Loaded ${renderConfig.name} (${renderConfig.description ?: "no description"}")
this.scene = scene
this.applicationName = applicationName
val hmd = hub.getWorkingHMDDisplay()
if (settings.get("vr.Active") && hmd != null) {
this.window.width = hmd.getRenderTargetSize().x().toInt() * 2
this.window.height = hmd.getRenderTargetSize().y().toInt()
}
if (embedIn != null || embedInDrawable != null) {
if (embedIn != null && embedInDrawable == null) {
val profile = GLProfile.getMaxProgrammableCore(true)
if (!profile.isGL4) {
throw UnsupportedOperationException("Could not create OpenGL 4 context, perhaps you need a graphics driver update?")
}
val caps = GLCapabilities(profile)
caps.hardwareAccelerated = true
caps.doubleBuffered = true
caps.isOnscreen = false
caps.numSamples = 1
caps.isPBuffer = true
caps.redBits = 8
caps.greenBits = 8
caps.blueBits = 8
caps.alphaBits = 8
val panel = embedIn
/* maybe better?
var canvas: ClearGLWindow? = null
SwingUtilities.invokeAndWait {
canvas = ClearGLWindow("", width, height, this)
canvas!!.newtCanvasAWT.shallUseOffscreenLayer = true
panel.component = canvas!!.newtCanvasAWT
panel.layout = BorderLayout()
panel.add(canvas!!.newtCanvasAWT, BorderLayout.CENTER)
panel.preferredSize = Dimension(width, height)
val frame = SwingUtilities.getAncestorOfClass(JFrame::class.java, panel) as JFrame
frame.preferredSize = Dimension(width, height)
frame.layout = BorderLayout()
frame.pack()
frame.isVisible = true
}
canvas!!.glAutoDrawable
*/
drawable = if (panel is SceneryJPanel) {
val canvas = ClearGLWindow("", width, height, null)
canvas.newtCanvasAWT.shallUseOffscreenLayer = true
panel.component = canvas.newtCanvasAWT
panel.cglWindow = canvas
panel.layout = BorderLayout()
panel.add(canvas.newtCanvasAWT, BorderLayout.CENTER)
panel.preferredSize = Dimension(width, height)
val frame = SwingUtilities.getAncestorOfClass(JFrame::class.java, panel) as JFrame
frame.preferredSize = Dimension(width, height)
frame.pack()
cglWindow = canvas
canvas.glAutoDrawable
} else {
val factory = GLDrawableFactory.getFactory(profile)
factory.createOffscreenAutoDrawable(factory.defaultDevice, caps,
DefaultGLCapabilitiesChooser(), window.width, window.height)
}
} else {
drawable = embedInDrawable
}
drawable?.apply {
addGLEventListener(this@OpenGLRenderer)
animator = FPSAnimator(this, 60)
animator.setUpdateFPSFrames(60, null)
animator.start()
embedInDrawable?.let { glAutoDrawable ->
window = SceneryWindow.JOGLDrawable(glAutoDrawable)
}
window.width = width
window.height = height
resizeHandler.lastWidth = window.width
resizeHandler.lastHeight = window.height
embedIn?.let { panel ->
panel.imageScaleY = -1.0f
when(panel) {
is SceneryFXPanel -> {
panel.widthProperty()?.addListener { _, _, newWidth ->
resizeHandler.lastWidth = newWidth.toInt()
}
panel.heightProperty()?.addListener { _, _, newHeight ->
resizeHandler.lastHeight = newHeight.toInt()
}
window = SceneryWindow.JavaFXStage(panel)
window.width = panel.panelWidth
window.height = panel.panelHeight
}
is SceneryJPanel -> {
window = SceneryWindow.SwingWindow(panel)
window.width = panel.panelWidth
window.height = panel.panelHeight
panel.addComponentListener(object: ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
super.componentResized(e)
logger.debug("SceneryJPanel component resized to ${e.component.width} ${e.component.height}")
resizeHandler.lastWidth = e.component.width
resizeHandler.lastHeight = e.component.height
}
})
}
}
}
resizeHandler.lastWidth = window.width
resizeHandler.lastHeight = window.height
}
} else {
val w = this.window.width
val h = this.window.height
// need to leak this here unfortunately
@Suppress("LeakingThis")
cglWindow = ClearGLWindow("",
w,
h, this).apply {
if(embedIn == null) {
window = SceneryWindow.ClearGLWindow(this)
window.width = w
window.height = h
val windowAdapter = object: WindowAdapter() {
override fun windowDestroyNotify(e: WindowEvent?) {
shouldClose = true
cglWindow?.close()
}
}
this.addWindowListener(windowAdapter)
this.setFPS(60)
this.start()
this.setDefaultCloseOperation(WindowClosingProtocol.WindowClosingMode.DO_NOTHING_ON_CLOSE)
this.isVisible = true
}
}
}
while(!initialized) {
Thread.sleep(20)
}
}
override fun init(pDrawable: GLAutoDrawable) {
this.gl = pDrawable.gl.gL4
val width = this.window.width
val height = this.window.height
gl.swapInterval = 0
val driverString = gl.glGetString(GL4.GL_RENDERER)
val driverVersion = gl.glGetString(GL4.GL_VERSION)
logger.info("OpenGLRenderer: $width x $height on $driverString, $driverVersion")
if (driverVersion.toLowerCase().indexOf("nvidia") != -1 && System.getProperty("os.name").toLowerCase().indexOf("windows") != -1) {
gpuStats = NvidiaGPUStats()
}
val tmp = IntArray(1)
gl.glGetIntegerv(GL4.GL_MAX_TEXTURE_IMAGE_UNITS, tmp, 0)
maxTextureUnits = tmp[0]
val numExtensionsBuffer = IntBuffer.allocate(1)
gl.glGetIntegerv(GL4.GL_NUM_EXTENSIONS, numExtensionsBuffer)
val extensions = (0 until numExtensionsBuffer[0]).map { gl.glGetStringi(GL4.GL_EXTENSIONS, it) }
logger.debug("Available OpenGL extensions: ${extensions.joinToString(", ")}")
settings.set("ssao.FilterRadius", GLVector(5.0f / width, 5.0f / height))
buffers["UBOBuffer"] = OpenGLBuffer(gl, 10 * 1024 * 1024)
buffers["LightParameters"] = OpenGLBuffer(gl, 10 * 1024 * 1024)
buffers["VRParameters"] = OpenGLBuffer(gl, 2 * 1024)
buffers["ShaderPropertyBuffer"] = OpenGLBuffer(gl, 10 * 1024 * 1024)
buffers["ShaderParametersBuffer"] = OpenGLBuffer(gl, 128 * 1024)
prepareDefaultTextures()
renderpasses = prepareRenderpasses(renderConfig, window.width, window.height)
// enable required features
// gl.glEnable(GL4.GL_TEXTURE_GATHER)
gl.glEnable(GL4.GL_PROGRAM_POINT_SIZE)
heartbeatTimer.scheduleAtFixedRate(object : TimerTask() {
override fun run() {
fps = framesPerSec
framesPerSec = 0
if(!pushMode) {
(hub?.get(SceneryElement.Statistics) as? Statistics)?.add("Renderer.fps", fps, false)
}
gpuStats?.let {
it.update(0)
hub?.get(SceneryElement.Statistics).let { s ->
val stats = s as Statistics
stats.add("GPU", it.get("GPU"), isTime = false)
stats.add("GPU bus", it.get("Bus"), isTime = false)
stats.add("GPU mem", it.get("AvailableDedicatedVideoMemory"), isTime = false)
}
if (settings.get("Renderer.PrintGPUStats")) {
logger.info(it.utilisationToString())
logger.info(it.memoryUtilisationToString())
}
}
}
}, 0, 1000)
initialized = true
}
private fun Node.rendererMetadata(): OpenGLObjectState? {
return this.metadata["OpenGLRenderer"] as? OpenGLObjectState
}
fun prepareRenderpasses(config: RenderConfigReader.RenderConfig, windowWidth: Int, windowHeight: Int): LinkedHashMap {
if(config.sRGB) {
gl.glEnable(GL4.GL_FRAMEBUFFER_SRGB)
} else {
gl.glDisable(GL4.GL_FRAMEBUFFER_SRGB)
}
buffers["ShaderParametersBuffer"]!!.reset()
val framebuffers = ConcurrentHashMap()
val passes = LinkedHashMap()
val flow = renderConfig.createRenderpassFlow()
val supersamplingFactor = if(settings.get("Renderer.SupersamplingFactor").toInt() == 1) {
if(cglWindow != null && ClearGLWindow.isRetina(cglWindow!!.gl)) {
logger.debug("Setting Renderer.SupersamplingFactor to 0.5, as we are rendering on a retina display.")
settings.set("Renderer.SupersamplingFactor", 0.5f)
0.5f
} else {
settings.get("Renderer.SupersamplingFactor")
}
} else {
settings.get("Renderer.SupersamplingFactor")
}
scene.findObserver()?.let { cam ->
cam.perspectiveCamera(cam.fov, windowWidth * supersamplingFactor, windowHeight * supersamplingFactor, cam.nearPlaneDistance, cam.farPlaneDistance)
}
settings.set("Renderer.displayWidth", (windowWidth * supersamplingFactor).toInt())
settings.set("Renderer.displayHeight", (windowHeight * supersamplingFactor).toInt())
flow.map { passName ->
val passConfig = config.renderpasses[passName]!!
val pass = OpenGLRenderpass(passName, passConfig)
var width = windowWidth
var height = windowHeight
config.rendertargets.filter { it.key == passConfig.output }.map { rt ->
width = (supersamplingFactor * windowWidth * rt.value.size.first).toInt()
height = (supersamplingFactor * windowHeight * rt.value.size.second).toInt()
logger.info("Creating render framebuffer ${rt.key} for pass $passName (${width}x$height)")
settings.set("Renderer.$passName.displayWidth", width)
settings.set("Renderer.$passName.displayHeight", height)
if (framebuffers.containsKey(rt.key)) {
logger.info("Reusing already created framebuffer")
pass.output.put(rt.key, framebuffers[rt.key]!!)
} else {
val framebuffer = GLFramebuffer(gl, width, height, renderConfig.sRGB)
rt.value.attachments.forEach { att ->
logger.info(" + attachment ${att.key}, ${att.value.name}")
when (att.value) {
RenderConfigReader.TargetFormat.RGBA_Float32 -> framebuffer.addFloatRGBABuffer(gl, att.key, 32)
RenderConfigReader.TargetFormat.RGBA_Float16 -> framebuffer.addFloatRGBABuffer(gl, att.key, 16)
RenderConfigReader.TargetFormat.RGB_Float32 -> framebuffer.addFloatRGBBuffer(gl, att.key, 32)
RenderConfigReader.TargetFormat.RGB_Float16 -> framebuffer.addFloatRGBBuffer(gl, att.key, 16)
RenderConfigReader.TargetFormat.RG_Float32 -> framebuffer.addFloatRGBuffer(gl, att.key, 32)
RenderConfigReader.TargetFormat.RG_Float16 -> framebuffer.addFloatRGBuffer(gl, att.key, 16)
RenderConfigReader.TargetFormat.R_Float16 -> framebuffer.addFloatRBuffer(gl, att.key, 16)
RenderConfigReader.TargetFormat.RGBA_UInt16 -> framebuffer.addUnsignedByteRGBABuffer(gl, att.key, 16)
RenderConfigReader.TargetFormat.RGBA_UInt8 -> framebuffer.addUnsignedByteRGBABuffer(gl, att.key, 8)
RenderConfigReader.TargetFormat.R_UInt16 -> framebuffer.addUnsignedByteRBuffer(gl, att.key, 16)
RenderConfigReader.TargetFormat.R_UInt8 -> framebuffer.addUnsignedByteRBuffer(gl, att.key, 8)
RenderConfigReader.TargetFormat.Depth32 -> framebuffer.addDepthBuffer(gl, att.key, 32)
RenderConfigReader.TargetFormat.Depth24 -> framebuffer.addDepthBuffer(gl, att.key, 24)
}
}
pass.output[rt.key] = framebuffer
framebuffers.put(rt.key, framebuffer)
}
}
if(passConfig.output == "Viewport") {
width = (supersamplingFactor * windowWidth).toInt()
height = (supersamplingFactor * windowHeight).toInt()
logger.info("Creating render framebuffer Viewport for pass $passName (${width}x$height)")
settings.set("Renderer.$passName.displayWidth", width)
settings.set("Renderer.$passName.displayHeight", height)
val framebuffer = GLFramebuffer(gl, width, height)
framebuffer.addUnsignedByteRGBABuffer(gl, "Viewport", 8)
pass.output["Viewport"] = framebuffer
framebuffers["Viewport"] = framebuffer
}
pass.openglMetadata.renderArea = OpenGLRenderpass.Rect2D(
(pass.passConfig.viewportSize.first * width).toInt(),
(pass.passConfig.viewportSize.second * height).toInt(),
(pass.passConfig.viewportOffset.first * width).toInt(),
(pass.passConfig.viewportOffset.second * height).toInt())
logger.debug("Render area for $passName: ${pass.openglMetadata.renderArea.width}x${pass.openglMetadata.renderArea.height}")
pass.openglMetadata.viewport = OpenGLRenderpass.Viewport(OpenGLRenderpass.Rect2D(
(pass.passConfig.viewportSize.first * width).toInt(),
(pass.passConfig.viewportSize.second * height).toInt(),
(pass.passConfig.viewportOffset.first * width).toInt(),
(pass.passConfig.viewportOffset.second * height).toInt()),
0.0f, 1.0f)
pass.openglMetadata.scissor = OpenGLRenderpass.Rect2D(
(pass.passConfig.viewportSize.first * width).toInt(),
(pass.passConfig.viewportSize.second * height).toInt(),
(pass.passConfig.viewportOffset.first * width).toInt(),
(pass.passConfig.viewportOffset.second * height).toInt())
pass.openglMetadata.eye = pass.passConfig.eye
pass.defaultShader = prepareShaderProgram(
Shaders.ShadersFromFiles(pass.passConfig.shaders.map { "shaders/$it" }.toTypedArray()))
pass.initializeShaderParameters(settings, buffers["ShaderParametersBuffer"]!!)
passes.put(passName, pass)
}
// connect inputs
passes.forEach { pass ->
val passConfig = config.renderpasses[pass.key]!!
passConfig.inputs?.forEach { inputTarget ->
val targetName = if(inputTarget.contains(".")) {
inputTarget.substringBefore(".")
} else {
inputTarget
}
passes.filter {
it.value.output.keys.contains(targetName)
}.forEach {
val output = it.value.output[targetName] ?: throw IllegalStateException("Output for $targetName not found in configuration")
pass.value.inputs[inputTarget] = output
}
}
with(pass.value) {
// initialize pass if needed
}
}
return passes
}
protected fun prepareShaderProgram(shaders: Shaders): OpenGLShaderProgram? {
val modules = HashMap()
ShaderType.values().forEach { type ->
try {
val m = OpenGLShaderModule.getFromCacheOrCreate(gl, "main", shaders.get(Shaders.ShaderTarget.OpenGL, type))
modules[m.shaderType] = m
} catch (e: ShaderNotFoundException) {
if(shaders is Shaders.ShadersFromFiles) {
logger.debug("Could not locate shader for $shaders, type=$type, ${shaders.shaders.joinToString(",")} - this is normal if there are no errors reported")
} else {
logger.debug("Could not locate shader for $shaders, type=$type - this is normal if there are no errors reported")
}
}
}
val program = OpenGLShaderProgram(gl, modules)
return if(program.isValid()) {
program
} else {
null
}
}
override fun display(pDrawable: GLAutoDrawable) {
val fps = pDrawable.animator?.lastFPS ?: 0.0f
if(embedIn == null) {
window.title = "$applicationName [${[email protected]}] - ${fps.toInt()} fps"
}
this.joglDrawable = pDrawable
if (mustRecreateFramebuffers) {
logger.info("Recreating framebuffers (${window.width}x${window.height}")
renderpasses = prepareRenderpasses(renderConfig, window.width, window.height)
flow = renderConfig.createRenderpassFlow()
framebufferRecreateHook.invoke()
frames = 0
mustRecreateFramebuffers = false
}
[email protected]()
}
override fun setClearGLWindow(pClearGLWindow: ClearGLWindow) {
cglWindow = pClearGLWindow
}
override fun getClearGLWindow(): ClearGLDisplayable {
return cglWindow!!
}
override fun reshape(pDrawable: GLAutoDrawable,
pX: Int,
pY: Int,
pWidth: Int,
pHeight: Int) {
var height = pHeight
if (height == 0)
height = 1
[email protected](pWidth, height)
}
override fun dispose(pDrawable: GLAutoDrawable) {
cglWindow?.stop()
}
/**
* Based on the [GLFramebuffer], devises a texture unit that can be used
* for object textures.
*
* @param[type] texture type
* @return Int of the texture unit to be used
*/
fun textureTypeToUnit(target: OpenGLRenderpass, type: String): Int {
val offset = if (target.inputs.values.isNotEmpty()) {
target.inputs.values.sumBy { it.boundBufferNum }
} else {
0
}
return offset + when (type) {
"ambient" -> 0
"diffuse" -> 1
"specular" -> 2
"normal" -> 3
"alphamask" -> 4
"displacement" -> 5
"3D-volume" -> 6
else -> {
logger.warn("Unknown ObjecTextures type $type")
0
}
}
}
private fun textureTypeToArrayName(type: String): String {
return when (type) {
"ambient" -> "ObjectTextures[0]"
"diffuse" -> "ObjectTextures[1]"
"specular" -> "ObjectTextures[2]"
"normal" -> "ObjectTextures[3]"
"alphamask" -> "ObjectTextures[4]"
"displacement" -> "ObjectTextures[5]"
"3D-volume" -> "VolumeTextures"
else -> {
logger.debug("Unknown texture type $type")
type
}
}
}
/**
* Converts a [GeometryType] to an OpenGL geometry type
*
* @return Int of the OpenGL geometry type.
*/
private fun GeometryType.toOpenGLType(): Int {
return when (this) {
GeometryType.TRIANGLE_STRIP -> GL4.GL_TRIANGLE_STRIP
GeometryType.POLYGON -> GL4.GL_TRIANGLES
GeometryType.TRIANGLES -> GL4.GL_TRIANGLES
GeometryType.TRIANGLE_FAN -> GL4.GL_TRIANGLE_FAN
GeometryType.POINTS -> GL4.GL_POINTS
GeometryType.LINE -> GL4.GL_LINE_STRIP
GeometryType.LINES_ADJACENCY -> GL4.GL_LINES_ADJACENCY
GeometryType.LINE_STRIP_ADJACENCY -> GL4.GL_LINE_STRIP_ADJACENCY
}
}
fun Int.toggle(): Int {
if (this == 0) {
return 1
} else if (this == 1) {
return 0
}
logger.warn("Property is not togglable.")
return this
}
/**
* Toggles deferred shading buffer debug view. Used for e.g.
* [graphics.scenery.controls.behaviours.ToggleCommand]
*/
@Suppress("UNUSED")
fun toggleDebug() {
settings.getAllSettings().forEach {
if (it.toLowerCase().contains("debug")) {
try {
val property = settings.get(it).toggle()
settings.set(it, property)
} catch(e: Exception) {
logger.warn("$it is a property that is not togglable.")
}
}
}
}
/**
* Toggles Screen-space ambient occlusion. Used for e.g.
* [graphics.scenery.controls.behaviours.ToggleCommand].
*/
@Suppress("UNUSED")
fun toggleSSAO() {
if (!settings.get("ssao.Active")) {
settings.set("ssao.Active", true)
} else {
settings.set("ssao.Active", false)
}
}
/**
* Toggles HDR rendering. Used for e.g.
* [graphics.scenery.controls.behaviours.ToggleCommand].
*/
@Suppress("UNUSED")
fun toggleHDR() {
if (!settings.get("hdr.Active")) {
settings.set("hdr.Active", true)
} else {
settings.set("hdr.Active", false)
}
}
/**
* Increases the HDR exposure value. Used for e.g.
* [graphics.scenery.controls.behaviours.ToggleCommand].
*/
@Suppress("UNUSED")
fun increaseExposure() {
val exp: Float = settings.get("hdr.Exposure")
settings.set("hdr.Exposure", exp + 0.05f)
}
/**
* Decreases the HDR exposure value.Used for e.g.
* [graphics.scenery.controls.behaviours.ToggleCommand].
*/
@Suppress("UNUSED")
fun decreaseExposure() {
val exp: Float = settings.get("hdr.Exposure")
settings.set("hdr.Exposure", exp - 0.05f)
}
/**
* Increases the HDR gamma value. Used for e.g.
* [graphics.scenery.controls.behaviours.ToggleCommand].
*/
@Suppress("unused")
fun increaseGamma() {
val gamma: Float = settings.get("hdr.Gamma")
settings.set("hdr.Gamma", gamma + 0.05f)
}
/**
* Decreases the HDR gamma value. Used for e.g.
* [graphics.scenery.controls.behaviours.ToggleCommand].
*/
@Suppress("unused")
fun decreaseGamma() {
val gamma: Float = settings.get("hdr.Gamma")
if (gamma - 0.05f >= 0) settings.set("hdr.Gamma", gamma - 0.05f)
}
/**
* Toggles fullscreen. Used for e.g.
* [graphics.scenery.controls.behaviours.ToggleCommand].
*/
@Suppress("unused")
fun toggleFullscreen() {
if (!settings.get("wantsFullscreen")) {
settings.set("wantsFullscreen", true)
} else {
settings.set("wantsFullscreen", false)
}
}
/**
* Convenience function that extracts the [OpenGLObjectState] from a [Node]'s
* metadata.
*
* @param[node] The node of interest
* @return The [OpenGLObjectState] of the [Node]
*/
fun getOpenGLObjectStateFromNode(node: Node): OpenGLObjectState {
return node.metadata["OpenGLRenderer"] as OpenGLObjectState
}
/**
* Initializes the [Scene] with the [OpenGLRenderer], to be called
* before [render].
*/
@Synchronized override fun initializeScene() {
scene.discover(scene, { it is HasGeometry })
.forEach { it ->
it.metadata["OpenGLRenderer"] = OpenGLObjectState()
initializeNode(it)
}
scene.initialized = true
logger.info("Initialized ${textureCache.size} textures")
}
private fun Display.wantsVR(): Display? {
return if (settings.get("vr.Active")) {
this@wantsVR
} else {
null
}
}
private fun findBuffer(name: String): OpenGLBuffer {
return buffers[name] ?: throw IllegalStateException("Required buffer $name not found")
}
@Synchronized protected fun updateDefaultUBOs(): Boolean {
// sticky boolean
var updated: Boolean by StickyBoolean(initial = false)
// find observer, if none, return
val cam = scene.findObserver() ?: return false
val hmd = hub?.getWorkingHMDDisplay()?.wantsVR()
cam.view = cam.getTransformation()
cam.updateWorld(true, false)
buffers["VRParameters"]!!.reset()
val vrUbo = uboCache.computeIfAbsent("VRParameters") {
OpenGLUBO(backingBuffer = findBuffer("VRParameters"))
}
vrUbo.add("projection0", {
(hmd?.getEyeProjection(0, cam.nearPlaneDistance, cam.farPlaneDistance)
?: cam.projection)
})
vrUbo.add("projection1", {
(hmd?.getEyeProjection(1, cam.nearPlaneDistance, cam.farPlaneDistance)
?: cam.projection)
})
vrUbo.add("inverseProjection0", {
(hmd?.getEyeProjection(0, cam.nearPlaneDistance, cam.farPlaneDistance)
?: cam.projection).inverse
})
vrUbo.add("inverseProjection1", {
(hmd?.getEyeProjection(1, cam.nearPlaneDistance, cam.farPlaneDistance)
?: cam.projection).inverse
})
vrUbo.add("headShift", { hmd?.getHeadToEyeTransform(0) ?: GLMatrix.getIdentity() })
vrUbo.add("IPD", { hmd?.getIPD() ?: 0.05f })
vrUbo.add("stereoEnabled", { renderConfig.stereoEnabled.toInt() })
updated = vrUbo.populate()
buffers["VRParameters"]!!.copyFromStagingBuffer()
buffers["UBOBuffer"]!!.reset()
buffers["ShaderPropertyBuffer"]!!.reset()
sceneUBOs.forEach { node ->
node.lock.withLock {
var nodeUpdated: Boolean by StickyBoolean(initial = false)
if (!node.metadata.containsKey(className)) {
return@withLock
}
val s = node.metadata[className] as? OpenGLObjectState
if(s == null) {
logger.warn("Could not get OpenGLObjectState for ${node.name}")
return@forEach
}
val ubo = s.UBOs["Matrices"]
if(ubo?.backingBuffer == null) {
logger.warn("Matrices UBO for ${node.name} does not exist or does not have a backing buffer")
return@forEach
}
node.updateWorld(true, false)
var bufferOffset = ubo.advanceBackingBuffer()
ubo.offset = bufferOffset
node.view.copyFrom(cam.view)
nodeUpdated = ubo.populate(offset = bufferOffset.toLong())
val materialUbo = (node.metadata["OpenGLRenderer"]!! as OpenGLObjectState).UBOs["MaterialProperties"]!!
bufferOffset = ubo.advanceBackingBuffer()
materialUbo.offset = bufferOffset
nodeUpdated = materialUbo.populate(offset = bufferOffset.toLong())
if (s.UBOs.containsKey("ShaderProperties")) {
val propertyUbo = s.UBOs["ShaderProperties"]!!
// TODO: Correct buffer advancement
val offset = propertyUbo.backingBuffer!!.advance()
updated = propertyUbo.populate(offset = offset.toLong())
propertyUbo.offset = offset
}
nodeUpdated = if (node.material.needsTextureReload) {
loadTexturesForNode(node, s)
true
} else {
false
}
nodeUpdated = if(node.material.hashCode() != s.materialHash) {
s.initialized = false
initializeNode(node)
true
} else {
false
}
if(nodeUpdated && node.getScene()?.onNodePropertiesChanged?.isNotEmpty() == true) {
GlobalScope.launch { node.getScene()?.onNodePropertiesChanged?.forEach { it.value.invoke(node) } }
}
updated = nodeUpdated
}
}
buffers["UBOBuffer"]!!.copyFromStagingBuffer()
buffers["LightParameters"]!!.reset()
// val lights = sceneObjects.filter { it is PointLight }
val lightUbo = uboCache.computeIfAbsent("LightParameters") {
OpenGLUBO(backingBuffer = findBuffer("LightParameters"))
}
lightUbo.add("ViewMatrix0", { cam.getTransformationForEye(0) })
lightUbo.add("ViewMatrix1", { cam.getTransformationForEye(1) })
lightUbo.add("InverseViewMatrix0", { cam.getTransformationForEye(0).inverse })
lightUbo.add("InverseViewMatrix1", { cam.getTransformationForEye(1).inverse })
lightUbo.add("ProjectionMatrix", { cam.projection })
lightUbo.add("InverseProjectionMatrix", { cam.projection.inverse })
lightUbo.add("CamPosition", { cam.position })
// lightUbo.add("numLights", { lights.size })
// lights.forEachIndexed { i, light ->
// val l = light as PointLight
// l.updateWorld(true, false)
//
// lightUbo.add("Linear-$i", { l.linear })
// lightUbo.add("Quadratic-$i", { l.quadratic })
// lightUbo.add("Intensity-$i", { l.intensity })
// lightUbo.add("Radius-$i", { -l.linear + Math.sqrt(l.linear * l.linear - 4 * l.quadratic * (1.0 - (256.0f / 5.0) * 100)).toFloat() })
// lightUbo.add("Position-$i", { l.position })
// lightUbo.add("Color-$i", { l.emissionColor })
// lightUbo.add("filler-$i", { 0.0f })
// }
updated = lightUbo.populate()
buffers["ShaderParametersBuffer"]?.let { shaderParametersBuffer ->
shaderParametersBuffer.reset()
renderpasses.forEach { name, pass ->
logger.trace("Updating shader parameters for {}", name)
updated = pass.updateShaderParameters()
}
shaderParametersBuffer.copyFromStagingBuffer()
}
buffers["LightParameters"]!!.copyFromStagingBuffer()
buffers["ShaderPropertyBuffer"]!!.copyFromStagingBuffer()
return updated
}
/**
* Update a [Node]'s geometry, if needed and run it's preDraw() routine.
*
* @param[n] The Node to update and preDraw()
*/
private fun preDrawAndUpdateGeometryForNode(n: Node) {
if (n is HasGeometry) {
if (n.dirty) {
n.preUpdate(this, hub!!)
if (n.lock.tryLock()) {
if (n.vertices.remaining() > 0 && n.normals.remaining() > 0) {
updateVertices(n)
updateNormals(n)
}
if (n.texcoords.remaining() > 0) {
updateTextureCoords(n)
}
if (n.indices.remaining() > 0) {
updateIndices(n)
}
n.dirty = false
n.lock.unlock()
}
}
n.preDraw()
}
}
/**
* Set a [GLProgram]'s uniforms according to a [Node]'s [ShaderProperty]s.
*
* This functions uses reflection to query for a Node's declared fields and checks
* whether they are marked up with the [ShaderProperty] annotation. If this is the case,
* the [GLProgram]'s uniform with the same name as the field is set to its value.
*
* Currently limited to GLVector, GLMatrix, Int and Float properties.
*
* @param[n] The Node to search for [ShaderProperty]s
* @param[program] The [GLProgram] used to render the Node
*/
@Suppress("unused")
private fun setShaderPropertiesForNode(n: Node, program: GLProgram) {
shaderPropertyCache
.getOrPut(n.javaClass) { n.javaClass.declaredFields.filter { it.isAnnotationPresent(ShaderProperty::class.java) } }
.forEach { property ->
property.isAccessible = true
val field = property.get(n)
when (property.type) {
GLVector::class.java -> {
program.getUniform(property.name).setFloatVector(field as GLVector)
}
Int::class.java -> {
program.getUniform(property.name).setInt(field as Int)
}
Float::class.java -> {
program.getUniform(property.name).setFloat(field as Float)
}
GLMatrix::class.java -> {
program.getUniform(property.name).setFloatMatrix((field as GLMatrix).floatArray, false)
}
else -> {
logger.warn("Could not derive shader data type for @ShaderProperty ${n.javaClass.canonicalName}.${property.name} of type ${property.type}!")
}
}
}
}
private fun blitFramebuffers(source: GLFramebuffer?, target: GLFramebuffer?,
sourceOffset: OpenGLRenderpass.Rect2D,
targetOffset: OpenGLRenderpass.Rect2D,
colorOnly: Boolean = false, depthOnly: Boolean = false,
sourceName: String? = null) {
if (target != null) {
target.setDrawBuffers(gl)
} else {
gl.glBindFramebuffer(GL4.GL_DRAW_FRAMEBUFFER, 0)
}
if (source != null) {
if(sourceName != null) {
source.setReadBuffers(gl, sourceName)
} else {
source.setReadBuffers(gl)
}
} else {
gl.glBindFramebuffer(GL4.GL_READ_FRAMEBUFFER, 0)
}
val (blitColor, blitDepth) = when {
colorOnly && !depthOnly -> true to false
!colorOnly && depthOnly -> false to true
else -> true to true
}
if(blitColor) {
if (source?.hasColorAttachment() != false) {
gl.glBlitFramebuffer(
sourceOffset.offsetX, sourceOffset.offsetY,
sourceOffset.offsetX + sourceOffset.width, sourceOffset.offsetY + sourceOffset.height,
targetOffset.offsetX, targetOffset.offsetY,
targetOffset.offsetX + targetOffset.width, targetOffset.offsetY + targetOffset.height,
GL4.GL_COLOR_BUFFER_BIT, GL4.GL_LINEAR)
}
}
if(blitDepth) {
if ((source?.hasDepthAttachment() != false && target?.hasDepthAttachment() != false) || (depthOnly && !colorOnly)) {
gl.glBlitFramebuffer(
sourceOffset.offsetX, sourceOffset.offsetY,
sourceOffset.offsetX + sourceOffset.width, sourceOffset.offsetY + sourceOffset.height,
targetOffset.offsetX, targetOffset.offsetY,
targetOffset.offsetX + targetOffset.width, targetOffset.offsetY + targetOffset.height,
GL4.GL_DEPTH_BUFFER_BIT, GL4.GL_NEAREST)
} else {
logger.trace("Either source or target don't have a depth buffer. If blitting to window surface, this is not a problem.")
}
}
gl.glBindFramebuffer(GL4.GL_FRAMEBUFFER, 0)
}
private fun updateInstanceBuffers(sceneObjects:List) {
val instanceMasters = sceneObjects.filter { it.instances.size > 0 }
instanceMasters.forEach { parent ->
var metadata = parent.rendererMetadata()
if(metadata == null) {
parent.metadata["OpenGLRenderer"] = OpenGLObjectState()
initializeNode(parent)
metadata = parent.rendererMetadata()
}
updateInstanceBuffer(parent, metadata)
}
}
private fun updateInstanceBuffer(parentNode: Node, state: OpenGLObjectState?): OpenGLObjectState {
if(state == null) {
throw IllegalStateException("Metadata for ${parentNode.name} is null at updateInstanceBuffer(${parentNode.name}). This is a bug.")
}
// parentNode.instances is a CopyOnWrite array list, and here we keep a reference to the original.
// If it changes in the meantime, no problemo.
val instances = parentNode.instances
logger.trace("Updating instance buffer for ${parentNode.name}")
if (instances.isEmpty()) {
logger.debug("$parentNode has no child instances attached, returning.")
return state
}
// first we create a fake UBO to gauge the size of the needed properties
val ubo = OpenGLUBO()
ubo.fromInstance(instances.first())
val instanceBufferSize = ubo.getSize() * instances.size
val existingStagingBuffer = state.vertexBuffers["instanceStaging"]
val stagingBuffer = if(existingStagingBuffer != null
&& existingStagingBuffer.capacity() >= instanceBufferSize
&& existingStagingBuffer.capacity() < 1.5*instanceBufferSize) {
existingStagingBuffer
} else {
logger.debug("${parentNode.name}: Creating new staging buffer with capacity=$instanceBufferSize (${ubo.getSize()} x ${parentNode.instances.size})")
val buffer = BufferUtils.allocateByte((1.2 * instanceBufferSize).toInt())
state.vertexBuffers["instanceStaging"] = buffer
buffer
}
logger.trace("{}: Staging buffer position, {}, cap={}", parentNode.name, stagingBuffer.position(), stagingBuffer.capacity())
val index = AtomicInteger(0)
instances.parallelStream().forEach { node ->
node.needsUpdate = true
node.needsUpdateWorld = true
node.updateWorld(true, false)
stagingBuffer.duplicate().order(ByteOrder.LITTLE_ENDIAN).run {
ubo.populateParallel(this, offset = index.getAndIncrement() * ubo.getSize()*1L, elements = node.instancedProperties)
}
}
stagingBuffer.position(stagingBuffer.limit())
stagingBuffer.flip()
val instanceBuffer = state.additionalBufferIds.getOrPut("instance") {
logger.debug("Instance buffer for ${parentNode.name} needs to be reallocated due to insufficient size ($instanceBufferSize vs ${state.vertexBuffers["instance"]?.capacity() ?: ""})")
val bufferArray = intArrayOf(0)
gl.glGenBuffers(1, bufferArray, 0)
gl.glBindVertexArray(state.mVertexArrayObject[0])
gl.glBindBuffer(GL4.GL_ARRAY_BUFFER, bufferArray[0])
// instance data starts after vertex, normal, texcoord
val locationBase = 3
var location = locationBase
parentNode.instances.first().instancedProperties.entries.forEachIndexed { locationOffset, element ->
val result = element.value.invoke()
val sizeAlignment = ubo.getSizeAndAlignment(result)
location += locationOffset
val glType = when(result.javaClass) {
java.lang.Integer::class.java,
Int::class.java -> GL4.GL_INT
java.lang.Float::class.java,
Float::class.java -> GL4.GL_FLOAT
java.lang.Boolean::class.java,
Boolean::class.java -> GL4.GL_INT
GLMatrix::class.java -> GL4.GL_FLOAT
GLVector::class.java -> GL4.GL_FLOAT
else -> { logger.error("Don't know how to serialise ${result.javaClass} for instancing."); GL4.GL_FLOAT }
}
val count = when (result) {
is GLMatrix -> 4
is GLVector -> result.toFloatArray().size
else -> { logger.error("Don't know element size of ${result.javaClass} for instancing."); 1 }
}
val necessaryAttributes = if(result is GLMatrix) {
result.floatArray.size / count
} else {
1
}
logger.trace("{} needs {} locations with {} elements:", result.javaClass, necessaryAttributes, count)
(0 until necessaryAttributes).forEach { attributeLocation ->
val stride = sizeAlignment.first
val offset = 1L * attributeLocation * (sizeAlignment.first/necessaryAttributes)
logger.trace("{}, stride={}, offset={}",
location + attributeLocation,
stride,
offset)
gl.glEnableVertexAttribArray(location + attributeLocation)
// glVertexAttribPoint takes parameters:
// * index: attribute location
// * size: element count per location
// * type: the OpenGL type
// * normalized: whether the OpenGL type should be taken as normalized
// * stride: the stride between different occupants in the instance array
// * pointer_buffer_offset: the offset of the current element (e.g. matrix row) with respect
// to the start of the element in the instance array
gl.glVertexAttribPointer(location + attributeLocation, count, glType, false,
stride, offset)
gl.glVertexAttribDivisor(location + attributeLocation, 1)
}
location += necessaryAttributes
}
gl.glBindVertexArray(0)
state.additionalBufferIds["instance"] = bufferArray[0]
bufferArray[0]
}
gl.glBindBuffer(GL4.GL_ARRAY_BUFFER, instanceBuffer)
gl.glBufferData(GL4.GL_ARRAY_BUFFER, instanceBufferSize.toLong(), stagingBuffer, GL4.GL_DYNAMIC_DRAW)
gl.glBindBuffer(GL4.GL_ARRAY_BUFFER, 0)
state.instanceCount = parentNode.instances.size
logger.trace("Updated instance buffer, {parentNode.name} has {} instances.", parentNode.name, state.instanceCount)
return state
}
protected fun destroyNode(node: Node) {
node.metadata.remove("OpenGLRenderer")
node.initialized = false
}
protected var previousSceneObjects: Array = emptyArray()
/**
* Renders the [Scene].
*
* The general rendering workflow works like this:
*
* 1) All visible elements of the Scene are gathered into the renderOrderList, based on their position
* 2) Nodes that are an instance of another Node, as indicated by their instanceOf property, are gathered
* into the instanceGroups map.
* 3) The eye-dependent geometry buffers are cleared, both color and depth buffers.
* 4) First for the non-instanced Nodes, then for the instanced Nodes the following steps are executed
* for each eye:
*
* i) The world state of the given Node is updated
* ii) Model, view, and model-view-projection matrices are calculated for each. If a HMD is present,
* the transformation coming from that is taken into account.
* iii) The Node's geometry is updated, if necessary.
* iv) The eye's geometry buffer is activated and the Node drawn into it.
*
* 5) The deferred shading pass is executed, together with eventual post-processing steps, such as SSAO.
* 6) If HDR is active, Exposure/Gamma tone mapping is performed. Else, this part is skipped.
* 7) The resulting image is drawn to the screen, or -- if a HMD is present -- submitted to the OpenVR
* compositor.
*/
@Synchronized override fun render() = runBlocking {
if(!initialized) {
return@runBlocking
}
if (scene.children.count() == 0 || !scene.initialized) {
initializeScene()
return@runBlocking
}
val newTime = System.nanoTime()
lastFrameTime = (System.nanoTime() - currentTime)/1e6f
currentTime = newTime
val stats = hub?.get(SceneryElement.Statistics) as? Statistics
hub?.getWorkingHMD()?.update()
if (shouldClose) {
try {
logger.info("Closing window")
scene.discover(scene, { _ -> true }).forEach {
destroyNode(it)
}
scene.initialized = false
joglDrawable?.animator?.stop()
cglWindow?.close()
} catch(e: ThreadDeath) {
logger.debug("Caught JOGL ThreadDeath, ignoring.")
}
return@runBlocking
}
val running = hub?.getApplication()?.running ?: true
embedIn?.let {
resizeHandler.queryResize()
}
if (scene.children.count() == 0 || renderpasses.isEmpty() || mustRecreateFramebuffers || !running) {
Thread.sleep(200)
return@runBlocking
}
val cam = scene.findObserver() ?: return@runBlocking
val sceneObjects = async {
scene.discover(scene, { n ->
n is HasGeometry
&& n.visible
&& cam.canSee(n)
}, useDiscoveryBarriers = true)
}
val startUboUpdate = System.nanoTime()
val updated = updateDefaultUBOs()
stats?.add("OpenGLRenderer.updateUBOs", System.nanoTime() - startUboUpdate)
val actualSceneObjects = sceneObjects.await().toTypedArray()
val sceneUpdated = !actualSceneObjects.contentDeepEquals(previousSceneObjects)
previousSceneObjects = actualSceneObjects
val startInstanceUpdate = System.nanoTime()
updateInstanceBuffers(sceneObjects.await())
stats?.add("OpenGLRenderer.updateInstanceBuffers", System.nanoTime() - startInstanceUpdate)
if(pushMode && !updated && !sceneUpdated && !screenshotRequested) {
if(updateLatch == null) {
updateLatch = CountDownLatch(2)
}
logger.trace("UBOs have not been updated, returning ({})", updateLatch?.count)
if(updateLatch?.count == 0L) {
// val animator = when {
// cglWindow != null -> cglWindow?.glAutoDrawable?.animator
// drawable != null -> drawable?.animator
// else -> null
// } as? FPSAnimator
//
// if(animator != null && animator.fps > 15) {
// animator.stop()
// animator.fps = 15
// animator.start()
// }
Thread.sleep(15)
return@runBlocking
}
}
if(updated || sceneUpdated || screenshotRequested) {
updateLatch = null
}
flow.forEach { t ->
if(logger.isDebugEnabled || logger.isTraceEnabled) {
val error = gl.glGetError()
if (error != 0) {
throw Exception("OpenGL error: $error")
}
}
val pass = renderpasses[t]!!
logger.trace("Running pass {}", pass.passName)
val startPass = System.nanoTime()
if (pass.passConfig.blitInputs) {
pass.inputs.forEach { name, input ->
val targetName = name.substringAfter(".")
if(name.contains(".") && input.getTextureType(targetName) == 0) {
logger.trace("Blitting {} into {} (color only)", targetName, pass.output.values.first().id)
blitFramebuffers(input, pass.output.values.firstOrNull(),
pass.openglMetadata.viewport.area, pass.openglMetadata.viewport.area, colorOnly = true, sourceName = name.substringAfter("."))
} else if(name.contains(".") && input.getTextureType(targetName) == 1) {
logger.trace("Blitting {} into {} (depth only)", targetName, pass.output.values.first().id)
blitFramebuffers(input, pass.output.values.firstOrNull(),
pass.openglMetadata.viewport.area, pass.openglMetadata.viewport.area, depthOnly = true)
} else {
logger.trace("Blitting {} into {}", targetName, pass.output.values.first().id)
blitFramebuffers(input, pass.output.values.firstOrNull(),
pass.openglMetadata.viewport.area, pass.openglMetadata.viewport.area)
}
}
}
if (pass.output.isNotEmpty()) {
pass.output.values.first().setDrawBuffers(gl)
} else {
gl.glBindFramebuffer(GL4.GL_FRAMEBUFFER, 0)
}
// bind framebuffers to texture units and determine total number
pass.inputs.values.reversed().fold(0) { acc, fb -> acc + fb.bindTexturesToUnitsWithOffset(gl, acc) }
gl.glViewport(
pass.openglMetadata.viewport.area.offsetX,
pass.openglMetadata.viewport.area.offsetY,
pass.openglMetadata.viewport.area.width,
pass.openglMetadata.viewport.area.height)
gl.glScissor(
pass.openglMetadata.scissor.offsetX,
pass.openglMetadata.scissor.offsetY,
pass.openglMetadata.scissor.width,
pass.openglMetadata.scissor.height
)
gl.glEnable(GL4.GL_SCISSOR_TEST)
gl.glClearColor(
pass.openglMetadata.clearValues.clearColor.x(),
pass.openglMetadata.clearValues.clearColor.y(),
pass.openglMetadata.clearValues.clearColor.z(),
pass.openglMetadata.clearValues.clearColor.w())
if (!pass.passConfig.blitInputs) {
pass.output.values.forEach {
if (it.hasDepthAttachment()) {
gl.glClear(GL4.GL_DEPTH_BUFFER_BIT)
}
}
gl.glClear(GL4.GL_COLOR_BUFFER_BIT)
}
gl.glDisable(GL4.GL_SCISSOR_TEST)
gl.glDepthRange(
pass.openglMetadata.viewport.minDepth.toDouble(),
pass.openglMetadata.viewport.maxDepth.toDouble())
if (pass.passConfig.type == RenderConfigReader.RenderpassType.geometry ||
pass.passConfig.type == RenderConfigReader.RenderpassType.lights) {
gl.glEnable(GL4.GL_DEPTH_TEST)
gl.glEnable(GL4.GL_CULL_FACE)
if (pass.passConfig.renderTransparent) {
gl.glEnable(GL4.GL_BLEND)
gl.glBlendFuncSeparate(
pass.passConfig.srcColorBlendFactor.toOpenGL(),
pass.passConfig.dstColorBlendFactor.toOpenGL(),
pass.passConfig.srcAlphaBlendFactor.toOpenGL(),
pass.passConfig.dstAlphaBlendFactor.toOpenGL())
gl.glBlendEquationSeparate(
pass.passConfig.colorBlendOp.toOpenGL(),
pass.passConfig.alphaBlendOp.toOpenGL()
)
} else {
gl.glDisable(GL4.GL_BLEND)
}
val actualObjects = if(pass.passConfig.type == RenderConfigReader.RenderpassType.geometry) {
actualSceneObjects.filter { it !is Light }
} else {
actualSceneObjects.filter { it is Light }
}
var currentShader: OpenGLShaderProgram? = null
actualObjects.forEach renderLoop@ { n ->
if (pass.passConfig.renderOpaque && n.material.blending.transparent && pass.passConfig.renderOpaque != pass.passConfig.renderTransparent) {
return@renderLoop
}
if (pass.passConfig.renderTransparent && !n.material.blending.transparent && pass.passConfig.renderOpaque != pass.passConfig.renderTransparent) {
return@renderLoop
}
gl.glEnable(GL4.GL_CULL_FACE)
when(n.material.cullingMode) {
Material.CullingMode.None -> gl.glDisable(GL4.GL_CULL_FACE)
Material.CullingMode.Front -> gl.glCullFace(GL4.GL_FRONT)
Material.CullingMode.Back -> gl.glCullFace(GL4.GL_BACK)
Material.CullingMode.FrontAndBack -> gl.glCullFace(GL4.GL_FRONT_AND_BACK)
}
if (n.material.blending.transparent) {
with(n.material.blending) {
gl.glBlendFuncSeparate(
sourceColorBlendFactor.toOpenGL(),
destinationColorBlendFactor.toOpenGL(),
sourceAlphaBlendFactor.toOpenGL(),
destinationAlphaBlendFactor.toOpenGL()
)
gl.glBlendEquationSeparate(
colorBlending.toOpenGL(),
alphaBlending.toOpenGL()
)
}
}
if (!n.metadata.containsKey("OpenGLRenderer") || !n.initialized) {
n.metadata["OpenGLRenderer"] = OpenGLObjectState()
initializeNode(n)
return@renderLoop
}
val s = getOpenGLObjectStateFromNode(n)
if (n is Skybox) {
gl.glCullFace(GL4.GL_FRONT)
gl.glDepthFunc(GL4.GL_LEQUAL)
}
preDrawAndUpdateGeometryForNode(n)
val shader = if (s.shader != null) {
s.shader!!
} else {
pass.defaultShader!!
}
if(currentShader != shader) {
shader.use(gl)
}
currentShader = shader
if (renderConfig.stereoEnabled) {
shader.getUniform("currentEye.eye").setInt(pass.openglMetadata.eye)
}
var unit = 0
pass.inputs.keys.reversed().forEach { name ->
renderConfig.rendertargets[name.substringBefore(".")]?.attachments?.forEach {
shader.getUniform("Input" + it.key).setInt(unit)
unit++
}
}
val unboundSamplers = (unit until maxTextureUnits).toMutableList()
var maxSamplerIndex = 0
val textures = s.textures.entries.groupBy { GenericTexture.objectTextures.contains(it.key) }
val objectTextures = textures[true]
val others = textures[false]
objectTextures?.forEach { texture ->
val samplerIndex = textureTypeToUnit(pass, texture.key)
maxSamplerIndex = max(samplerIndex, maxSamplerIndex)
@Suppress("SENSELESS_COMPARISON")
if (texture.value != null) {
gl.glActiveTexture(GL4.GL_TEXTURE0 + samplerIndex)
val target = if (texture.value.depth > 1) {
GL4.GL_TEXTURE_3D
} else {
GL4.GL_TEXTURE_2D
}
gl.glBindTexture(target, texture.value.id)
shader.getUniform(textureTypeToArrayName(texture.key)).setInt(samplerIndex)
unboundSamplers.remove(samplerIndex)
}
}
var samplerIndex = maxSamplerIndex
others?.forEach { texture ->
@Suppress("SENSELESS_COMPARISON")
if(texture.value != null) {
val minIndex = unboundSamplers.min() ?: maxSamplerIndex
gl.glActiveTexture(GL4.GL_TEXTURE0 + minIndex)
val target = if (texture.value.depth > 1) {
GL4.GL_TEXTURE_3D
} else {
GL4.GL_TEXTURE_2D
}
gl.glBindTexture(target, texture.value.id)
shader.getUniform(texture.key).setInt(minIndex)
samplerIndex++
unboundSamplers.remove(minIndex)
}
}
var binding = 0
(s.UBOs + pass.UBOs).forEach { name, ubo ->
val actualName = if (name.contains("ShaderParameters")) {
"ShaderParameters"
} else {
name
}
if(shader.uboSpecs.containsKey(actualName) && shader.isValid()) {
val index = shader.getUniformBlockIndex(actualName)
logger.trace("Binding {} for {}, index={}, binding={}, size={}", actualName, n.name, index, binding, ubo.getSize())
if (index == -1) {
logger.error("Failed to bind UBO $actualName for ${n.name} to $binding")
} else {
gl.glUniformBlockBinding(shader.id, index, binding)
gl.glBindBufferRange(GL4.GL_UNIFORM_BUFFER, binding,
ubo.backingBuffer!!.id[0], 1L * ubo.offset, 1L * ubo.getSize())
binding++
}
}
}
arrayOf("VRParameters", "LightParameters").forEach uboBinding@ { name ->
if (shader.uboSpecs.containsKey(name) && shader.isValid()) {
val buffer = buffers[name]
if(buffer == null) {
logger.warn("Buffer for $name not found")
return@uboBinding
}
val index = shader.getUniformBlockIndex(name)
if (index == -1) {
logger.error("Failed to bind shader parameter UBO $name for ${pass.passName} to $binding, though it is required by the shader")
} else {
gl.glUniformBlockBinding(shader.id, index, binding)
gl.glBindBufferRange(GL4.GL_UNIFORM_BUFFER, binding,
buffer.id[0],
0L, buffer.buffer.remaining().toLong())
binding++
}
}
}
if(n.instances.size > 0) {
drawNodeInstanced(n)
} else {
drawNode(n)
}
}
} else {
gl.glDisable(GL4.GL_CULL_FACE)
if (pass.output.any { it.value.hasDepthAttachment() }) {
gl.glEnable(GL4.GL_DEPTH_TEST)
} else {
gl.glDisable(GL4.GL_DEPTH_TEST)
}
if (pass.passConfig.renderTransparent) {
gl.glEnable(GL4.GL_BLEND)
gl.glBlendFuncSeparate(
pass.passConfig.srcColorBlendFactor.toOpenGL(),
pass.passConfig.dstColorBlendFactor.toOpenGL(),
pass.passConfig.srcAlphaBlendFactor.toOpenGL(),
pass.passConfig.dstAlphaBlendFactor.toOpenGL())
gl.glBlendEquationSeparate(
pass.passConfig.colorBlendOp.toOpenGL(),
pass.passConfig.alphaBlendOp.toOpenGL()
)
} else {
gl.glDisable(GL4.GL_BLEND)
}
pass.defaultShader?.let { shader ->
shader.use(gl)
var unit = 0
pass.inputs.keys.reversed().forEach { name ->
renderConfig.rendertargets[name.substringBefore(".")]?.attachments?.forEach {
shader.getUniform("Input" + it.key).setInt(unit)
unit++
}
}
var binding = 0
pass.UBOs.forEach { name, ubo ->
val actualName = if (name.contains("ShaderParameters")) {
"ShaderParameters"
} else {
name
}
val index = shader.getUniformBlockIndex(actualName)
gl.glUniformBlockBinding(shader.id, index, binding)
gl.glBindBufferRange(GL4.GL_UNIFORM_BUFFER, binding,
ubo.backingBuffer!!.id[0],
1L * ubo.offset, 1L * ubo.getSize())
if (index == -1) {
logger.error("Failed to bind shader parameter UBO $actualName for ${pass.passName} to $binding")
}
binding++
}
arrayOf("LightParameters", "VRParameters").forEach { name ->
if (shader.uboSpecs.containsKey(name)) {
val index = shader.getUniformBlockIndex(name)
gl.glUniformBlockBinding(shader.id, index, binding)
gl.glBindBufferRange(GL4.GL_UNIFORM_BUFFER, binding,
buffers[name]!!.id[0],
0L, buffers[name]!!.buffer.remaining().toLong())
if (index == -1) {
logger.error("Failed to bind shader parameter UBO $name for ${pass.passName} to $binding, though it is required by the shader")
}
binding++
}
}
renderFullscreenQuad(shader)
}
}
stats?.add("Renderer.$t.renderTiming", System.nanoTime() - startPass)
}
if(logger.isDebugEnabled || logger.isTraceEnabled) {
val error = gl.glGetError()
if (error != 0) {
throw Exception("OpenGL error: $error")
}
}
logger.trace("Running viewport pass")
val startPass = System.nanoTime()
val viewportPass = renderpasses[flow.last()]!!
gl.glBindFramebuffer(GL4.GL_DRAW_FRAMEBUFFER, 0)
blitFramebuffers(viewportPass.output.values.first(), null,
OpenGLRenderpass.Rect2D(
settings.get("Renderer.${viewportPass.passName}.displayWidth"),
settings.get("Renderer.${viewportPass.passName}.displayHeight"), 0, 0),
OpenGLRenderpass.Rect2D(window.width, window.height, 0, 0))
// submit to OpenVR if attached
if(hub?.getWorkingHMDDisplay()?.hasCompositor() == true && !mustRecreateFramebuffers) {
hub?.getWorkingHMDDisplay()?.wantsVR()?.submitToCompositor(
viewportPass.output.values.first().getTextureId("Viewport"))
}
if((embedIn != null && embedIn !is SceneryJPanel) || recordMovie) {
if (shouldClose || mustRecreateFramebuffers) {
encoder?.finish()
encoder = null
return@runBlocking
}
if (recordMovie && (encoder == null || encoder?.frameWidth != window.width || encoder?.frameHeight != window.height)) {
encoder = H264Encoder(window.width, window.height, System.getProperty("user.home") + File.separator + "Desktop" + File.separator + "$applicationName - ${SimpleDateFormat("yyyy-MM-dd_HH.mm.ss").format(Date())}.mp4")
}
readIndex = (readIndex + 1) % pboCount
updateIndex = (updateIndex + 1) % pboCount
if (pbos.any { it == 0 } || mustRecreateFramebuffers) {
gl.glGenBuffers(pboCount, pbos, 0)
pbos.forEachIndexed { index, pbo ->
gl.glBindBuffer(GL4.GL_PIXEL_PACK_BUFFER, pbos[index])
gl.glBufferData(GL4.GL_PIXEL_PACK_BUFFER, window.width * window.height * 4L, null, GL4.GL_STREAM_READ)
if(pboBuffers[index] != null) {
MemoryUtil.memFree(pboBuffers[index])
pboBuffers[index] = null
}
}
gl.glBindBuffer(GL4.GL_PIXEL_PACK_BUFFER, 0)
}
pboBuffers.forEachIndexed { i, _ ->
if(pboBuffers[i] == null) {
pboBuffers[i] = MemoryUtil.memAlloc(4 * window.width * window.height)
}
}
val startUpdate = System.nanoTime()
if(frames < pboCount) {
gl.glBindBuffer(GL4.GL_PIXEL_PACK_BUFFER, pbos[updateIndex])
gl.glReadPixels(0, 0, window.width, window.height, GL4.GL_BGRA, GL4.GL_UNSIGNED_BYTE, 0)
} else {
gl.glBindBuffer(GL4.GL_PIXEL_PACK_BUFFER, pbos[updateIndex])
val readBuffer = gl.glMapBuffer(GL4.GL_PIXEL_PACK_BUFFER, GL4.GL_READ_ONLY)
MemoryUtil.memCopy(readBuffer, pboBuffers[readIndex]!!)
gl.glUnmapBuffer(GL4.GL_PIXEL_PACK_BUFFER)
gl.glReadPixels(0, 0, window.width, window.height, GL4.GL_BGRA, GL4.GL_UNSIGNED_BYTE, 0)
}
if (!mustRecreateFramebuffers && frames > pboCount) {
embedIn?.let { embedPanel ->
pboBuffers[readIndex]?.let {
val id = viewportPass.output.values.first().getTextureId("Viewport")
embedPanel.update(it, id = id)
}
}
encoder?.let { e ->
pboBuffers[readIndex]?.let {
e.encodeFrame(it)
}
}
}
val updateDuration = (System.nanoTime() - startUpdate)*1.0f
stats?.add("Renderer.updateEmbed", updateDuration, true)
gl.glBindBuffer(GL4.GL_PIXEL_PACK_BUFFER, 0)
}
if (screenshotRequested && joglDrawable != null) {
try {
val readBufferUtil = AWTGLReadBufferUtil(joglDrawable!!.glProfile, false)
val image = readBufferUtil.readPixelsToBufferedImage(gl, true)
val file = SystemHelpers.addFileCounter(if(screenshotFilename == "") {
File(System.getProperty("user.home"), "Desktop" + File.separator + "$applicationName - ${SimpleDateFormat("yyyy-MM-dd HH.mm.ss").format(Date())}.png")
} else {
File(screenshotFilename)
}, screenshotOverwriteExisting)
ImageIO.write(image, "png", file)
logger.info("Screenshot saved to ${file.absolutePath}")
} catch (e: Exception) {
logger.error("Unable to take screenshot: ")
e.printStackTrace()
}
screenshotOverwriteExisting = false
screenshotRequested = false
}
stats?.add("Renderer.${flow.last()}.renderTiming", System.nanoTime() - startPass)
updateLatch?.countDown()
firstImageReady = true
frames++
framesPerSec++
}
/**
* Renders a fullscreen quad, using from an on-the-fly generated
* Node that is saved in [nodeStore], with the [GLProgram]'s ID.
*
* @param[program] The [GLProgram] to draw into the fullscreen quad.
*/
fun renderFullscreenQuad(program: OpenGLShaderProgram) {
val quad: Node
val quadName = "fullscreenQuad-${program.id}"
quad = nodeStore.getOrPut(quadName) {
val q = Plane(GLVector(1.0f, 1.0f, 0.0f))
q.metadata["OpenGLRenderer"] = OpenGLObjectState()
initializeNode(q)
q
}
drawNode(quad, count = 3)
program.gl.glBindTexture(GL4.GL_TEXTURE_2D, 0)
}
/**
* Initializes a given [Node].
*
* This function initializes a Node, equipping its metadata with an [OpenGLObjectState],
* generating VAOs and VBOs. If the Node has a [Material] assigned, a [GLProgram] fitting
* this Material will be used. Else, a default GLProgram will be used.
*
* For the assigned Material case, the GLProgram is derived either from the class name of the
* Node (if useClassDerivedShader is set), or from a set [ShaderMaterial] which may define
* the whole shader pipeline for the Node.
*
* If the [Node] implements [HasGeometry], it's geometry is also initialized by this function.
*
* @param[node]: The [Node] to initialise.
* @return True if the initialisation went alright, False if it failed.
*/
@Synchronized fun initializeNode(node: Node): Boolean {
if(!node.lock.tryLock(2, TimeUnit.MILLISECONDS)) {
return false
}
val s = node.metadata["OpenGLRenderer"] as OpenGLObjectState
if (s.initialized) {
return true
}
// generate VAO for attachment of VBO and indices
gl.glGenVertexArrays(1, s.mVertexArrayObject, 0)
// generate three VBOs for coords, normals, texcoords
gl.glGenBuffers(3, s.mVertexBuffers, 0)
gl.glGenBuffers(1, s.mIndexBuffer, 0)
when {
node.material is ShaderMaterial -> {
val shaders = (node.material as ShaderMaterial).shaders
try {
s.shader = prepareShaderProgram(shaders)
} catch (e: ShaderCompilationException) {
logger.warn("Shader compilation for node ${node.name} with shaders $shaders failed, falling back to default shaders.")
logger.warn("Shader compiler error was: ${e.message}")
}
}
else -> s.shader = null
}
if (node is HasGeometry) {
setVerticesAndCreateBufferForNode(node)
setNormalsAndCreateBufferForNode(node)
if (node.texcoords.limit() > 0) {
setTextureCoordsAndCreateBufferForNode(node)
}
if (node.indices.limit() > 0) {
setIndicesAndCreateBufferForNode(node)
}
}
s.materialHash = node.material.hashCode()
val matricesUbo = OpenGLUBO(backingBuffer = buffers["UBOBuffer"])
with(matricesUbo) {
name = "Matrices"
add("ModelMatrix", { node.world })
add("NormalMatrix", { node.world.inverse.transpose() })
add("isBillboard", { node.isBillboard.toInt() })
sceneUBOs.add(node)
s.UBOs.put("Matrices", this)
}
loadTexturesForNode(node, s)
val materialUbo = OpenGLUBO(backingBuffer = buffers["UBOBuffer"])
with(materialUbo) {
name = "MaterialProperties"
add("materialType", { node.materialToMaterialType() })
add("Ka", { node.material.ambient })
add("Kd", { node.material.diffuse })
add("Ks", { node.material.specular })
add("Roughness", { node.material.roughness })
add("Metallic", { node.material.metallic })
add("Opacity", { node.material.blending.opacity })
s.UBOs.put("MaterialProperties", this)
}
if (node.javaClass.kotlin.memberProperties.filter { it.findAnnotation() != null }.count() > 0) {
val shaderPropertyUBO = OpenGLUBO(backingBuffer = buffers["ShaderPropertyBuffer"])
with(shaderPropertyUBO) {
name = "ShaderProperties"
val shader = if (node.material is ShaderMaterial) {
s.shader
} else {
renderpasses.filter {
(it.value.passConfig.type == RenderConfigReader.RenderpassType.geometry || it.value.passConfig.type == RenderConfigReader.RenderpassType.lights)
&& it.value.passConfig.renderTransparent == node.material.blending.transparent
}.entries.firstOrNull()?.value?.defaultShader
}
logger.debug("Shader properties are: ${shader?.getShaderPropertyOrder()}")
shader?.getShaderPropertyOrder()?.forEach { name, offset ->
add(name, { node.getShaderProperty(name) ?: 0 }, offset)
}
}
s.UBOs["ShaderProperties"] = shaderPropertyUBO
}
s.initialized = true
node.initialized = true
node.metadata[className] = s
s.initialized = true
node.lock.unlock()
return true
}
private val defaultTextureNames = arrayOf("ambient", "diffuse", "specular", "normal", "alphamask", "displacement")
private fun Node.materialToMaterialType(): Int {
var materialType = 0
val s = this.metadata["OpenGLRenderer"] as? OpenGLObjectState ?: return 0
if (this.material.textures.containsKey("ambient") && !s.defaultTexturesFor.contains("ambient")) {
materialType = materialType or MATERIAL_HAS_AMBIENT
}
if (this.material.textures.containsKey("diffuse") && !s.defaultTexturesFor.contains("diffuse")) {
materialType = materialType or MATERIAL_HAS_DIFFUSE
}
if (this.material.textures.containsKey("specular") && !s.defaultTexturesFor.contains("specular")) {
materialType = materialType or MATERIAL_HAS_SPECULAR
}
if (this.material.textures.containsKey("normal") && !s.defaultTexturesFor.contains("normal")) {
materialType = materialType or MATERIAL_HAS_NORMAL
}
if (this.material.textures.containsKey("alphamask") && !s.defaultTexturesFor.contains("alphamask")) {
materialType = materialType or MATERIAL_HAS_ALPHAMASK
}
return materialType
}
/**
* Initializes a default set of textures that the renderer can fall back to and provide a non-intrusive
* hint to the user that a texture could not be loaded.
*/
private fun prepareDefaultTextures() {
val t = GLTexture.loadFromFile(gl, Renderer::class.java.getResourceAsStream("DefaultTexture.png"), "png", false, true, 8)
if(t == null) {
logger.error("Could not load default texture! This indicates a serious issue.")
} else {
textureCache["DefaultTexture"] = t
}
}
/**
* Returns true if the current [GLTexture] can be reused to store the information in the [GenericTexture]
* [other]. Returns false otherwise.
*/
protected fun GLTexture.canBeReused(other: GenericTexture, miplevels: Int): Boolean {
return this.width == other.dimensions.x().toInt() &&
this.height == other.dimensions.y().toInt() &&
this.depth == other.dimensions.z().toInt() &&
this.nativeType == other.type
}
private fun dumpTextureToFile(gl: GL4, name: String, texture: GLTexture) {
val filename = "${name}_${Date().toInstant().epochSecond}.raw"
val bytes = texture.width*texture.height*texture.depth*texture.channels*texture.bitsPerChannel/8
logger.info("Dumping $name to $filename ($bytes bytes)")
val buffer = MemoryUtil.memAlloc(bytes)
gl.glPixelStorei(GL4.GL_PACK_ALIGNMENT, 1)
texture.bind()
gl.glGetTexImage(texture.textureTarget, 0, texture.format, texture.type, buffer)
texture.unbind()
val stream = FileOutputStream(filename)
val outChannel = stream.channel
outChannel.write(buffer)
logger.info("Written $texture to $stream")
outChannel.close()
stream.close()
MemoryUtil.memFree(buffer)
}
/**
* Loads textures for a [Node]. The textures either come from a [Material.transferTextures] buffer,
* or from a file. This is indicated by stating fromBuffer:bufferName in the textures hash map.
*
* @param[node] The [Node] to load textures for.
* @param[s] The [Node]'s [OpenGLObjectState]
*/
@Suppress("USELESS_ELVIS")
private fun loadTexturesForNode(node: Node, s: OpenGLObjectState): OpenGLObjectState {
if (node.lock.tryLock()) {
node.material.textures.forEach {
type, texture ->
if (!textureCache.containsKey(texture) || node.material.needsTextureReload) {
logger.debug("Loading texture $texture for ${node.name}")
val generateMipmaps = GenericTexture.mipmappedObjectTextures.contains(type)
if (texture.startsWith("fromBuffer:")) {
val gt = node.material.transferTextures[texture.substringAfter("fromBuffer:")]
gt?.let { (_, dimensions, channels, type1, contents, repeatS, repeatT, repeatU, normalized, mipmap, minLinear, maxLinear, updates) ->
logger.debug("Dims of $texture: $dimensions, mipmaps=$generateMipmaps")
val mm = generateMipmaps or mipmap
val miplevels = if (mm && dimensions.z().toInt() == 1) {
1 + Math.floor(Math.log(Math.max(dimensions.x() * 1.0, dimensions.y() * 1.0)) / Math.log(2.0)).toInt()
} else {
1
}
val existingTexture = s.textures[type]
val t = if(existingTexture != null && existingTexture.canBeReused(gt, miplevels)) {
existingTexture
} else {
GLTexture(gl, type1, channels,
dimensions.x().toInt(),
dimensions.y().toInt(),
dimensions.z().toInt() ?: 1,
minLinear,
miplevels, 32, normalized, renderConfig.sRGB)
}
if (mm) {
t.updateMipMaps()
}
t.setClamp(!repeatS, !repeatT)
val unpackAlignment = intArrayOf(0)
gl.glGetIntegerv(GL4.GL_UNPACK_ALIGNMENT, unpackAlignment, 0)
// textures might have very uneven dimensions, so we adjust GL_UNPACK_ALIGNMENT here correspondingly
// in case the byte count of the texture is not divisible by it.
if (contents != null && !gt.hasConsumableUpdates()) {
if (contents.remaining() % unpackAlignment[0] == 0 && dimensions.x().toInt() % unpackAlignment[0] == 0) {
t.copyFrom(contents)
} else {
gl.glPixelStorei(GL4.GL_UNPACK_ALIGNMENT, 1)
t.copyFrom(contents)
}
gl.glPixelStorei(GL4.GL_UNPACK_ALIGNMENT, unpackAlignment[0])
}
if (gt.hasConsumableUpdates()) {
gl.glPixelStorei(GL4.GL_UNPACK_ALIGNMENT, 1)
updates.forEach { update ->
if (!update.consumed) {
t.copyFrom(update.contents,
update.extents.w, update.extents.h, update.extents.d,
update.extents.x, update.extents.y, update.extents.z, true)
update.consumed = true
}
}
gt.clearConsumedUpdates()
gl.glPixelStorei(GL4.GL_UNPACK_ALIGNMENT, unpackAlignment[0])
}
s.textures[type] = t
textureCache.put(texture, t)
}
} else {
val glTexture = if(texture.contains("jar!")) {
val f = texture.substringAfterLast("!")
val stream = node.javaClass.getResourceAsStream(f)
if(stream == null) {
logger.error("Texture not found for $node: $f (from JAR)")
textureCache["DefaultTexture"]!!
} else {
GLTexture.loadFromFile(gl, stream, texture.substringAfterLast("."), true, generateMipmaps, 8)
}
} else {
try {
GLTexture.loadFromFile(gl, texture, true, generateMipmaps, 8)
} catch(e: FileNotFoundException) {
logger.error("Texture not found for $node: $texture")
textureCache["DefaultTexture"]!!
}
}
s.textures[type] = glTexture
textureCache[texture] = glTexture
}
} else {
s.textures[type] = textureCache[texture]!!
}
}
// update default textures
// s.defaultTexturesFor = defaultTextureNames.mapNotNull { if(!s.textures.containsKey(it)) { it } else { null } }.toHashSet()
s.defaultTexturesFor.clear()
defaultTextureNames.forEach {
if (!s.textures.containsKey(it)) {
s.defaultTexturesFor.add(it)
}
}
node.material.needsTextureReload = false
s.initialized = true
node.lock.unlock()
return s
} else {
return s
}
}
/**
* Reshape the renderer's viewports
*
* This routine is called when a change in window size is detected, e.g. when resizing
* it manually or toggling fullscreen. This function updates the sizes of all used
* geometry buffers and will also create new buffers in case vr.Active is changed.
*
* This function also clears color and depth buffer bits.
*
* @param[newWidth] The resized window's width
* @param[newHeight] The resized window's height
*/
override fun reshape(newWidth: Int, newHeight: Int) {
if (!initialized) {
return
}
lastResizeTimer.cancel()
lastResizeTimer = Timer()
lastResizeTimer.schedule(object : TimerTask() {
override fun run() {
window.width = newWidth
window.height = newHeight
logger.debug("Resizing window to ${newWidth}x$newHeight")
mustRecreateFramebuffers = true
}
}, WINDOW_RESIZE_TIMEOUT)
}
/**
* Creates VAOs and VBO for a given [Node]'s vertices.
*
* @param[node] The [Node] to create the VAO/VBO for.
*/
fun setVerticesAndCreateBufferForNode(node: Node) {
val s = getOpenGLObjectStateFromNode(node)
val pVertexBuffer: FloatBuffer = (node as HasGeometry).vertices
s.mStoredPrimitiveCount = pVertexBuffer.remaining() / node.vertexSize
gl.glBindVertexArray(s.mVertexArrayObject[0])
gl.glBindBuffer(GL4.GL_ARRAY_BUFFER, s.mVertexBuffers[0])
gl.glEnableVertexAttribArray(0)
gl.glBufferData(GL4.GL_ARRAY_BUFFER,
(pVertexBuffer.remaining() * (java.lang.Float.SIZE / java.lang.Byte.SIZE)).toLong(),
pVertexBuffer,
if (s.isDynamic)
GL4.GL_DYNAMIC_DRAW
else
GL4.GL_DYNAMIC_DRAW)
gl.glVertexAttribPointer(0,
node.vertexSize,
GL4.GL_FLOAT,
false,
0,
0)
gl.glBindVertexArray(0)
gl.glBindBuffer(GL4.GL_ARRAY_BUFFER, 0)
}
/**
* Updates a [Node]'s vertices.
*
* @param[node] The [Node] to update the vertices for.
*/
fun updateVertices(node: Node) {
val s = getOpenGLObjectStateFromNode(node)
val pVertexBuffer: FloatBuffer = (node as HasGeometry).vertices
s.mStoredPrimitiveCount = pVertexBuffer.remaining() / node.vertexSize
gl.glBindVertexArray(s.mVertexArrayObject[0])
gl.glBindBuffer(GL4.GL_ARRAY_BUFFER, s.mVertexBuffers[0])
gl.glEnableVertexAttribArray(0)
gl.glBufferData(GL4.GL_ARRAY_BUFFER,
(pVertexBuffer.remaining() * (java.lang.Float.SIZE / java.lang.Byte.SIZE)).toLong(),
pVertexBuffer,
GL4.GL_DYNAMIC_DRAW)
gl.glVertexAttribPointer(0,
node.vertexSize,
GL4.GL_FLOAT,
false,
0,
0)
gl.glBindVertexArray(0)
gl.glBindBuffer(GL4.GL_ARRAY_BUFFER, 0)
}
/**
* Creates VAOs and VBO for a given [Node]'s normals.
*
* @param[node] The [Node] to create the normals VBO for.
*/
fun setNormalsAndCreateBufferForNode(node: Node) {
val s = getOpenGLObjectStateFromNode(node)
val pNormalBuffer: FloatBuffer = (node as HasGeometry).normals
gl.glBindVertexArray(s.mVertexArrayObject[0])
gl.glBindBuffer(GL4.GL_ARRAY_BUFFER, s.mVertexBuffers[1])
if (pNormalBuffer.limit() > 0) {
gl.glEnableVertexAttribArray(1)
gl.glBufferData(GL4.GL_ARRAY_BUFFER,
(pNormalBuffer.remaining() * (java.lang.Float.SIZE / java.lang.Byte.SIZE)).toLong(),
pNormalBuffer,
if (s.isDynamic)
GL4.GL_DYNAMIC_DRAW
else
GL4.GL_STATIC_DRAW)
gl.glVertexAttribPointer(1,
node.vertexSize,
GL4.GL_FLOAT,
false,
0,
0)
}
gl.glBindVertexArray(0)
gl.glBindBuffer(GL4.GL_ARRAY_BUFFER, 0)
}
/**
* Updates a given [Node]'s normals.
*
* @param[node] The [Node] whose normals need updating.
*/
fun updateNormals(node: Node) {
val s = getOpenGLObjectStateFromNode(node)
val pNormalBuffer: FloatBuffer = (node as HasGeometry).normals
gl.glBindVertexArray(s.mVertexArrayObject[0])
gl.glBindBuffer(GL4.GL_ARRAY_BUFFER, s.mVertexBuffers[1])
gl.glEnableVertexAttribArray(1)
gl.glBufferData(GL4.GL_ARRAY_BUFFER,
(pNormalBuffer.remaining() * (java.lang.Float.SIZE / java.lang.Byte.SIZE)).toLong(),
pNormalBuffer,
if (s.isDynamic)
GL4.GL_DYNAMIC_DRAW
else
GL4.GL_STATIC_DRAW)
gl.glVertexAttribPointer(1,
node.vertexSize,
GL4.GL_FLOAT,
false,
0,
0)
gl.glBindVertexArray(0)
gl.glBindBuffer(GL4.GL_ARRAY_BUFFER, 0)
}
/**
* Creates VAOs and VBO for a given [Node]'s texcoords.
*
* @param[node] The [Node] to create the texcoord VBO for.
*/
fun setTextureCoordsAndCreateBufferForNode(node: Node) {
val s = getOpenGLObjectStateFromNode(node)
val pTextureCoordsBuffer: FloatBuffer = (node as HasGeometry).texcoords
gl.glBindVertexArray(s.mVertexArrayObject[0])
gl.glBindBuffer(GL4.GL_ARRAY_BUFFER, s.mVertexBuffers[2])
gl.glEnableVertexAttribArray(2)
gl.glBufferData(GL4.GL_ARRAY_BUFFER,
(pTextureCoordsBuffer.remaining() * (java.lang.Float.SIZE / java.lang.Byte.SIZE)).toLong(),
pTextureCoordsBuffer,
if (s.isDynamic)
GL4.GL_DYNAMIC_DRAW
else
GL4.GL_DYNAMIC_DRAW)
gl.glVertexAttribPointer(2,
node.texcoordSize,
GL4.GL_FLOAT,
false,
0,
0)
gl.glBindVertexArray(0)
gl.glBindBuffer(GL4.GL_ARRAY_BUFFER, 0)
}
/**
* Updates a given [Node]'s texcoords.
*
* @param[node] The [Node] whose texcoords need updating.
*/
fun updateTextureCoords(node: Node) {
val s = getOpenGLObjectStateFromNode(node)
val pTextureCoordsBuffer: FloatBuffer = (node as HasGeometry).texcoords
gl.glBindVertexArray(s.mVertexArrayObject[0])
gl.glBindBuffer(GL4.GL_ARRAY_BUFFER,
s.mVertexBuffers[2])
gl.glEnableVertexAttribArray(2)
gl.glBufferData(GL4.GL_ARRAY_BUFFER,
(pTextureCoordsBuffer.remaining() * (java.lang.Float.SIZE / java.lang.Byte.SIZE)).toLong(),
pTextureCoordsBuffer,
if (s.isDynamic)
GL4.GL_DYNAMIC_DRAW
else
GL4.GL_STATIC_DRAW)
gl.glVertexAttribPointer(2,
node.texcoordSize,
GL4.GL_FLOAT,
false,
0,
0)
gl.glBindVertexArray(0)
gl.glBindBuffer(GL4.GL_ARRAY_BUFFER, 0)
}
/**
* Creates a index buffer for a given [Node]'s indices.
*
* @param[node] The [Node] to create the index buffer for.
*/
fun setIndicesAndCreateBufferForNode(node: Node) {
val s = getOpenGLObjectStateFromNode(node)
val pIndexBuffer: IntBuffer = (node as HasGeometry).indices
s.mStoredIndexCount = pIndexBuffer.remaining()
gl.glBindVertexArray(s.mVertexArrayObject[0])
gl.glBindBuffer(GL4.GL_ELEMENT_ARRAY_BUFFER, s.mIndexBuffer[0])
gl.glBufferData(GL4.GL_ELEMENT_ARRAY_BUFFER,
(pIndexBuffer.remaining() * (Integer.SIZE / java.lang.Byte.SIZE)).toLong(),
pIndexBuffer,
if (s.isDynamic)
GL4.GL_DYNAMIC_DRAW
else
GL4.GL_DYNAMIC_DRAW)
gl.glBindVertexArray(0)
gl.glBindBuffer(GL4.GL_ELEMENT_ARRAY_BUFFER, 0)
}
/**
* Updates a given [Node]'s indices.
*
* @param[node] The [Node] whose indices need updating.
*/
fun updateIndices(node: Node) {
val s = getOpenGLObjectStateFromNode(node)
val pIndexBuffer: IntBuffer = (node as HasGeometry).indices
s.mStoredIndexCount = pIndexBuffer.remaining()
gl.glBindVertexArray(s.mVertexArrayObject[0])
gl.glBindBuffer(GL4.GL_ELEMENT_ARRAY_BUFFER, s.mIndexBuffer[0])
gl.glBufferData(GL4.GL_ELEMENT_ARRAY_BUFFER,
(pIndexBuffer.remaining() * (Integer.SIZE / java.lang.Byte.SIZE)).toLong(),
pIndexBuffer,
if (s.isDynamic)
GL4.GL_DYNAMIC_DRAW
else
GL4.GL_DYNAMIC_DRAW)
gl.glBindVertexArray(0)
gl.glBindBuffer(GL4.GL_ELEMENT_ARRAY_BUFFER, 0)
}
/**
* Draws a given [Node], either in element or in index draw mode.
*
* @param[node] The node to be drawn.
* @param[offset] offset in the array or index buffer.
*/
fun drawNode(node: Node, offset: Int = 0, count: Int? = null) {
val s = getOpenGLObjectStateFromNode(node)
if (s.mStoredIndexCount == 0 && s.mStoredPrimitiveCount == 0) {
return
}
logger.trace("Drawing {} with {}, {} primitives, {} indices", node.name, s.shader?.modules?.entries?.joinToString(", "), s.mStoredPrimitiveCount, s.mStoredIndexCount)
gl.glBindVertexArray(s.mVertexArrayObject[0])
if (s.mStoredIndexCount > 0) {
gl.glBindBuffer(GL4.GL_ELEMENT_ARRAY_BUFFER,
s.mIndexBuffer[0])
gl.glDrawElements((node as HasGeometry).geometryType.toOpenGLType(),
count ?: s.mStoredIndexCount,
GL4.GL_UNSIGNED_INT,
offset.toLong())
gl.glBindBuffer(GL4.GL_ELEMENT_ARRAY_BUFFER, 0)
} else {
gl.glDrawArrays((node as HasGeometry).geometryType.toOpenGLType(), offset, count ?: s.mStoredPrimitiveCount)
}
// gl.glUseProgram(0)
// gl.glBindVertexArray(0)
}
/**
* Draws a given instanced [Node] either in element or in index draw mode.
*
* @param[node] The node to be drawn.
* @param[offset] offset in the array or index buffer.
*/
protected fun drawNodeInstanced(node: Node, offset: Long = 0) {
val s = getOpenGLObjectStateFromNode(node)
gl.glBindVertexArray(s.mVertexArrayObject[0])
if (s.mStoredIndexCount > 0) {
gl.glDrawElementsInstanced(
(node as HasGeometry).geometryType.toOpenGLType(),
s.mStoredIndexCount,
GL4.GL_UNSIGNED_INT,
offset,
s.instanceCount)
} else {
gl.glDrawArraysInstanced(
(node as HasGeometry).geometryType.toOpenGLType(),
0, s.mStoredPrimitiveCount, s.instanceCount)
}
// gl.glUseProgram(0)
// gl.glBindVertexArray(0)
}
override fun screenshot(filename: String, overwrite: Boolean) {
screenshotRequested = true
screenshotOverwriteExisting = overwrite
screenshotFilename = filename
}
@Suppress("unused")
fun recordMovie() {
if(recordMovie) {
encoder?.finish()
encoder = null
recordMovie = false
} else {
recordMovie = true
}
}
private fun Blending.BlendFactor.toOpenGL() = when (this) {
Blending.BlendFactor.Zero -> GL4.GL_ZERO
Blending.BlendFactor.One -> GL4.GL_ONE
Blending.BlendFactor.SrcAlpha -> GL4.GL_SRC_ALPHA
Blending.BlendFactor.OneMinusSrcAlpha -> GL4.GL_ONE_MINUS_SRC_ALPHA
Blending.BlendFactor.SrcColor -> GL4.GL_SRC_COLOR
Blending.BlendFactor.OneMinusSrcColor -> GL4.GL_ONE_MINUS_SRC_COLOR
Blending.BlendFactor.DstColor -> GL4.GL_DST_COLOR
Blending.BlendFactor.OneMinusDstColor -> GL4.GL_ONE_MINUS_DST_COLOR
Blending.BlendFactor.DstAlpha -> GL4.GL_DST_ALPHA
Blending.BlendFactor.OneMinusDstAlpha -> GL4.GL_ONE_MINUS_DST_ALPHA
Blending.BlendFactor.ConstantColor -> GL4.GL_CONSTANT_COLOR
Blending.BlendFactor.OneMinusConstantColor -> GL4.GL_ONE_MINUS_CONSTANT_COLOR
Blending.BlendFactor.ConstantAlpha -> GL4.GL_CONSTANT_ALPHA
Blending.BlendFactor.OneMinusConstantAlpha -> GL4.GL_ONE_MINUS_CONSTANT_ALPHA
Blending.BlendFactor.Src1Color -> GL4.GL_SRC1_COLOR
Blending.BlendFactor.OneMinusSrc1Color -> GL4.GL_ONE_MINUS_SRC1_COLOR
Blending.BlendFactor.Src1Alpha -> GL4.GL_SRC1_ALPHA
Blending.BlendFactor.OneMinusSrc1Alpha -> GL4.GL_ONE_MINUS_SRC1_ALPHA
Blending.BlendFactor.SrcAlphaSaturate -> GL4.GL_SRC_ALPHA_SATURATE
}
private fun Blending.BlendOp.toOpenGL() = when (this) {
Blending.BlendOp.add -> GL4.GL_FUNC_ADD
Blending.BlendOp.subtract -> GL4.GL_FUNC_SUBTRACT
Blending.BlendOp.min -> GL4.GL_MIN
Blending.BlendOp.max -> GL4.GL_MAX
Blending.BlendOp.reverse_subtract -> GL4.GL_FUNC_REVERSE_SUBTRACT
}
/**
* Sets the rendering quality, if the loaded renderer config file supports it.
*
* @param[quality] The [RenderConfigReader.RenderingQuality] to be set.
*/
override fun setRenderingQuality(quality: RenderConfigReader.RenderingQuality) {
fun setConfigSetting(key: String, value: Any) {
val setting = "Renderer.$key"
logger.debug("Setting $setting: ${settings.get(setting)} -> $value")
settings.set(setting, value)
}
if(renderConfig.qualitySettings.isNotEmpty()) {
logger.info("Setting rendering quality to $quality")
renderConfig.qualitySettings[quality]?.forEach { setting ->
if(setting.key.endsWith(".shaders") && setting.value is List<*>) {
val pass = setting.key.substringBeforeLast(".shaders")
val shaders = setting.value as? List ?: return@forEach
renderConfig.renderpasses[pass]?.shaders = shaders
mustRecreateFramebuffers = true
framebufferRecreateHook = {
renderConfig.qualitySettings[quality]?.filter { !it.key.endsWith(".shaders") }?.forEach {
setConfigSetting(it.key, it.value)
}
framebufferRecreateHook = {}
}
} else {
setConfigSetting(setting.key, setting.value)
}
}
} else {
logger.warn("The current renderer config, $renderConfigFile, does not support setting quality options.")
}
}
/**
* Closes this renderer instance.
*/
override fun close() {
shouldClose = true
encoder?.finish()
}
}