Skip to content

ImGuiStereoQuadExample.kt

src/jvmMain/kotlin/afield/examples/four/ImGuiStereoQuadExample.kt · 195 lines

package afield.examples.four

import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.imgui.ImGuiRenderToTexture
import afield.document.context.*
import afield.graphics.render.*
import afield.graphics.scene.*
import afield.graphics.shader.*
import afield.graphics.vulkan.targets.SideBySideStereoTarget
import afield.graphics.texture.TextureSampler
import afield.graphics.vulkan.util.Window
import afield.graphics.vulkan.util.updateForever
import afield.graphics.shader.fromViews
import imgui.ImGui
import afield.math.linalg.Matrix4f
import afield.math.linalg.Vector3f
import org.lwjgl.vulkan.VK10.VK_FORMAT_B8G8R8A8_SRGB
import kotlin.math.abs

class ImGuiStereoQuadExample

class ImGuiStereoQuadExample(
    val window: Window = fromContext(),
    val renderer: Renderer = fromContext(),
    val sbs: SideBySideStereoTarget = fromContext()
) {

var lastHitWorld: Vector3f? = null

fun go()

body — 134 lines, from line 28
    fun go() {

        val imgui = ImGuiRenderToTexture(
            renderer = renderer,
            window = window,
            parameters = {
                width = 1024*2
                height = 1024*2
                format = VK_FORMAT_B8G8R8A8_SRGB
                mipLevels = 4
            }, displayScale = 1f, fontSupersample = 1f
        )
        imgui.useWindowMouseEvents = false

        imgui.loadFontFromResource("IosevkaField-Light.ttf", sizePixels = 12f*4)
        imgui.addUi("demo") { gui { ImGui.showDemoWindow() } }

        // Render ImGui each frame.
        renderer.renderList.preTargets["imgui-rtt"] = imgui

        // Panel geometry sized to RTT aspect.
        val aspect = imgui.width.toFloat() / imgui.height.toFloat()
        val halfH = 0.5f
        val halfW = halfH * aspect
        val positions = floatArrayOf(
            -halfW, +halfH, 0f,
            -halfW, -halfH, 0f,
            +halfW, -halfH, 0f,
            +halfW, +halfH, 0f,
        )
        val uvs = floatArrayOf(
            0f, 0f,
            0f, 1f,
            1f, 1f,
            1f, 0f,
        )
        val indices = intArrayOf(0, 1, 2, 0, 2, 3)

        val model = Matrix4f().translate(0f, 0f, -1.5f)
        val sampler = TextureSampler(renderer, mipLevels = 4, anisotropyEnable = true)

        sbs += "imgui-panel" to {
            val module = Module(
                parameters(renderer) {
                    name = "imgui-stereo-panel"
                    color = 1
                    depth = 1
                    depthTest = true
                    // Match the underlying RTT formats (default B8G8R8A8_SRGB for SideBySideStereoTarget eyes).
                    colorFormats = listOf(VK_FORMAT_B8G8R8A8_SRGB)
                    vertex = panelVertexShader
                    fragment = panelFragmentShader
                    textureResolver = { tex ->
                        if (tex.name == "panelTex") fromViews(listOf(imgui), tex.name, sampler) else null
                    }
                }
            )

            val mp = module.pointer()
            val mesh = Mesh(renderer, mp)
            mesh.resize(4, 2)
            mp.current.meshes["panel"] = mesh

            mesh.v[0]!!.mapAndWrite {
                positions.forEach { put(it) }
                rewind()
            }
            mesh.v[1]!!.mapAndWrite {
                uvs.forEach { put(it) }
                rewind()
            }
            mesh.t!!.mapAndWrite_ints {
                indices.forEach { put(it) }
                rewind()
            }

            mesh.update["mvp"] = updateForever {
                val view = sbs.currentEyeViewMatrix(Matrix4f())
                val proj = sbs.currentEyeProjectionMatrix(Matrix4f())
                val mvp = proj.mul(view.mul(Matrix4f(model)))
                mesh.push["mvp"] = mvp

                // Ray hit testing from controller/mouse to ImGui panel.
                val ctrl = sbs.controllerState()
//                println(ctrl.position)
                if (ctrl.active && ctrl.poseValid) {
                    val origin = Vector3f(ctrl.position)
                    val dir = Vector3f(0f, 0f, 1f)
                    ctrl.rotation.transform(dir)

                    val inv = Matrix4f(model).invert()
                    val localOrigin = inv.transformPosition(Vector3f(origin))
                    val localDir = inv.transformDirection(Vector3f(dir))
                    val dz = localDir.z
                     if (abs(dz) > 1e-5f) {
                        val t = -localOrigin.z / dz

                         if (t > 0f) {
                            val hit = Vector3f(localOrigin).fma(t, localDir)
                            if (abs(hit.x) <= halfW && abs(hit.y) <= halfH) {
                                lastHitWorld = Matrix4f(model).transformPosition(hit, Vector3f())
                                val u = (hit.x / (halfW * 2f)) + 0.5f
                                val v = 0.5f - (hit.y / (halfH * 2f))
                                imgui.injectPointer(
                                    u * imgui.width,
                                    v * imgui.height,
                                    ctrl.selectPressed || ctrl.triggerPressed
                                )
                            } else {
                                lastHitWorld = Matrix4f(model).transformPosition(hit, Vector3f())
                                imgui.injectPointer(imgui.width.toFloat() * 2f, imgui.height.toFloat() * 2f, false)
                            }
                        } else {
                            lastHitWorld = null
                            imgui.injectPointer(imgui.width.toFloat() * 2f, imgui.height.toFloat() * 2f, false)
                        }
                    } else {
                        lastHitWorld = null
                        imgui.injectPointer(imgui.width.toFloat() * 2f, imgui.height.toFloat() * 2f, false)
                    }
                } else {
                    lastHitWorld = null
                    imgui.injectPointer(imgui.width.toFloat() * 2f, imgui.height.toFloat() * 2f, false)
                }

                true
            }

            mp
        }

        // Enable mouse-driven mock controller when not in XR.
        sbs.enableMouseController(distanceMeters = 1.5f)
    }

companion object

    companion object {
        private val panelVertexShader = """
            #version 460
            layout(location = 0) in vec3 inPosition;
            layout(location = 1) in vec2 inUV;
            layout(location = 0) out vec2 fragUV;

            layout(push_constant) uniform Push {
                mat4 mvp;
            } push;

            void main() {
                fragUV = inUV;
                gl_Position = push.mvp * vec4(inPosition, 1.0);
            }
        """.trimIndent()

        private val panelFragmentShader = """
            #version 460
            layout(location = 0) in vec2 fragUV;
            layout(location = 0) out vec4 outColor;

            layout(set = 0, binding = 0) uniform sampler2D panelTex;

            void main() {
                outColor = texture(panelTex, fragUV);
                outColor.x += 0.01;
                outColor.w += 0.01;
            }
        """.trimIndent()
    }
}