Search code examples
c++webglglfwwebassemblyemscripten

How to get Emscripten + WebGL to render before termination or input requested


I'm trying to get some old C++ code I wrote to compile to WebAssembly and WebGL using Emscripten. I have a very simple one-file C++ project that renders the Mandelbrot set, which works great using g++. I run it and it does in fact render the Mandelbrot set in ever-increasing detail.

I've gotten my code to compile using emcc, which took a lot of reading... but it still doesn't work as I want. For some reason, when I go to the HTML page, nothing is rendered until the code either terminates or asks for input. At that time, the correct frame (the Mandelbrot set) is in fact rendered.

Obviously what I'd want is for each frame to render before the frame rate timer starts. However I can't get that to work, even with a glFinish() call.

Would someone please take a look at my code and see if I've done anything obvious wrong?

Thanks:

// Charles Swanson, [email protected]
// Written 2019/11/30
// Compile using:
// g++ SmallestOpenGL.cpp -o SmallestOpenGL_Bin -lGL -lglfw
// emcc SmallestOpenGL.cpp -o ../IgnoreMe/SmallestOpenGL.html -s USE_GLFW=3 -s MIN_WEBGL_VERSION=2

// Copyright me, all rights reserved for now.

#define GLFW_INCLUDE_ES3
#include <GLFW/glfw3.h>
#include <iostream>
#include <time.h>
#include <thread>

const char* vertexShaderSource = "#version 300 es\n"
    "layout (location = 0) in vec3 aPos;\n"
    "out float u;\n"
    "out float v;\n"
    "void main()\n"
    "{\n"
    "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
    "   u = aPos.x;\n"
    "   v = aPos.y;\n"
    "}\0";

const char* fragmentShaderSource = "#version 300 es\n"
    "precision mediump float;\n"
    "uniform float maxiters;\n"
    "in float u;\n"
    "in float v;\n"
    "out vec4 FragColor;\n"
    "void main()\n"
    "{\n"
    "   FragColor = vec4(0.0f, 0.0f, 0.0f, 1.0f);\n"
    "   float scaledx = (u-0.5)*1.5f;\n"
    "   float scaledy = v*1.0f;\n"
    "   float currentx = scaledx;\n"
    "   float currenty = scaledy;\n"
    "   float currentx2 = 0.0f;\n"
    "   float currenty2 = 0.0f;\n"
    "   for (int loopind = 1;loopind<int(maxiters);loopind++)\n"
    "   {\n"
    "      currentx2 = scaledx + currentx*currentx - currenty*currenty;\n"
    "      currenty2 = scaledy + 2.0f*currentx*currenty;\n"
    "      currentx = clamp(currentx2,-10000.0f,10000.0f);\n"
    "      currenty = clamp(currenty2,-10000.0f,10000.0f);\n"
    "   }\n"
    "   float amplitude = currentx2*currentx + currenty2*currenty;\n"
    "   if (amplitude>4.0)\n"
    "       FragColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);\n"
    "}\0";

int main(void)
{
    glfwInit();

    glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR,0);

    GLFWwindow* window = glfwCreateWindow(400,300,"The Window Title",NULL,NULL);
    glfwMakeContextCurrent(window);

    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader,1,&vertexShaderSource,NULL);
    glCompileShader(vertexShader);

    GLchar infoLog[256];
    glGetShaderInfoLog(vertexShader,256,NULL,infoLog);
    std::cout<<"Vertex shader info log says: "<<infoLog<<std::endl;

    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader,1,&fragmentShaderSource,NULL);
    glCompileShader(fragmentShader);

    glGetShaderInfoLog(fragmentShader,256,NULL,infoLog);
    std::cout<<"Fragment shader info log says: "<<infoLog<<std::endl;

    GLuint program = glCreateProgram();
    glAttachShader(program,vertexShader);
    glAttachShader(program,fragmentShader);
    glLinkProgram(program);

    glGetProgramInfoLog(program,256,NULL,infoLog);
    std::cout<<"Progam info log says: "<<infoLog<<std::endl;
    
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    float vertices[] = {
        -1.0f, -1.0f, 0.0f, // bottom left
        -1.0f,  1.0f, 0.0f, // top left 
         1.0f, -1.0f, 0.0f, // bottom right  
        -1.0f,  1.0f, 0.0f, // top left
         1.0f, -1.0f, 0.0f, // bottom right 
         1.0f, 1.0f, 0.0f, // top right 
    }; 

    GLuint VAO;
    glGenVertexArrays(1,&VAO);
    glBindVertexArray(VAO);
    glEnableVertexAttribArray(0);

    GLuint VBO;
    glGenBuffers(1,&VBO);
    glBindBuffer(GL_ARRAY_BUFFER,VBO);
    glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);

    glVertexAttribPointer(0,3,GL_FLOAT,GL_TRUE,3*sizeof(float),0);

    glUseProgram(program);

    GLuint maxitersLocation = glGetUniformLocation(program,"maxiters");
    GLuint xsizeLocation = glGetUniformLocation(program,"xsize");
    GLuint ysizeLocation = glGetUniformLocation(program,"ysize");
    std::cout<<"Maxiters location is "<<maxitersLocation<<std::endl;

    int xsize;
    int ysize;
    glfwGetWindowSize(window,&xsize,&ysize);
    std::cout<<"Window size is "<<xsize<<" x "<<ysize<<std::endl;

    glUniform1f(maxitersLocation,10);
    

    float iters = 0;
    while (iters<10)
    {
        
        iters = iters+0.1;
        if (iters>10)
                iters = 10;
        glUniform1f(maxitersLocation,iters);
        glClearColor(1,0.5,0,1);
        glClear(GL_COLOR_BUFFER_BIT);

        glDrawArrays(GL_TRIANGLES,0,6);

        glfwSwapBuffers(window);
        glfwPollEvents();
        glFinish();
        std::this_thread::sleep_for(std::chrono::milliseconds(long(30)));
        
    }

    char in;
    std::cin>>in;
    glfwTerminate();
}

Solution

  • Ok, I figured it out. Many web examples implement the solution; I guess I glossed over them. Here are a few:

    https://github.com/pcbaecker/example-emscripten-webgl

    https://gist.github.com/mortennobel/0e9e90c9bbc61cc99d5c3e9c038d8115

    https://gist.github.com/ousttrue/0f3a11d5d28e365b129fe08f18f4e141

    https://github.com/QafooLabs/emscripten-opengl-example

    The key is to use the function implemented in Emscripten.h called emscripten_set_main_loop. Pass that function a handle to your render function, and it renders every frame. Excellent!