MinVR Tutorial for Mac

ABOUT

MinVR is an Open Source Project developed and maintained collaboratively by the University of Minnesota, Macalester College, and Brown University.

The goal of MinVR is to facilitate a variety of data visualization and virtual reality research projects by providing a robust, cross-platform VR toolkit for use with many different VR displays (e.g., CAVE's, PowerWalls, multi-touch stereoscopic tables, 3DTV's, head-mounted displays) and input devices (e.g., 6 degree-of-freedom trackers, multi-touch input devices, haptic devices, home-built devices).

More information at: https://github.com/MinVR/MinVR

Written by Jacob Leiken and Ruiqi Mao

SETUP

Estimated Time: 5 minutes

If you do not have Homebrew installed get it from https://brew.sh/.  (Note that Macports will also work on mac to install dependencies https://www.macports.org/, use "port install" instead of brew)

Using Homebrew, ensure that you have git, make, and cmake installed by running

brew install git

brew install make

brew install cmake

brew install freeglut

When you install freeglut it may ask you to install XQuartz. If so, run:

brew cask install xquartz

and then install freeglut again.

MINVR

Estimated Time: 5 minutes

OpenVR

If you do not intend on running your application on an OpenVR device (HTC Vive or Oculus Rift), then you can skip this section.

1.   Clone the latest version of OpenVR from https://github.com/ValveSoftware/openvr.

2.   From inside the OpenVR directory, install the framework by running

sudo cp -r bin/osx64/OpenVR.framework /Library/Frameworks

MinVR Stencil

As there are currently a number of issues with MinVR that cause it to fail to build on macOS, we won't have you build it yourself, but rather use a stencil we've produced that uses a version of MinVR with all of the issues fixed.

Clone the stencil from https://github.com/jleiken/MinVR-Tutorial by running:

git clone https://github.com/jleiken/MinVR-Tutorial

HELLO WORLD

Estimated Time: 1 hour

Everything will be done from the stencil provided. You will be filling in files in the src and res folders.

Writing the Code

Note: This tutorial glosses over the details of the OpenGL calls in the code. This is because OpenGL itself would take an entire tutorial to explain. Further details on what is going on can be figured out by researching online or by taking CS123.

1.   Let's make our main function. Open src/main.cpp.

2.   Put an empty main function in the file for now:

int main(int argc, char *argv[]) {

  return 0;

}

3.   Let's make sure everything is able to be built. Run make in the top level folder, then ./test. The program should exit, looking like it did nothing.

4.   There are some includes that we want to share across all of our files. To make things a little easier, we're going to edit the header file src/common/common.h.

5.   Put in the following:

#ifndef COMMON_H

#define COMMON_H


// MinVR.

#include <minvr/api/MinVR.h>


// GLM.

#define GLM_ENABLE_EXPERIMENTAL

#include <glm/glm.hpp>

#include <glm/gtx/transform.hpp>

#include <glm/gtc/type_ptr.hpp>


// GLEW.

#define GLEW_STATIC

#include "GL/glew.h"

#ifdef _WIN32

#include "GL/wglew.h"

#elif (!defined(__APPLE__))

#include "GL/glxew.h"

#endif


// OpenGL.

#if defined(WIN32)

#define NOMINMAX

#include <windows.h>

#include <GL/gl.h>

#elif defined(__APPLE__)

#define GL_GLEXT_PROTOTYPES

#include <OpenGL/gl3.h>

#include <OpenGL/glext.h>

#else

#define GL_GLEXT_PROTOTYPES

#include <GL/gl.h>

#endif


#include <iostream>

#include <sdsf>

#include <vector>


#endif

6.   Now we can edit our application class. 

7.   Fill out src/app/app.h:

#ifndef MYVRAPP_H

#define MYVRAPP_H


#include "../common/common.h"

#include "../util/util.h"


class MyVRApp : public MinVR::VRApp

{


public:

    MyVRApp(int argc, char *argv[]);


    virtual ~MyVRApp();


    /**

     * Called whenever an new input event happens.

     */

    void onVREvent(const MinVR::VREvent &event);


    /**

     * Called before renders to allow the user to set context-specific variables.

     */

    void onVRRenderGraphicsContext(const MinVR::VRGraphicsState &renderState);


    /**

     * Called when the application draws.

     */

    void onVRRenderGraphics(const MinVR::VRGraphicsState &renderState);


private:


    glm::mat4 m_model;


    GLuint m_vbo;

    GLuint m_vao;


    GLuint m_vs;

    GLuint m_fs;

    GLuint m_shader;


};


#endif

8.   Now we can work on src/app/app.cpp. Start by setting the constructor and destructor:

#include "app.h"


MyVRApp::MyVRApp(int argc, char *argv[])

  : m_model(1.0f),

    m_vbo(0),

    m_vao(0),

    m_vs(0),

    m_fs(0),

    m_shader(0),

    VRApp(argc, argv)

{ }


MyVRApp::~MyVRApp() {

    glDeleteBuffers(1, &m_vbo);

    glDeleteVertexArrays(1, &m_vao);

    glDetachShader(m_shader, m_vs);

    glDetachShader(m_shader, m_fs);

    glDeleteShader(m_vs);

    glDeleteShader(m_fs);

    glDeleteProgram(m_shader);

}

9.   MinVR uses an event system to handle interactions and application ticks. In our case, we only care about two events: when a tick, or a frame, starts, and when the Escape key is pressed so that we can exit the application. In this code, we're going to be rotating a cube. To determine how much to rotate the cute by every second, we use the amount of time elapsed since the application started to calculate a rotation matrix that we will apply to the cube. Add the event handler to src/app/app.cpp:

void MyVRApp::onVREvent(const MinVR::VREvent &event) {

    // Called on every tick.

    if (event.getName() == "FrameStart") {

        // Get the total amount of time elapsed.

        float time = event.getDataAsFloat("ElapsedSeconds");


        // Rotate the cube.

        m_model = glm::mat4(1.0f);

        m_model *= glm::translate(glm::vec3(0.0f, 0.0f, -5.0f));

        m_model *= glm::rotate(time, glm::vec3(0.0f, 1.0f, 0.0f));

    }


    // Press escape to quit.

    if (event.getName() == "KbdEsc_Down") {

        shutdown();

    }

}

10.   Next is our code for setting the graphics context variables. This is essentially code for configuring flags in OpenGL before a draw call so that OpenGL knows how to draw. In our case, we only care about the first time this is called so that we can generate the geometry for a cube and send it to the GPU for drawing. Add the function to src/app/app.cpp:

void MyVRApp::onVRRenderGraphicsContext(const MinVR::VRGraphicsState &renderState) {

    // Run setup if this is the initial call.

    if (renderState.isInitialRenderCall()) {

        // Initialize GLEW.

        glewExperimental = GL_TRUE;

        if (glewInit() != GLEW_OK) {

            std::cout << "Error initializing GLEW." << std::endl;

        }


        // Initialize OpenGL.

        glEnable(GL_DEPTH_TEST);

        glClearDepth(1.0f);

        glDepthFunc(GL_LEQUAL);

        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);


        // Create cube vertices.

        GLfloat vertices[] = {

            1.0f, 1.0f, 1.0f,  -1.0f, 1.0f, 1.0f,  -1.0f,-1.0f, 1.0f,      // v0-v1-v2 (front)

            -1.0f,-1.0f, 1.0f,   1.0f,-1.0f, 1.0f,   1.0f, 1.0f, 1.0f,     // v2-v3-v0


            1.0f, 1.0f, 1.0f,   1.0f,-1.0f, 1.0f,   1.0f,-1.0f,-1.0f,      // v0-v3-v4 (right)

            1.0f,-1.0f,-1.0f,   1.0f, 1.0f,-1.0f,   1.0f, 1.0f, 1.0f,      // v4-v5-v0


            1.0f, 1.0f, 1.0f,   1.0f, 1.0f,-1.0f,  -1.0f, 1.0f,-1.0f,      // v0-v5-v6 (top)

            -1.0f, 1.0f,-1.0f,  -1.0f, 1.0f, 1.0f,   1.0f, 1.0f, 1.0f,     // v6-v1-v0


            -1.0f, 1.0f, 1.0f,  -1.0f, 1.0f,-1.0f,  -1.0f,-1.0f,-1.0f,     // v1-v6-v7 (left)

            -1.0f,-1.0f,-1.0f,  -1.0f,-1.0f, 1.0f,  -1.0f, 1.0f, 1.0f,     // v7-v2-v1.0


            -1.0f,-1.0f,-1.0f,   1.0f,-1.0f,-1.0f,   1.0f,-1.0f, 1.0f,     // v7-v4-v3 (bottom)

            1.0f,-1.0f, 1.0f,  -1.0f,-1.0f, 1.0f,  -1.0f,-1.0f,-1.0f,      // v3-v2-v7


            1.0f,-1.0f,-1.0f,  -1.0f,-1.0f,-1.0f,  -1.0f, 1.0f,-1.0f,      // v4-v7-v6 (back)

            -1.0f, 1.0f,-1.0f,   1.0f, 1.0f,-1.0f,   1.0f,-1.0f,-1.0f      // v6-v5-v4

        };


        // Cube normals.

        GLfloat normals[] = {

            0.0f, 0.0f, 1.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f, 1.0f,      // v0-v1-v2 (front)

            0.0f, 0.0f, 1.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f, 1.0f,      // v2-v3-v0


            1.0f, 0.0f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 0.0f, 0.0f,      // v0-v3-v4 (right)

            1.0f, 0.0f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 0.0f, 0.0f,      // v4-v5-v0


            0.0f, 1.0f, 0.0f,   0.0f, 1.0f, 0.0f,   0.0f, 1.0f, 0.0f,      // v0-v5-v6 (top)

            0.0f, 1.0f, 0.0f,   0.0f, 1.0f, 0.0f,   0.0f, 1.0f, 0.0f,      // v6-v1-v0


            -1.0f, 0.0f, 0.0f,  -1.0f, 0.0f, 0.0f,  -1.0f, 0.0f, 0.0f,      // v1-v6-v7 (left)

            -1.0f, 0.0f, 0.0f,  -1.0f, 0.0f, 0.0f,  -1.0f, 0.0f, 0.0f,      // v7-v2-v1


            0.0f,-1.0f, 0.0f,   0.0f,-1.0f, 0.0f,   0.0f,-1.0f, 0.0f,      // v7-v4-v3 (bottom)

            0.0f,-1.0f, 0.0f,   0.0f,-1.0f, 0.0f,   0.0f,-1.0f, 0.0f,      // v3-v2-v7


            0.0f, 0.0f,-1.0f,   0.0f, 0.0f,-1.0f,   0.0f, 0.0f,-1.0f,      // v4-v7-v6 (back)

            0.0f, 0.0f,-1.0f,   0.0f, 0.0f,-1.0f,   0.0f, 0.0f,-1.0f       // v6-v5-v4

        };


        // Create the VBO.

        glGenBuffers(1, &m_vbo);

        glBindBuffer(GL_ARRAY_BUFFER, m_vbo);

        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices) + sizeof(normals), 0, GL_STATIC_DRAW);

        glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);

        glBufferSubData(GL_ARRAY_BUFFER, sizeof(vertices), sizeof(normals), normals);

        glBindBuffer(GL_ARRAY_BUFFER, 0);


        // Create the VAO.

        glGenVertexArrays(1, &m_vao);

        glBindVertexArray(m_vao);

        glBindBuffer(GL_ARRAY_BUFFER, m_vbo);

        glEnableVertexAttribArray(0);

        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (char*)0);

        glEnableVertexAttribArray(1);

        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (char*) sizeof(vertices));

        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glBindVertexArray(0);


        // Create the shader.

        m_vs = Util::compileShader(Util::load("res/shaders/shader.vert"), GL_VERTEX_SHADER);

        m_fs = Util::compileShader(Util::load("res/shaders/shader.frag"), GL_FRAGMENT_SHADER);

        m_shader = Util::linkShaderProgram(m_vs, m_fs);

    }

}

11. Your editor may start complaining about missing functions at this point. This is because we're missing three helper functions. Add this to src/util/util.h:

#ifndef UTIL_H

#define UTIL_H


#include "common/common.h"


namespace Util {

  /**

  * Load a file from a path.

  */

  std::string load(std::string path);

  /**

  * Compile a shader.

  */

  GLuint compileShader(std::string code, GLuint type);

  /**

  * Link a shader.

  */

  GLuint linkShaderProgram(GLuint vs, GLuint fs);

}


#endif

12. Now we need to actually implement those functions.  Add these to src/util/util.cpp:

#include <fstream>

#include <sstream>


#include "util.h"


std::string Util::load(std::string path) {

    // Load the file.

    std::ifstream file(path);


    // Load the contents of the file into a string stream.

    std::stringstream buffer;

    buffer << file.rdbuf();


    // Return the contents of the file.

    return buffer.str();

}


GLuint Util::compileShader(std::string code, GLuint type) {

    // Convert the code into a C style string.

    const char *source = code.c_str();

    int length = code.size();


    // Create and compile a shader.

    GLuint shader = glCreateShader(type);

    glShaderSource(shader, 1, &source, &length);

    glCompileShader(shader);


    // Verify that the shader compiled correctly.

    GLint status;

    glGetShaderiv(shader, GL_COMPILE_STATUS, &status);

    if (!status) {

        // Get the shader log.

        GLint length;

        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);

        std::vector<char> log(length);

        glGetShaderInfoLog(shader, length, &length, log.data());


        // Print the error.

        std::cerr << log.data() << std::endl;

    }


    // Return the shader.

    return shader;

}


GLuint Util::linkShaderProgram(GLuint vs, GLuint fs) {

    // Create a new shader program.

    GLuint program = glCreateProgram();


    // Attach the shaders.

    glAttachShader(program, vs);

    glAttachShader(program, fs);


    // Link the shaders.

    glLinkProgram(program);


    // Verify that the program linked correctly.

    GLint status;

    glGetProgramiv(program, GL_LINK_STATUS, &status);

    if (!status) {

        // Get the program log.

        GLint length;

        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);

        std::vector<char> log(length);

        glGetProgramInfoLog(program, length, &length, log.data());


        // Print the error.

        std::cerr << log.data() << std::endl;;

    }


    // Return the shader program.

    return program;

}

13.   Finally, let's have our application actually render something! Fill in the last function in src/app/app.c:

void MyVRApp::onVRRenderGraphics(const MinVR::VRGraphicsState &renderState) {

    // Only render if running.

    if (isRunning()) {

        // Clear the screen.

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


        // Use the shader.

        glUseProgram(m_shader);


        // Set the shader uniforms.

        glUniformMatrix4fv(glGetUniformLocation(m_shader, "p"), 1, GL_FALSE, renderState.getProjectionMatrix());

        glUniformMatrix4fv(glGetUniformLocation(m_shader, "v"), 1, GL_FALSE, renderState.getViewMatrix());

        glUniformMatrix4fv(glGetUniformLocation(m_shader, "m"), 1, GL_FALSE, glm::value_ptr(m_model));


        // Draw the cube.

        glBindVertexArray(m_vao);

        glDrawArrays(GL_TRIANGLES, 0, 36);

        glBindVertexArray(0);


        // Reset the shader.

        glUseProgram(0);

    }

}

14.   Let's get our main function to run the application. Go back to src/main.cpp and include app.h:

#include "app/app.h"

15.   Add code to run the application inside the body of main:

MyVRApp app(argc, argv);

app.run();

We're almost done here! At this point, we've told OpenGL what to draw, but we haven't told it how to draw it. If you've been paying attention to the code, you'll notice that we refer to two files: shader.vert and shader.frag. These two files are our vertex and fragment shaders, which are GPU code that will be used to process the data we pass to the GPU and render to the screen.

16.   Open res/shaders/shader.vert and put the following shader code in:

#version 410


layout(location = 0) in vec3 position;

layout(location = 1) in vec3 normal;


uniform mat4 p;

uniform mat4 v;

uniform mat4 m;


out vec4 pos;

out vec4 norm;


void main() {

    pos = v * m * vec4(position, 1.0);

    norm = normalize(v * m * vec4(normal, 0.0));


    gl_Position = p * pos;

}

17.   Likewise,  open res/shaders/shader.frag and put the following shader code in:

#version 410


const vec4 lightPos = vec4(0.0, 2.0, 2.0, 1.0);

const vec4 color = vec4(1.0, 0.0, 0.0, 1.0);


in vec4 pos;

in vec4 norm;


out vec4 fragColor;


void main() {

    float ambient = 0.1;

    float diffuse = clamp(dot(norm, normalize(lightPos - pos)), 0.0, 1.0);


    fragColor = (ambient + diffuse) * color;

}

Running the Application

1.   Try to run the application. You'll notice that the application will immediately crash. This is because MinVR requires configuration files that dictate how to display the application, which is the secret behind its versatility. In the top level, create a "config" folder, and in it, create a new file named "stereo.minvr". We are going to create a configuration file that tells MinVR to display two separate views: one for each eye.

<MinVR>

    <GLFWPlugin pluginType="MinVR_GLFW"/>

    <OpenGLPlugin pluginType="MinVR_OpenGL"/>


    <RGBBits>8</RGBBits>

    <AlphaBits>8</AlphaBits>

    <DepthBits>24</DepthBits>

    <StencilBits>8</StencilBits>

    <FullScreen>0</FullScreen>

    <Resizable>1</Resizable>

    <AllowMaximize>1</AllowMaximize>

    <Visible>1</Visible>

    <SharedContextGroupID>-1</SharedContextGroupID>

    <ContextVersionMajor>3</ContextVersionMajor>

    <ContextVersionMinor>3</ContextVersionMinor>

    <UseGPUAffinity>1</UseGPUAffinity>

    <UseDebugContext>0</UseDebugContext>

    <MSAASamples>1</MSAASamples>

    <QuadBuffered>0</QuadBuffered>

    <StereoFormat>SideBySide</StereoFormat>


    <HeadTrackingEvent>Head_Move</HeadTrackingEvent>

    <NearClip>0.001</NearClip>

    <FarClip>500.0</FarClip>


    <VRSetups>

        <Desktop hostType="VRStandAlone">

            <GLFWToolkit windowtoolkitType="VRGLFWWindowToolkit"/>

            <OpenGLToolkit graphicstoolkitType="VROpenGLGraphicsToolkit"/>

            <RootNode displaynodeType="VRGraphicsWindowNode">

                <Border>1</Border>

                <Caption>Desktop</Caption>

                <GPUAffinity>0</GPUAffinity>

                <XPos>100</XPos>

                <YPos>100</YPos>

                <Width>1280</Width>

                <Height>640</Height>

                <LookAtNode displaynodeType="VRTrackedLookAtNode">

                    <LookAtUp type="floatarray">0,1,0</LookAtUp>

                    <LookAtEye type="floatarray">0,0,8</LookAtEye>

                    <LookAtCenter type="floatarray">0,0,0</LookAtCenter>


                    <StereoNode displaynodeType="VRStereoNode">

                        <EyeSeparation>0.203</EyeSeparation>

                        <ProjectionNode displaynodeType="VROffAxisProjectionNode">

                             <TopLeft type="floatarray">-2,2,0</TopLeft>

                             <TopRight type="floatarray">-2,2,0</TopRight>

                             <BottomLeft type="floatarray">-2,-2,0</BottomLeft>

                             <BottomRight type="floatarray">2,-2,0</BottomRight>

                             <DUMMY/>

                        </ProjectionNode>

                    </StereoNode>

                </LookAtNode>

            </RootNode>

        </Desktop>

    </VRSetups>

</MinVR>

2.   In order to tell MinVR to use this configuration file, we have to use a command line argument. This time, when running the application, use the command

./test -c config/stereo.minvr

You should see two views both looking at a rotating red cube, each at a slightly different angle.

Running with OpenVR

If you do not intend on running your application on an OpenVR device (HTC Vive or Oculus Rift), then you can skip this section.

Simply displaying a stereo view on desktop is boring. Let's look at our application through a headset!

1.   In the config folder, create another configuration file "openvr.minvr". This will be the configuration file used to run applications through OpenVR.

<MinVR>

    <GLFWPlugin pluginType="MinVR_GLFW"/>

    <OpenGLPlugin pluginType="MinVR_OpenGL"/>

    <OpenVRPlugin pluginType="MinVR_OpenVR"/>


    <RGBBits>8</RGBBits>

    <AlphaBits>8</AlphaBits>

    <DepthBits>24</DepthBits>

    <StencilBits>8</StencilBits>

    <FullScreen>0</FullScreen>

    <Resizable>1</Resizable>

    <AllowMaximize>1</AllowMaximize>

    <Visible>1</Visible>

    <SharedContextGroupID>-1</SharedContextGroupID>

    <ContextVersionMajor>3</ContextVersionMajor>

    <ContextVersionMinor>3</ContextVersionMinor>

    <UseGPUAffinity>1</UseGPUAffinity>

    <UseDebugContext>0</UseDebugContext>

    <MSAASamples>1</MSAASamples>

    <QuadBuffered>0</QuadBuffered>


    <StereoFormat>Mono</StereoFormat>


    <VRSetups>

        <Desktop hostType="VRStandAlone">

            <GLFWToolkit windowtoolkitType="VRGLFWWindowToolkit"/>

            <OpenGLToolkit graphicstoolkitType="VROpenGLGraphicsToolkit"/>

            <RootNode displaynodeType="VRGraphicsWindowNode">

                <Border>1</Border>

                <Caption>Mirror</Caption>

                <GPUAffinity>0</GPUAffinity>

                <XPos>100</XPos>

                <YPos>100</YPos>

                <Width>640</Width>

                <Height>640</Height>

                <HTC displaynodeType="VROpenVRNode">

                    <HideTracker>0</HideTracker>

                    <ReportStatePressed>1</ReportStatePressed>

                    <ReportStateTouched>1</ReportStateTouched>

                    <ReportStateAxis>1</ReportStateAxis>

                    <ReportStatePose>1</ReportStatePose>

                    <DrawHMDOnly>0</DrawHMDOnly>

                    <MSAA_buffers>4</MSAA_buffers>

                </HTC>

            </RootNode>

        </Desktop>

    </VRSetups>

</MinVR>

2.   Connect your SteamVR-compatible headset and launch SteamVR. Perform any setup as needed.

3.   Now, run your application with the new config file.

./test -c config/openvr.minvr

This time, you should be able to view your spinning red cube through your headset with a mirror displayed on the desktop as well!