The structure of most programs in LearnOpenGL is the same. In this we will translate the C++ file that draws a triangle on a canvas using OpenGL, the 'HelloTriangle.cpp' program. In the book it is in Chapter 5. We will see that the differences between the C++ and the javascript versions are not that many.
We have to create two programs. One runs on the CPU and the other on a dedicated graphics card, the GPU. The first is defined using OpenGL calls and the other is defined using the GLSL language (GL-Shading-Language). The LearnOpenGL programs are based on OpenGL version 3.3 and GLSL version version 330 core. Joey de Vries took the decision not to use later versions. Using OpenGL is described in chapter 2 of the book.
We use WebGL2 for OpenGL calls and GLSL version 300 es. The specification of WebGL2 is on the WebGL2, Khronos site. WebGL2 is based on OpenGL ES 3.00 (ES stands for Embedded-Systems). Reading the specification will show that much of it refers to text in the OpenGL ES 3.0 specification. The specification of OpenGL ES3.00 is also on the same site OpenGL ES 3, Khronos site. In this specification, section 1.5.1 it says
OpenGL ES 3.0 implementations are guaranteed to support versions 3.00 and 1.00 of the OpenGL ES Shading Language. All references to sections of that specification refer to version 3.00. The latest supported version of the shading language may be queried as described in section 6.1.5.`
We assume that OpenGL ES 3.00 is implemented on our computer and that we can use version 3.00 of the GLSL shading language.
Because we use a different version of GLSL we have to change a few things in the shaders we use. In the code of the C++ program the fragment shader is defined in the following fragment:
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";
The translation of this code we use in our javascipt is:
const fragmentShaderSource = `#version 300 es
precision mediump float;
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}`
We see, there are not many differences. Because javascript supports multiline strings (from the first backtick to the next), we do not have to enter the newlines \n
in the text. But we see a real difference at the top: we have to define another version for GLSL and also add a precision for floats.
We have to be carefull when using shaders of others. Older version shaders often use the attribute
and varying
keywords. They are now replaced by in
and out
. Those shaders also use the texture2d() function that is now replaced by texture(). But for the shaders in the book we have to make only a few changes.
The signatures of all functions in the WebGL2 interface can be downloaded using npm. In the previous section ('project.html') npm is already mentioned. In the terminal we give the command npm install --save @types/webgl2
and in our rootdirectory a new directory node_modules
with subdirectory @types/webgl2
is created. In it is the file 'index.d.ts', the file with all the definitions of Webgl2. When editing typescript code with VS Code this file is automatically detected.
In our C++ program a library GLAD is used to get pointers to the C++ functions of OpenGL. In the program we see the text:
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
In javascript we get an object gl that has the implementation of 'all' OpenGL functions. This object, called the GL context, can be created using a function on a canvas:
The object gl is of type WebGL2RenderingContext and we can call all methods in that interface, for example gl.BindVertexArray();
. In the C++ code there is no context object and we call the same function directly using glBindVertexArray();
(the same, but now without the dot). Most C++ call to OpenGL can be converted to javascript calls by adding the dot. One notable exception is the creation of buffers and vertex arrays. In C++ we use glGenVertexArrays(1, &VAO);
and glGenBuffers(1, &VBO);
to create 1 vertex array object and 1 OpenGL buffer. In javascript we use VAO = gl.createVertexArray();
and VBO = gl.createBuffer();
.
We will be using the requestAnimationFrame method to ask for an update of the scene and a repaint of the canvas. Following text is copied from the Mozilla docs:
The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update an animation before the next repaint. The method takes a callback as an argument to be invoked before the repaint.
Note: Your callback routine must itself call requestAnimationFrame() if you want to animate another frame at the next repaint. You should call this method whenever you're ready to update your animation onscreen. This will request that your animation function be called before the browser performs the next repaint. The number of callbacks is usually 60 times per second, but will generally match the display refresh rate in most web browsers as per W3C recommendation. requestAnimationFrame() calls are paused in most browsers when running in background tabs or hidden <iframe>s in order to improve performance and battery life. The callback method is passed a single argument, a DOMHighResTimeStamp, which indicates the current time (based on the number of milliseconds since time origin). When callbacks queued by requestAnimationFrame() begin to fire multiple callbacks in a single frame, each receives the same timestamp even though time has passed during the computation of every previous callback's workload. This timestamp is a decimal number, in milliseconds, but with a minimal precision of 1ms (1000 µs).
The C++ code uses in the main() function a render loop
while (!glfwWindowShouldClose(window))
We will translate this by ending our main() javascript function with a call to requestAnimationFrame(render) and making render() a function that contains the translation of the code inside the C++ loop. To get the loop structure back we can end the code in render() by calling requestAnimationFrame(render) again. This is only nescessary if we really want to redraw the scene as in animations. Many programs in the book do not need the loop and we may forget putting the call of requestAnimationFrame inside the render() function.
Another place where requestAnimationFrame(render) appears is in the callback that gets called when the window resizes.
In the C++ programs the keyboard state gets polled. In the render loop a call is made to processInput() and in that function a test is done if certain keys are pressed. In javascript code we react to pressing a key with writing a callback function. In our javascript programs we will simulate the polling with a self made class KeyInput. We will have to register the keys that are used in the program like so:
//D3Q: the keyboard-keys the program reacts to; the keyInput gets queried
const GLFW_KEY_W = 'w', GLFW_KEY_S = 's', GLFW_KEY_A = 'a', GLFW_KEY_D = 'd',
GLFW_KEY_SPACE = ' ';
let keyInput = new KeyInput({
GLFW_KEY_W, GLFW_KEY_S, GLFW_KEY_A, GLFW_KEY_D,
GLFW_KEY_SPACE
});
The names of the constants are taken from the GLFW library. Of course we could also have registered te keys with let keyInput = new KeyInput({'w', 's', 'a', 'd', ' '})
.
In the program of 'HelloTriangle' the only key that is used is the GLFW_KEY_ESCAPE key for closing a window. Because our canvas is a part of a html document we will not react to this key.
First we look at the structure of our C++ program:
//includes
...
// settings
...
const char *vertexShaderSource = "#version 330 core\n"
...
const char *fragmentShaderSource = "#version 330 core\n"
...
int main()
{
// glfw: initialize and configure
gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)
...
// glfw window creation
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
...
// glad: load all OpenGL function pointers
...
// build and compile our shader program
// vertex shader
int vertexShader = glCreateShader(GL_VERTEX_SHADER);
...
// fragment shader
int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
...
// link shaders
int shaderProgram = glCreateProgram();
...
glLinkProgram(shaderProgram);
...
// set up vertex data (and buffer(s)) and configure vertex attributes
float vertices[] = {
-0.5f, -0.5f, 0.0f, // left
0.5f, -0.5f, 0.0f, // right
0.0f, 0.5f, 0.0f // top
};
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
...
glBindBuffer(GL_ARRAY_BUFFER, 0);
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// input
processInput(window);
// render
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// draw our first triangle
glUseProgram(shaderProgram);
...
glDrawArrays(GL_TRIANGLES, 0, 3);
...
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
return 0;
}
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
We will use a similar structure in typescript. The use of a method render() next to the main() method has as a consequence that the variables they share must be declared as global variables. In this program they are canvas
, gl
, shaderProgram
and VAO
.
// imports
...
// settings
...
const char vertexShaderSource =`#version 300 es
precision mediump float;...
...
const char fragmentShaderSource = `#version 300 es
precision mediump float;...
...
// globals
let gl: WebGL2RenderingContext = null;
let shaderProgram: WebGL2 = null;
let VAO: WebGL2 = null;
let canvas: = null;
int main()
{
// canvas creation and initializing OpenGL context
let canvas = document.createElement('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.body.appendChild(canvas);
gl = canvas.getContext('webgl2');
if (!gl) {
console.log("WebGL 2 needed"); return;
}
...
// resize callback
glfwSetFramebufferSizeCallback(window, framebufferSizeCallback);
...
// build and compile our shader program
// vertex shader
let vertexShader = gl.CreateShader(gl.VERTEX_SHADER);
...
// fragment shader
let fragmentShader = gl.CreateShader(gl.FRAGMENT_SHADER);
...
// link shaders
shaderProgram = gl.CreateProgram();
...
gl.LinkProgram(shaderProgram);
...
// set up vertex data (and buffer(s)) and configure vertex attributes
float vertices[] = {
-0.5f, -0.5f, 0.0f, // left
0.5f, -0.5f, 0.0f, // right
0.0f, 0.5f, 0.0f // top
};
unsigned int VBO, VAO;
VAO = gl.createVertexArrays();
let VBO = gl.createBuffers();
...
gl.BindBuffer(gl.ARRAY_BUFFER, null);
requestAnimationFrame(render);
}
// render loop
function render(){
// input
processInput(window);
// render
gl.ClearColor(0.2, 0.3, 0.3, 1.0);
gl.Clear(gl.COLOR_BUFFER_BIT);
// draw our first triangle
gl.UseProgram(shaderProgram);
...
gl.DrawArrays(gl.TRIANGLES, 0, 3);
...
// if looping
requestAnimationFrame(render);
}
}
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
void framebufferSizeCallback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
requestAnimationFrame(render);
}
We will use a step by step approach to make a start of the translation of the C++ text into a Typescript/javascript file. These are the steps:
add new ChXX (XX = chapter number given in offline book) directory and add the C++ file we will translate to the project. Rename the C++ program to XXX.ts. Update tsconfig to compile XXX.ts. Add a XXX.html file. Put shader files in the js/ChXX subdirectory 'shaders'. Rename files from shadername.vs to vs_shadername.js and from shadername.fs to fs_shadername.js and add file index.js in the shaders directory.
add a section // global variables
before main() add variables let canvas: HTMLCanvasElement
and let gl: WebGL2RenderingContext
to this section
deletes and replacements at the start of each file.
#includes
with import { X } from "../XXX"
. In Part1 more and more incluses will be used: for a shader, math, keyboard... If nescessary add keyboard, mouse, camera or shader variables to globals. Instantiate them in main()const unsigned int SCR_WIDTH = 800;
and const unsigned int SCR_HEIGHT = 600;
from the setting sectionadd the const sizeFloat = 4;
to the setting section
remove from cpp in main() sections // glfw: initialize and configure // glad: load all OpenGL function pointers
and replace this with
// canvas creation and initializing OpenGL context
canvas = document.createElement('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.body.appendChild(canvas);
gl = canvas.getContext('webgl2');
if (!gl) {
console.log("WebGL 2 needed"); return;
}
window.onresize = () => { framebufferSizeCallback(window.innerWidth,
window.innerHeight) }
search/replace (regular expresiion) gl functions and constants
gl([A-Z])(\w+)\(
with gl.$1$2(
gl\.([A-Z])
, then activate editor and select all ocurrences with Alt+Enter
. then use Ctrl+Shift+P >toLower
functionsearch/replace (case sensitive) GL_
with gl.
some renaming of gl functions
gl.gen
with gl.Create
, but not for gl.generateMipmapsadd for each createVertexArray a variable in the global variables: let VAO: WebGLVertexArrayObject update the VAO and VBA syntax VAO = gl.createVertexArray(); let VBO = gl.createBuffer();
textures. for textures add variables to globals: let textureX: WebGLTexture; Add method to initialize texture with blue 1 pixel: function initTexture(gl: WebGL2RenderingContext, texture: WebGLTexture) {...
. Use loading an Image for filling the texture.
for each createProgram a variable in the global variables: let shaderProgram: WebGLProgram;
create the functions: function main()
: let main = function() {...}() remove the cleanup at the end of main(): delete of vertexarrays, buffers and the terminate() function render()
: replace while (!glfwWindowShouldClose(window)) with function render(); remove glfwSwapbuffers and glfwPollevents at end of render(). rename framebuffer_size_callback
to function framebufferSizeCallback(width, height) replace processInput(window)
with function processInput()
add requestAnimationFrame(render); at the end of main at the end of render (if needed) at the end of framebufferSizeCallback
update syntax buffers from C++ to typescript let vertices = new Float32Array([...]);
let indices = new Uint16Array([...]);
replace float number notation (--not-- in the shaders!) ([0-9])f
with $1
replace the error messages ^(.+) << "(.+)<< infoLog(.+)$
with console.log($2); return;
look out for places in the code where a 0 must be replaced with null e.g. gl.bindBuffer(gl.ARRAY_BUFFER, null
); and gl.bindVertexArray(null
);
now change calls to openGL that have gotten a different signature, for instance:
shaders. Fill index.js in shaders directory when shaders are in separate subdirectory /shaders/ with imports and export statement. search/replace (in all the files with shader code) \\n"$
with empty \\0
with empty
update vs shader code: remove "
symbols "#version 330 core"
-> export let vs_myname = `#version 300 es
update fs shader code: remove "
symbols "#version 330 core"
-> export let fs_myname = `#version 300 es precision mediump float;
etc.
math: replace glm:: vec3(
with vec3.fromValues(
. Replace glm:: mat4(1.0)
with mat4.create()
. Replace glm::
with empty
. Replace 'radians(...)' with '(...*Math.PI/180)'
I have made no method Shader.setMat4() or Shader.setVec3() for setting uniforms. Here we first will look for the uniformLocation and then fill the uniform e.g.: gl.uniformMatrix4fv(gl.getUniformLocation(shader.programId, "projection"), false, projection);
find a solution for the remaining problems.
The result of this procedure for the programs in part1 of LearnOpenGL can be seen in the files of project LearnOpenGl2_p1B. In the next section the remarks that goes with the translated programs can be seen.