This weekend I spent a lot of time on implementing the simple triangle example based on OpenGL ES in Go for my second milestone. There were again a few interesting revelations. As well as two simple bugs for which I had to invest extra hours.
But oh, the joy of seeing it work! Associated code can be found on GitHub.
The Gist
This nice triangle is now painted using the modern way of drawing primitives in OpenGL: Via buffers and shader programs. The Go code is environment agnostic (native or browser) and the binding code thin enough to flatten out only the environment specific differences.
Approaching the Implementation
Contrary to last time, this time I started with an example implementation in WebGL, since this is the more limiting environment. I added the missing OpenGL binding methods one after the other, recreating the example code in Go.
At peak times I had about four browser windows open, with several tabs each. The basic example is here, but I also had to filter a few JavaScript specialities. For instance, many examples store some helper variables on the WebGL objects, like on the buffer below:
function initBuffers() {
triangleVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
var vertices = [ ... ]; // a 3x3 float array
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
triangleVertexPositionBuffer.itemSize = 3;
triangleVertexPositionBuffer.numItems = 3;
...
Which, to the uninitiated like me, can be confusing whether this is a public API or not (Note: it isn't).
I then also found the quite handy reference cards from the Khronos group. They even were so nice to point out the few differences between WebGL and OpenGL ES 2.0.
Furthermore I compared the functions with the native wrapper from go-gl and finally always referred to the actual Khronos C-header file for names and function signatures.
First Hiccups
While mapping the official API functions to the WebGL wrapper of gopherjs, I encountered small problems.
One was with the result values of the GetProgramParameter() and GetShaderParameter() calls, which return an int. They are used to determine whether there exist compilation or linker problems, in which case they return (on the native level) either 1 or 0 as integers.
But the WebGL interface from the browser already returns a boolean in this case - and the gopherjs wrapper calls js.Object.Int() to convert it. This fails with the boolean "true" value and returns 0 in this case, indicating an error although everything is fine.
The binding code now calls the API function directly and converts a "true" to 1 for the result.
The second issue is with the function signature of UniformMatrix4fv(). This one is meant to expect an arbitrary number of matrices, not just one. The gopherjs wrapper limits it to be only one.
This is the second case where the binding code calls the browser method directly.
Note: Open an issue at least for the second problem.
Does Not Work
So, binding code looks good, value conversions in place, GetError(), planted everywhere, doesn't report any issue... And the canvas in the browser is still black. Why?
Now it's getting difficult. I always like it when the example code works - and your test code, which supposedly does the same thing, doesn't.
First I suspected an issue with the back-and-forth conversions of slices and interfaces which might mess up the JavaScript transpiler. So I opened up the JavaScript file and placed log outputs at my first suspected error causes - but all looked fine.
Then I wielded the power of the prototype language and modified the WebGL context prototype - to trace any function call and their parameters. My intention was to then compare the executions of my project and the example.
This is the code that I injected in both sources:
function wrapOpenGl() {
var name;
var entry;
function wrap(name) {
var old = WebGLRenderingContext.prototype[name];
WebGLRenderingContext.prototype[name] = function() {
console.log("--> called: " + name + " " + JSON.stringify(Array.prototype.slice.call(arguments)));
return old.apply(this, arguments);
}
}
for (name in WebGLRenderingContext.prototype) {
entry = WebGLRenderingContext.prototype[name];
if (typeof entry === "function") {
console.log("Wrapping: " + name);
wrap(name);
}
}
}
wrapOpenGl();
And then I could compare the results. Note that I could do this only in Chrome; Firefox doesn't enumerate over prototype functions. The main difference was with how I calculated the projection matrix.
I then found the culprit: I had the calculation of the projection matrix wrong. The example uses gl-matrix.js, a JavaScript library that I used in previous projects. For Go the best counterpart (currently active) that I could find so far is go3d. go3d provides a mat4.AssignPerspectiveProjection() function, which I mistook to be similar to the mat4.perspective() from gl-matrix.js (Link is from an older version that I knew of) .
Comparing the code I realized that the gl-matrix.js function is actually a wrapper over a method that then behaves like the one from go3d. And that extra wrapper magic was missing.
Incidentally, I then realized that I already had such a wrapper code right in the last version. But didn't connect the dots since the original OpenGL function was called glFrustum() - completely different to AssignPerspectiveProjection...
And voilĂ , I had a coloured triangle in the browser!
Still - Does Not Work
With the program now working, all that was missing was the native implementation. The methods there were quickly implemented. And the native window stayed black.
Now that's difficult. How to get behind the issue now? I tried the tool apitrace, which is precisely for such cases: To log OpenGL calls, similar to what I did in JavaScript. But that program didn't log anything - trace file of 0 bytes. And I concluded with the best option available: get some rest and consider the problem after a good sleep.
The next day, I tinkered a bit more until I realized that go-gl indeed does have an example code based on the new OpenGL methods. And I saw the issue immediately - something that I thought about for a moment the evening before, half-asleep: The one parameter of the API that is unused in the JavaScript binding had the wrong value. The size parameter for buffering data does not specify the amount of elements, but the amount of bytes!
Fixed that, and... Success! It's working!
In Conclusion
I learned more about the OpenGL interface, both old and new functions, found a way to trace OpenGL calls under JavaScript, and proofed once again that sleep is necessary for problem solving.
Next up I'll either tackle the still pending topic of inversion of control, or, what urges me more now: Think about automated testing and continuous integration. That triangle is nice, but each change must be verified in two environments already...
So, what are your magic moments of OpenGL programming? Leave a comment below and follow my progress of this project!