Help Transitioning from Legacy OpenGL to Modern OpenGL in JUCE

Hi everyone,

I’m working on a project where I need to integrate OpenGL code into the JUCE framework. I’ve only worked with the older version of OpenGL (using functions like glBegin(), glVertex(), and gluPerspective()), but JUCE uses modern OpenGL, and I’m struggling to understand how to modify my code to fit into this.

I’ve read the JUCE OpenGL tutorial, but I’m still very lost on how to make the transition. I have some specific code that renders a simple guitar pick and responds to mouse input to rotate the view and move the pick on screen.

Here’s my original code:

#define _USE_MATH_DEFINES

#include <GL/glut.h>
#include <iostream>

void SetupRC(void);
void RenderScene(void);
void TimerFunc(int value);
void ChangeSize(int w, int h);
void Mouse(int button, int state, int x, int y);
void MouseMotion(int x, int y);

// Window Dimensions
const int INI_WINDOW_WIDTH = 1500;
const int INI_WINDOW_HEIGHT = 750;
const float INI_WINDOW_DEPTH = 1000.0f;
const float DEPTH = 600.0f;

// Pick Dimensions
const float PICK_WIDTH = 27.94; // Pick width (mm)
const float PICK_LENGTH = 32; // Pick length (mm)
const float PICK_THICKNESS = 0.73; // Pick thickness (mm)

// Guitar Dimensions
const float SCALE_LENGTH = 647.7f; // Scale length (mm)

// Mouse Motion
bool mouseLeftDown;
bool mouseRightDown;
float mouseX;
float mouseY;
float preMouseY; // Previous mouse Y coordinate
float rotateX;
float rotateY;
float cameraAngleX = 0.0f;
float cameraAngleY = 0.0f;
float cameraDistance;


int main(int argc, char* argv[])
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
	glutInitWindowSize(INI_WINDOW_WIDTH, INI_WINDOW_HEIGHT);
	glutCreateWindow("Guitar Simulator");

	glutDisplayFunc(RenderScene);
	glutReshapeFunc(ChangeSize);
	glutMouseFunc(Mouse);
	glutMotionFunc(MouseMotion);

	glutTimerFunc(1, TimerFunc, 1);

	SetupRC();
	glutMainLoop();

	return 0;
}

void RenderScene(void)
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();

	glPushMatrix();
	glTranslatef(0.0f, 0.0f, -DEPTH);
	glRotatef(cameraAngleX, 1, 0, 0);
	glRotatef(cameraAngleY, 0, 1, 0);
	glTranslatef(-SCALE_LENGTH / 2.0f, 0.0f, 0.0f);

	// Pick
	glPushMatrix();
	glColor3f(1.0f, 0.0f, 0.0f); // red
	glBegin(GL_TRIANGLES);
	glVertex3f(mouseX, mouseY, 0.0f); // Tip of the pick
	glVertex3f(mouseX - PICK_WIDTH / 2, mouseY - 10.0f, PICK_LENGTH); // One end of the base
	glVertex3f(mouseX + PICK_WIDTH / 2, mouseY - 10.0f, PICK_LENGTH); // The other end of the base
	glEnd();
	glPopMatrix();

	glPopMatrix();

	glFlush();
	glutSwapBuffers();
}

void SetupRC(void)
{
	// Antialiasing 
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glEnable(GL_BLEND);
	glEnable(GL_POINT_SMOOTH);
	glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);
	glEnable(GL_LINE_SMOOTH);
	glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
	glEnable(GL_POLYGON_SMOOTH);
	glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST);

	// Set material properties
	glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE);

	// Background
	glClearColor(1.0f, 1.0f, 1.0f, 1.0f); // White

}

void TimerFunc(int value)
{
	// Redraw the scene with new coordinates
	glutPostRedisplay();
	glutTimerFunc(1, TimerFunc, 1);
}

void ChangeSize(int w, int h)
{
	GLfloat aspectRatio;

	// Prevent a divide by zero
	if (h == 0)
		h = 1;

	// Set Viewport to window dimensions
	glViewport(0, 0, w, h);

	// Reset coordinate system
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();

	// Establish perspective projection
	aspectRatio = (GLfloat)w / (GLfloat)h;
	gluPerspective(45.0, aspectRatio, 1.0, INI_WINDOW_DEPTH);

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
}

void Mouse(int button, int state, int x, int y)
{
	if (button == GLUT_RIGHT_BUTTON)
	{
		rotateX = x;
		rotateY = y;
		if (state == GLUT_DOWN)
			mouseRightDown = true;

		else if (state == GLUT_UP)
			mouseRightDown = false;
	}

	else if (button == GLUT_LEFT_BUTTON)
	{
		mouseX = (float)x / glutGet(GLUT_WINDOW_WIDTH) * SCALE_LENGTH;
		mouseY = -(float)y + glutGet(GLUT_WINDOW_HEIGHT) / 2.0f;
		if (state == GLUT_DOWN)
			mouseLeftDown = true;

		else if (state == GLUT_UP)
			mouseLeftDown = false;
	}
}

void MouseMotion(int x, int y)
{
	if (mouseRightDown)
	{
		cameraAngleY += (x - rotateX) * 0.1f;
		cameraAngleX += (y - rotateY) * 0.1f;
		rotateX = x;
		rotateY = y;
	}
	else if (mouseLeftDown)
	{
		mouseX = (float)x / (float)glutGet(GLUT_WINDOW_WIDTH) * SCALE_LENGTH;
		mouseY = -(float)y + (float)glutGet(GLUT_WINDOW_HEIGHT) / 2.0f;

		preMouseY = mouseY;
	}
}

This would require you to learn how it works, but I can give you some guidelines to get started.

The drawing data (vertices, colors, and whatever else you want) is stored in buffers, this could be a basic class for a buffer

struct Buffer
{
    GLuint ID = 0;

    void create(int size)
    {
        glGenBuffers(1, &ID);
        glBindBuffer(GL_ARRAY_BUFFER, ID);
        glBufferStorage(GL_ARRAY_BUFFER, size, nullptr, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT);
    }

    void writeData(const void* data, int size)
    {
        glBindBuffer(GL_ARRAY_BUFFER, ID);
        void *bufferMap = glMapBufferRange(GL_ARRAY_BUFFER, 0, size, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT);
        memcpy(bufferMap, (void*)data, size);
        glUnmapBuffer(GL_ARRAY_BUFFER);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }

    ~Buffer()
    {
        if (ID) glDeleteBuffers(1, &ID);
    }
};

The drawing program is stored in shaders, this would be a basic example for a shader that draws data (stored in a buffer) that has 2 vertices and 4 colors.

    std::unique_ptr<juce::OpenGLShaderProgram> shaderProgram;
    //---------------------------------------------------
        const char vertexShader[] =
        R"FOO"(
            #version 430 core
            layout(location = 0) in vec2 position;        
            layout(location = 1) in vec4 color;
            uniform mat4 mvpMatrix;
            out vec4 vColor;
            void main()
            {
                gl_Position = mvpMatrix  * vec4(position.x,position.y,0,1);
                vColor = color;
            };
        )FOO"";
    
        const char fragmentShader[] =
        R"FOO"(
            #version 430 core
            in vec4 vColor;
            out vec4 color;
		    void main()	{
			    color = vColor;
		    };
	    )FOO"";      
        

        shaderProgram.reset(new juce::OpenGLShaderProgram(*juce::OpenGLContext::getCurrentContext()));
        shaderProgram->addVertexShader(vertexShader);
        shaderProgram->addFragmentShader(fragmentShader);
        shaderProgram->link();

and this would be the call to draw, since it is a few calls to the graphics card where the shader and buffer reside, this puts almost zero load on the processor.

    void drawBuffer(Buffer& buffer, GLenum mode, GLsizei count, GLint first = 0)
    {
        glBindBuffer(GL_ARRAY_BUFFER, buffer.ID);
        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, x));
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, color.r));
        glEnableVertexAttribArray(1);

        shaderProgram->use();

        float matModelViewProjection[16];
        float matModelView[16];
        glGetFloatv(GL_MODELVIEW_MATRIX, matModelView);

        glMatrixMode(GL_PROJECTION);
        glPushMatrix();
        glMultMatrixf(matModelView);

        glGetFloatv(GL_PROJECTION_MATRIX, matModelViewProjection);
        glPopMatrix();
        glMatrixMode(GL_MODELVIEW);

        GLint location;
        if ( (location = glGetUniformLocation(shaderProgram->getProgramID(), "mvpMatrix"))>=0 ) glUniformMatrix4fv(location, 1, false, matModelViewProjection);

        glDrawArrays(mode, first, count);

        glUseProgram(0);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glDisableVertexAttribArray(0);
        glDisableVertexAttribArray(1);
    }

before drawing when you get the values ​​from modelview and projection glGetFloatv and multiply them to pass them to the shader, it should be able to be drawn with the current camera and view, without any further modifications or updates to the code. (although some fixed pipeline drawing operations will no longer have an effect on this specific drawing since the shader is now in control), in my opinion you can continue to use glVertex freely since it is not incompatible.

So to draw in a modern way these would be the steps.

initiation:
createShader();
buffer.create(100);

update:
buffer.write(data, 100);

drawing:
render(buffer, GL_LINE_STRIP, 100, 0);

how you put it together is up to you, for example you could create a class that contains the shader, the buffer, and the drawing function. Or you could separate them into different classes, or you could use no classes at all.

if you want to get rid of glut, you really only need to define the view and a camera, put all your code in an OpenGLAppComponent and at the start define a view port in initialise()

  void initialise() override
  {
      glViewport(0, 0, getWidth(), getHeight());
  }

and the camera (2D) and clear screen in render()

  void render() override
  {
        glMatrixMode(GL_PROJECTION); glLoadIdentity();
        glOrtho(camera.left, camera.right, camera.bottom, camera.top, 0, 1);
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
        glScalef(camera.zoomX, camera.zoomY, 1.0f);

        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
  }

I got it, thank you so much