Hello Triangle! using Emscripten & CMake (C++/WebGL)

> Coding, hacking, computer graphics, game dev, and such...
User avatar
fips
Site Admin
Posts: 170
Joined: Wed Nov 12, 2008 9:49 pm
Location: Prague
Contact:

Hello Triangle! using Emscripten & CMake (C++/WebGL)

Post by fips »

I've been intrigued by the idea of trying Emscripten for cross-compiling C++ code into JavaScript for some time. So I've decided to give it a try and also involved CMake to help me set up a testing pipeline for bringing an OpenGL/GLUT-based application (Hello Triangle! in this case) from C++ to JavaScript, all controlled by a simple self-contained build script written in Python (build.py).

As mentioned above, in order to be able to build the example, there're are some packages to install, namely: Emscripten SDK and CMake. Python (2.x) is also required but you can use the one bundled with Emscripten, just make sure both CMake & Python are added into PATH (to make them accessible from the command-line). Please note that the whole set-up has been tested on a Windows machine only, but it should be a matter of minutes to tweak it for other platforms as well (OSX & Linux).

Download the source package: hello_triangle_emscripten.zip (~70 kB)
Check the resulting page: Hello Triangle!

The image below shows the expected result (a spinning triangle):

Image

In order to build the demo application, please start a new command prompt using emcmdprompt.bat (typically located in c:\Program Files\Emscripten\), then cd into the hello_triangle_emscripten directory and type build.py. The image bellow shows the steps and the output:

Image
Then check the build directory for hello_triangle.js/html

Basically, the whole project consists of these 3 files:

VIEW THE CODE BELOW IN FULL-SCREEN (main.cpp)

Code: Select all

/*
(c) 2014 +++ Filip Stoklas, aka FipS, http://www.4FipS.com +++
THIS CODE IS FREE - LICENSED UNDER THE MIT LICENSE
ARTICLE URL: http://forums.4fips.com/viewtopic.php?f=3&t=1201 
*/

#define GL_GLEXT_PROTOTYPES

#include <cstdio>
#include <cassert>
#include <GL/glut.h>

struct Context
{
   int width, height;
   GLuint vert_id, frag_id;
   GLuint prog_id, geom_id;
   GLint u_time_loc;
   
   enum { Position_loc, Color_loc };

   Context():
   width(400), height(300),
   vert_id(0), frag_id(0),
   prog_id(0), geom_id(0),
   u_time_loc(-1)
   {}

} g_context;

void init()
{
   printf("init()\n");

   glClearColor(.3f, .3f, .3f, 1.f);

   auto load_shader = [](GLenum type, const char *src) -> GLuint
   {
      const GLuint id = glCreateShader(type);
      assert(id);
      glShaderSource(id, 1, &src, nullptr);
      glCompileShader(id);
      GLint compiled = 0;
      glGetShaderiv(id, GL_COMPILE_STATUS, &compiled);
      assert(compiled);
      return id;
   };

   g_context.vert_id = load_shader(
    GL_VERTEX_SHADER,
    "attribute vec4 a_position;              \n"
    "attribute vec4 a_color;                 \n"
    "uniform float u_time;                   \n"
    "varying vec4 v_color;                   \n"
    "void main()                             \n"
    "{                                       \n"
    "    float sz = sin(u_time);             \n"
    "    float cz = cos(u_time);             \n"
    "    mat4 rot = mat4(                    \n"
    "     cz, -sz, 0,  0,                    \n"
    "     sz,  cz, 0,  0,                    \n"
    "     0,   0,  1,  0,                    \n"
    "     0,   0,  0,  1                     \n"
    "    );                                  \n"
    "    gl_Position = a_position * rot;     \n"
    "    v_color = a_color;                  \n"
    "}                                       \n"
   );
   printf("- vertex shader loaded\n");

   g_context.frag_id = load_shader(
    GL_FRAGMENT_SHADER,
    "precision mediump float;                \n"
    "varying vec4 v_color;                   \n"
    "void main()                             \n"
    "{                                       \n"
    "    gl_FragColor = v_color;             \n"
    "}                                       \n"
   );
   printf("- fragment shader loaded\n");

   g_context.prog_id = glCreateProgram();
   assert(g_context.prog_id);
   glAttachShader(g_context.prog_id, g_context.vert_id);
   glAttachShader(g_context.prog_id, g_context.frag_id);
   glBindAttribLocation(g_context.prog_id, Context::Position_loc, "a_position");
   glBindAttribLocation(g_context.prog_id, Context::Color_loc, "a_color");
   glLinkProgram(g_context.prog_id);
   GLint linked = 0;
   glGetProgramiv(g_context.prog_id, GL_LINK_STATUS, &linked);
   assert(linked);
   g_context.u_time_loc = glGetUniformLocation(g_context.prog_id, "u_time");
   assert(g_context.u_time_loc >= 0);
   glUseProgram(g_context.prog_id);
   printf("- shader program linked & bound\n");

   struct Vertex { float x, y, z; unsigned char r, g, b, a; };
   const Vertex vtcs[] {
    {  0.f,  .5f, 0.f,   255, 0, 0, 255 },
    { -.5f, -.5f, 0.f,   0, 255, 0, 255 },
    {  .5f, -.5f, 0.f,   0, 0, 255, 255 }
   };
   glGenBuffers(1, &g_context.geom_id);
   assert(g_context.geom_id);
   glBindBuffer(GL_ARRAY_BUFFER, g_context.geom_id);
   glBufferData(GL_ARRAY_BUFFER, sizeof(vtcs), vtcs, GL_STATIC_DRAW);
   auto offset = [](size_t value) -> const GLvoid * { return reinterpret_cast<const GLvoid *>(value); };
   glVertexAttribPointer(Context::Position_loc, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), offset(0));
   glEnableVertexAttribArray(Context::Position_loc);
   glVertexAttribPointer(Context::Color_loc, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(Vertex), offset(3 * sizeof(float)));
   glEnableVertexAttribArray(Context::Color_loc);
   printf("- geometry created & bound\n");
}

void resize(int width, int height)
{
   printf("resize(%d, %d)\n", width, height);
   
   g_context.width = width;
   g_context.height = height;
}

void draw()
{
   glViewport(0, 0, g_context.width, g_context.height);
   glClear(GL_COLOR_BUFFER_BIT);

   glUniform1f(g_context.u_time_loc, glutGet(GLUT_ELAPSED_TIME) / 1000.f);
   glDrawArrays(GL_TRIANGLES, 0, 3);
   
   glutSwapBuffers();
}

void update()
{
   glutPostRedisplay();   
}

int main(int argc, char *argv[])
{
   printf("main()\n");

   glutInit(&argc, argv);
   glutInitWindowSize(g_context.width, g_context.height);
   glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);

   glutCreateWindow("Hello Triangle! | 4FipS.com");

   glutReshapeFunc(resize);
   glutDisplayFunc(draw);
   glutIdleFunc(update);

   init();

   glutMainLoop();

   return 0;
}
VIEW THE CODE BELOW IN FULL-SCREEN (CMakeLists.txt)

Code: Select all

# /*
# (c) 2014 +++ Filip Stoklas, aka FipS, http://www.4FipS.com +++
# THIS CODE IS FREE - LICENSED UNDER THE MIT LICENSE
# ARTICLE URL: http://forums.4fips.com/viewtopic.php?f=3&t=1201 
# */

cmake_minimum_required(VERSION 2.8)
add_definitions("-std=c++11")
project(hello_triangle)
file(GLOB sources *.cpp)
add_executable(hello_triangle.html ${sources})
VIEW THE CODE BELOW IN FULL-SCREEN (build.py)

Code: Select all

#!/usr/bin/env python

# /*
# (c) 2014 +++ Filip Stoklas, aka FipS, http://www.4FipS.com +++
# THIS CODE IS FREE - LICENSED UNDER THE MIT LICENSE
# ARTICLE URL: http://forums.4fips.com/viewtopic.php?f=3&t=1201 
# */

import os
import stat
from shutil import rmtree
from subprocess import check_call

def resolve_path(rel_path):
    return os.path.abspath(os.path.join(os.path.dirname(__file__), rel_path)) 

def rmtree_silent(root):
    def remove_readonly_handler(fn, root, excinfo):
        if fn is os.rmdir:
            if os.path.isdir(root): # if exists
                os.chmod(root, stat.S_IWRITE) # make writable
                os.rmdir(root)
        elif fn is os.remove:
            if os.path.isfile(root): # if exists
                os.chmod(root, stat.S_IWRITE) # make writable
                os.remove(root)
    rmtree(root, onerror=remove_readonly_handler)

def makedirs_silent(root):
    try:
        os.makedirs(root)
    except OSError: # mute if exists
        pass

if __name__ == "__main__":

    build_dir = resolve_path("./build")
    rmtree_silent(build_dir)
    makedirs_silent(build_dir)
    os.chdir(build_dir)

    check_call([
     "cmake",
     os.path.expandvars("-DCMAKE_TOOLCHAIN_FILE=$EMSCRIPTEN/cmake/Platform/Emscripten.cmake"),
     "-DCMAKE_BUILD_TYPE=Release",
     "-DCMAKE_MAKE_PROGRAM=mingw32-make",
     "-G", "Unix Makefiles",
     ".."
    ])

    check_call(["mingw32-make"])