de.bwravencl.controllerbuddy.gui.OpenVrOverlay.java Source code

Java tutorial

Introduction

Here is the source code for de.bwravencl.controllerbuddy.gui.OpenVrOverlay.java

Source

/* Copyright (C) 2019  Matteo Hausner
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package de.bwravencl.controllerbuddy.gui;

import static org.lwjgl.opengl.GL11.GL_LINEAR;
import static org.lwjgl.opengl.GL11.GL_RGBA;
import static org.lwjgl.opengl.GL11.GL_RGBA8;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_MAG_FILTER;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_MIN_FILTER;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_BYTE;
import static org.lwjgl.opengl.GL11.glBindTexture;
import static org.lwjgl.opengl.GL11.glGenTextures;
import static org.lwjgl.opengl.GL11.glTexImage2D;
import static org.lwjgl.opengl.GL11.glTexParameteri;
import static org.lwjgl.opengl.WGL.wglCreateContext;
import static org.lwjgl.opengl.WGL.wglGetCurrentContext;
import static org.lwjgl.opengl.WGL.wglMakeCurrent;
import static org.lwjgl.openvr.VR.EColorSpace_ColorSpace_Auto;
import static org.lwjgl.openvr.VR.ETextureType_TextureType_OpenGL;
import static org.lwjgl.openvr.VR.ETrackingUniverseOrigin_TrackingUniverseSeated;
import static org.lwjgl.openvr.VR.EVRApplicationType_VRApplication_Background;
import static org.lwjgl.openvr.VR.EVREventType_VREvent_Quit;
import static org.lwjgl.openvr.VR.EVRInitError_VRInitError_None;
import static org.lwjgl.openvr.VR.EVROverlayError_VROverlayError_None;
import static org.lwjgl.openvr.VR.VR_GetVRInitErrorAsEnglishDescription;
import static org.lwjgl.openvr.VR.VR_InitInternal;
import static org.lwjgl.openvr.VR.VR_ShutdownInternal;
import static org.lwjgl.openvr.VROverlay.VROverlay_CreateOverlay;
import static org.lwjgl.openvr.VROverlay.VROverlay_GetOverlayErrorNameFromEnum;
import static org.lwjgl.openvr.VROverlay.VROverlay_HideOverlay;
import static org.lwjgl.openvr.VROverlay.VROverlay_IsOverlayVisible;
import static org.lwjgl.openvr.VROverlay.VROverlay_PollNextOverlayEvent;
import static org.lwjgl.openvr.VROverlay.VROverlay_SetOverlayTexture;
import static org.lwjgl.openvr.VROverlay.VROverlay_SetOverlayTextureBounds;
import static org.lwjgl.openvr.VROverlay.VROverlay_SetOverlayTransformAbsolute;
import static org.lwjgl.openvr.VROverlay.VROverlay_SetOverlayWidthInMeters;
import static org.lwjgl.openvr.VROverlay.VROverlay_ShowOverlay;
import static org.lwjgl.system.Checks.check;
import static org.lwjgl.system.MemoryStack.stackPush;
import static org.lwjgl.system.MemoryUtil.NULL;
import static org.lwjgl.system.MemoryUtil.memPutAddress;
import static org.lwjgl.system.windows.GDI32.ChoosePixelFormat;
import static org.lwjgl.system.windows.GDI32.DescribePixelFormat;
import static org.lwjgl.system.windows.GDI32.PFD_SUPPORT_OPENGL;
import static org.lwjgl.system.windows.GDI32.SetPixelFormat;
import static org.lwjgl.system.windows.User32.CS_HREDRAW;
import static org.lwjgl.system.windows.User32.CS_VREDRAW;
import static org.lwjgl.system.windows.User32.DestroyWindow;
import static org.lwjgl.system.windows.User32.GetDC;
import static org.lwjgl.system.windows.User32.RegisterClassEx;
import static org.lwjgl.system.windows.User32.WS_CLIPCHILDREN;
import static org.lwjgl.system.windows.User32.WS_CLIPSIBLINGS;
import static org.lwjgl.system.windows.User32.WS_OVERLAPPEDWINDOW;
import static org.lwjgl.system.windows.User32.nCreateWindowEx;
import static org.lwjgl.system.windows.User32.nUnregisterClass;
import static org.lwjgl.system.windows.WindowsUtil.windowsThrowException;

import java.awt.Frame;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.lang.System.Logger;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.swing.SwingUtilities;

import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GLCapabilities;
import org.lwjgl.openvr.HmdMatrix34;
import org.lwjgl.openvr.OpenVR;
import org.lwjgl.openvr.Texture;
import org.lwjgl.openvr.VREvent;
import org.lwjgl.openvr.VRTextureBounds;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.windows.PIXELFORMATDESCRIPTOR;
import org.lwjgl.system.windows.User32;
import org.lwjgl.system.windows.WNDCLASSEX;
import org.lwjgl.system.windows.WindowsLibrary;

class OpenVrOverlay {

    private static class TextureData {

        private BufferedImage image;
        private int textureObject;
        private Graphics2D g2d;

    }

    private static final Logger log = System.getLogger(OpenVrOverlay.class.getName());

    private static final String OVERLAY_KEY_PREFIX = OpenVrOverlay.class.getPackageName() + ".";
    private static final long OVERLAY_FPS = 25L;
    private static final float STATUS_OVERLAY_WIDTH = 0.08f;
    private static final float STATUS_OVERLAY_POSITION_X = 0.2f;
    private static final float STATUS_OVERLAY_POSITION_Y = -0.1f;
    private static final float STATUS_OVERLAY_POSITION_Z = -0.4f;
    private static final float ON_SCREEN_KEYBOARD_WIDTH = 0.4f;
    private static final float ON_SCREEN_KEYBOARD_OVERLAY_POSITION_X = 0f;
    private static final float ON_SCREEN_KEYBOARD_OVERLAY_POSITION_Y = -0.3f;
    private static final float ON_SCREEN_KEYBOARD_OVERLAY_POSITION_Z = -0.4f;

    private static ByteBuffer bufferedImageToByteBuffer(final BufferedImage image, final MemoryStack stack) {
        final var width = image.getWidth();
        final var height = image.getHeight();

        final var pixels = new int[width * height];
        image.getRGB(0, 0, width, height, pixels, 0, width);

        final var buffer = stack.malloc(width * height * 4);

        for (var y = 0; y < height; y++)
            for (var x = 0; x < width; x++) {
                final var pixel = pixels[y * width + x];
                buffer.put((byte) (pixel >> 16 & 0xFF));
                buffer.put((byte) (pixel >> 8 & 0xFF));
                buffer.put((byte) (pixel & 0xFF));
                buffer.put((byte) (pixel >> 24 & 0xFF));
            }

        return buffer.flip();
    }

    private static void createGLCapabilitiesIfRequired() {
        GLCapabilities capabilities = null;

        try {
            capabilities = GL.getCapabilities();
        } catch (final IllegalStateException e) {
        }

        if (capabilities == null)
            GL.createCapabilities();
    }

    private static HmdMatrix34 createIdentityHmdMatrix34(final MemoryStack stack) {
        final var mat = HmdMatrix34.callocStack(stack);

        mat.m(0, 1f);
        mat.m(5, 1f);
        mat.m(10, 1f);

        return mat;
    }

    private static void makeTransformFacing(final HmdMatrix34 mat) {
        rotate(mat, (float) Math.atan(mat.m(3) / mat.m(11)), 0f, 1f, 0f);
        rotate(mat, (float) -Math.atan(mat.m(7) / mat.m(11)), 1f, 0f, 0f);
    }

    private static void rotate(final HmdMatrix34 mat, final float angle, final float x, final float y,
            final float z) {
        final var c = (float) Math.cos(angle);
        final var s = (float) Math.sin(angle);

        final var dot = x * x + y * y + z * z;
        final var inv = 1f / (float) Math.sqrt(dot);

        final var aX = x * inv;
        final var aY = y * inv;
        final var aZ = z * inv;

        final var tempX = (1f - c) * aX;
        final var tempY = (1f - c) * aY;
        final var tempZ = (1f - c) * aZ;

        final var rotate00 = c + tempX * aX;
        final var rotate01 = tempX * aY + s * aZ;
        final var rotate02 = tempX * aZ - s * aY;

        final var rotate10 = tempY * aX - s * aZ;
        final var rotate11 = c + tempY * aY;
        final var rotate12 = tempY * aZ + s * aX;

        final var rotate20 = tempZ * aX + s * aY;
        final var rotate21 = tempZ * aY - s * aX;
        final var rotate22 = c + tempZ * aZ;

        final var res00 = mat.m(0) * rotate00 + mat.m(1) * rotate01 + mat.m(2) * rotate02;
        final var res01 = mat.m(4) * rotate00 + mat.m(5) * rotate01 + mat.m(6) * rotate02;
        final var res02 = mat.m(8) * rotate00 + mat.m(9) * rotate01 + mat.m(10) * rotate02;

        final var res10 = mat.m(0) * rotate10 + mat.m(1) * rotate11 + mat.m(2) * rotate12;
        final var res11 = mat.m(4) * rotate10 + mat.m(5) * rotate11 + mat.m(6) * rotate12;
        final var res12 = mat.m(8) * rotate10 + mat.m(9) * rotate11 + mat.m(10) * rotate12;

        final var res20 = mat.m(0) * rotate20 + mat.m(1) * rotate21 + mat.m(2) * rotate22;
        final var res21 = mat.m(4) * rotate20 + mat.m(5) * rotate21 + mat.m(6) * rotate22;
        final var res22 = mat.m(8) * rotate20 + mat.m(9) * rotate21 + mat.m(10) * rotate22;

        mat.m(0, res00);
        mat.m(4, res01);
        mat.m(8, res02);

        mat.m(1, res10);
        mat.m(5, res11);
        mat.m(9, res12);

        mat.m(2, res20);
        mat.m(6, res21);
        mat.m(10, res22);
    }

    private static void translate(final HmdMatrix34 mat, final float x, final float y, final float z) {
        mat.m(3, x);
        mat.m(7, y);
        mat.m(11, z);
    }

    private final Main main;
    private final OnScreenKeyboard onScreenKeyboard;
    private long statusOverlayHandle;
    private final long onScreenKeyboardOverlayHandle;
    private final Map<Long, TextureData> textureDataCache = new HashMap<>();
    private long hdc = NULL;
    private long hglrc = NULL;
    private short classAtom = 0;
    private long hwnd = NULL;
    private MemoryStack renderingMemoryStack;
    private final ScheduledExecutorService executorService;

    OpenVrOverlay(final Main main) throws Exception {
        this.main = main;
        onScreenKeyboard = main.getOnScreenKeyboard();

        try (var stack = stackPush()) {
            final var peError = stack.mallocInt(1);
            final var token = VR_InitInternal(peError, EVRApplicationType_VRApplication_Background);
            final var initError = peError.get();
            if (initError != EVRInitError_VRInitError_None)
                throw new Exception(getClass().getName() + ": " + VR_GetVRInitErrorAsEnglishDescription(initError));

            try {
                OpenVR.create(token);
                final var overlayTextureBounds = VRTextureBounds.mallocStack(stack);
                overlayTextureBounds.set(0f, 1f, 1f, 0f);

                final var overlayFrame = main.getOverlayFrame();
                if (overlayFrame != null) {
                    final var statusOverlayHandleBuffer = stack.mallocLong(1);
                    checkOverlayError(VROverlay_CreateOverlay(OVERLAY_KEY_PREFIX + overlayFrame.getTitle(),
                            overlayFrame.getTitle(), statusOverlayHandleBuffer));
                    statusOverlayHandle = statusOverlayHandleBuffer.get();

                    checkOverlayError(VROverlay_SetOverlayWidthInMeters(statusOverlayHandle, STATUS_OVERLAY_WIDTH));

                    final var statusOverlayTransform = createIdentityHmdMatrix34(stack);
                    translate(statusOverlayTransform, STATUS_OVERLAY_POSITION_X, STATUS_OVERLAY_POSITION_Y,
                            STATUS_OVERLAY_POSITION_Z);
                    makeTransformFacing(statusOverlayTransform);
                    checkOverlayError(VROverlay_SetOverlayTransformAbsolute(statusOverlayHandle,
                            ETrackingUniverseOrigin_TrackingUniverseSeated, statusOverlayTransform));

                    checkOverlayError(VROverlay_SetOverlayTextureBounds(statusOverlayHandle, overlayTextureBounds));
                }

                final var onScreenKeyboardOverlayHandleBuffer = stack.mallocLong(1);
                checkOverlayError(VROverlay_CreateOverlay(OVERLAY_KEY_PREFIX + onScreenKeyboard.getTitle(),
                        onScreenKeyboard.getTitle(), onScreenKeyboardOverlayHandleBuffer));
                onScreenKeyboardOverlayHandle = onScreenKeyboardOverlayHandleBuffer.get();

                checkOverlayError(
                        VROverlay_SetOverlayWidthInMeters(onScreenKeyboardOverlayHandle, ON_SCREEN_KEYBOARD_WIDTH));

                final var onScreenKeyboardOverlayTransform = createIdentityHmdMatrix34(stack);
                translate(onScreenKeyboardOverlayTransform, ON_SCREEN_KEYBOARD_OVERLAY_POSITION_X,
                        ON_SCREEN_KEYBOARD_OVERLAY_POSITION_Y, ON_SCREEN_KEYBOARD_OVERLAY_POSITION_Z);
                makeTransformFacing(onScreenKeyboardOverlayTransform);
                checkOverlayError(VROverlay_SetOverlayTransformAbsolute(onScreenKeyboardOverlayHandle,
                        ETrackingUniverseOrigin_TrackingUniverseSeated, onScreenKeyboardOverlayTransform));

                checkOverlayError(
                        VROverlay_SetOverlayTextureBounds(onScreenKeyboardOverlayHandle, overlayTextureBounds));

                final var wc = WNDCLASSEX.callocStack(stack).cbSize(WNDCLASSEX.SIZEOF)
                        .style(CS_HREDRAW | CS_VREDRAW).hInstance(WindowsLibrary.HINSTANCE)
                        .lpszClassName(stack.UTF16("WGL"));

                memPutAddress(wc.address() + WNDCLASSEX.LPFNWNDPROC, User32.Functions.DefWindowProc);

                classAtom = RegisterClassEx(wc);
                if (classAtom == 0)
                    throw new IllegalStateException(getClass().getName() + ": failed to register WGL window class");

                hwnd = check(nCreateWindowEx(0, classAtom & 0xFFFF, NULL,
                        WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, 0, 0, 1, 1, NULL, NULL, NULL,
                        NULL));

                hdc = check(GetDC(hwnd));

                final var pfd = PIXELFORMATDESCRIPTOR.callocStack(stack).nSize((short) PIXELFORMATDESCRIPTOR.SIZEOF)
                        .nVersion((short) 1).dwFlags(PFD_SUPPORT_OPENGL);
                final var pixelFormat = ChoosePixelFormat(hdc, pfd);
                if (pixelFormat == 0)
                    windowsThrowException(
                            getClass().getName() + ": failed to choose an OpenGL-compatible pixel format");

                if (DescribePixelFormat(hdc, pixelFormat, pfd) == 0)
                    windowsThrowException(getClass().getName() + ": failed to obtain pixel format information");

                if (!SetPixelFormat(hdc, pixelFormat, pfd))
                    windowsThrowException(getClass().getName() + ": failed to set the pixel format");

                hglrc = check(wglCreateContext(hdc));

                renderingMemoryStack = MemoryStack.create(2048000);

                executorService = Executors.newSingleThreadScheduledExecutor();
                executorService.scheduleAtFixedRate(this::render, 0L, 1000L / OVERLAY_FPS, TimeUnit.MILLISECONDS);
            } catch (final Throwable t) {
                log.log(Logger.Level.ERROR, t.getMessage(), t);
                deInit();
                throw t;
            }
        }
    }

    private void checkOverlayError(final int overlayError) throws Exception {
        if (overlayError != EVROverlayError_VROverlayError_None)
            throw new Exception(getClass().getName() + ": " + VROverlay_GetOverlayErrorNameFromEnum(overlayError));
    }

    private void deInit() {
        VR_ShutdownInternal();

        if (hwnd != NULL)
            DestroyWindow(hwnd);

        if (classAtom != 0)
            nUnregisterClass(classAtom & 0xFFFF, WindowsLibrary.HINSTANCE);
    }

    private void render() {
        renderingMemoryStack.push();
        try {
            final var vrEvent = VREvent.mallocStack(renderingMemoryStack);
            while (VROverlay_PollNextOverlayEvent(onScreenKeyboardOverlayHandle, vrEvent))
                if (vrEvent.eventType() == EVREventType_VREvent_Quit)
                    SwingUtilities.invokeLater(() -> stop());

            final var overlayFrame = main.getOverlayFrame();
            if (overlayFrame != null)
                updateOverlay(statusOverlayHandle, overlayFrame);

            updateOverlay(onScreenKeyboardOverlayHandle, onScreenKeyboard);
        } catch (final Throwable t) {
            log.log(Logger.Level.ERROR, t.getMessage(), t);
        } finally {
            if (wglGetCurrentContext() == hglrc)
                wglMakeCurrent(NULL, NULL);

            renderingMemoryStack.pop();
        }
    }

    void stop() {
        executorService.shutdown();
        try {
            if (executorService.awaitTermination(2L, TimeUnit.SECONDS))
                deInit();
        } catch (final InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    private void updateOverlay(final long overlayHandle, final Frame frame) throws Exception {
        renderingMemoryStack.push();
        try {
            if (frame.isVisible()) {
                checkOverlayError(VROverlay_ShowOverlay(overlayHandle));

                if (VROverlay_IsOverlayVisible(overlayHandle)) {
                    var textureData = textureDataCache.get(overlayHandle);
                    if (textureData == null) {
                        textureData = new TextureData();
                        textureDataCache.put(overlayHandle, textureData);
                    }

                    final var imageResized = textureData.image == null
                            || textureData.image.getWidth() != frame.getWidth()
                            || textureData.image.getHeight() != frame.getHeight();

                    if (imageResized) {
                        textureData.image = new BufferedImage(frame.getWidth(), frame.getHeight(),
                                BufferedImage.TYPE_INT_ARGB_PRE);
                        textureData.g2d = textureData.image.createGraphics();
                    }

                    frame.paint(textureData.g2d);
                    final var byteBuffer = bufferedImageToByteBuffer(textureData.image, renderingMemoryStack);

                    if (wglGetCurrentContext() != hglrc) {
                        if (!wglMakeCurrent(hdc, hglrc))
                            throw new Exception(getClass().getName() + ": could not acquire OpenGL context");
                        createGLCapabilitiesIfRequired();
                    }

                    if (imageResized)
                        textureData.textureObject = glGenTextures();

                    glBindTexture(GL_TEXTURE_2D, textureData.textureObject);
                    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, textureData.image.getWidth(),
                            textureData.image.getHeight(), 0, GL_RGBA, GL_UNSIGNED_BYTE, byteBuffer);

                    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
                    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

                    final var texture = Texture.mallocStack(renderingMemoryStack);
                    texture.set(textureData.textureObject, ETextureType_TextureType_OpenGL,
                            EColorSpace_ColorSpace_Auto);
                    checkOverlayError(VROverlay_SetOverlayTexture(overlayHandle, texture));
                }
            } else
                checkOverlayError(VROverlay_HideOverlay(overlayHandle));
        } finally {
            renderingMemoryStack.pop();
        }
    }

}