The Life of a Programmer

JavaScript and 3d graphics don’t mix

Writing WebGL code in JavaScript has been very frustrating. There are numerous aspects of the language which make this task harder than it should be. Lack of typing, no operator overload, and by reference assignment are the three major issues. Performance is also a factor, but I understood that one going in.

I’m using the three.js library for my WebGL coding. My criticism in this article is aimed at JavaScript itself, not this fine library. As comparison I’ve also done a bit of OpenGL work in Java and C++. Some of the issues exist in Java as well.

By reference assignment

I’ve done a lot of coding of JavaScript and haven’t really been bothered by the reference assignments until now. It generally made sense to be dealing with high-level objects by reference. The problem with math is that I don’t consider vectors and matrices to be high-level objects. In the context of graphics these are fundamentals.

Several defects in my code result from failing to take a copy of a variable. Simple assignment is sometimes to blame, though function parameters are more problematic. Consider a common example of creating a world object with a helper function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function place_new_object( where ) {
    //make some object
    var object = create_object()

    var wrap = {
        object: object,
        position: where,
    }

    scene.add(wrap)
    return wrap
}

The defect is not obvious to see, even though I know it is there. The assignment of position in the map is at fault. The caller is expecting the current value of position to be used, instead the new object binds itself by reference and thus shares a position value. These two common examples show how easily this breaks the application.

1
2
3
4
5
6
7
// create several new objects into the distance
var p = new Vector3(100,100,0)
var group = []
for( i=0; i < 5; ++i ) {
    p.z += 100
    group.push( place_new_object(p) )
}
1
2
//clone an object where it is now
var n = place_new_object( cur_object.position )

place_new_object needs to explicitly clone the position: position: where.clone(). It’s easy to forget since it’s not obvious. Making matters worse, JavaScript doesn’t actually have a built-in clone function. Each value could end up being cloned in a different fashion, depending on the library — assuming a clone function has even been provided.

Lack of type safety

One of the key features of dynamic languages is also a significant problem: the ability to assign anything to a variable. It isn’t unique to JavaScript, though in my work with WebGL it’s becoming a significant hurdle. I accidentally assign a Vector3 when a Vector4 is required, or I assign an object of {x,y,z} where a Vector3 is required. Since many operations can actually work on the wrong type (Γ  la duck typing), the error may not occur anywhere close to the incorrect assignment. Instead I’m left perplexed with an error deep in the library code.

Perhaps the problem is more pronounced in graphics. A lot of similar data types are used, and must be mixed within an expression. 2, 3, and 4 dimensional vectors, along with corresponding matrices, are all required to produce a geometry. Rotations can be represented with both euler angles or quaternions. A color may be a strict color type, or a vector in RGB colorspace. Even very basic code has no choice but to use a lot of very similar types.

A lack of type safety also complicates the use of objects. It’s too easy to assign to a member when instead you should be calling set or assigning to a sub-field. This can be seen with uniforms in three.js. The uniform is an object containing some meta-data, as well as the value.

1
2
//update the angle of a shader effect
material.uniforms['angle'] = nextAngle(time)

Far away from this code an error will be generated. Instead of setting the value of the uniform I’ve completely replaced the uniform object with a floating point value! I should have set the value field instead.

1
material.uniforms['angle'].value = nextAngle(time)

These types of problems sour my attitude towards dynamic typing. On this project it has absolutely not been convenient. I’ve wasted a fair amount of time tracking down incorrect type assignments. It’s something that even very basic type declarations would prevent.

No (operator) overloading

Linear algebra uses common operations like addition and multiplication. It is natural to write the multiplication of a vector by a matrix as M * V. With OpenGL the use of vectors and matrices in expressions is as common as integers and floating point. A lack of operators leads to a loss of clarity.

Also see my article The Evil and Joy of Overloading

1
2
3
4
5
//JavaScript
var next = perp.applyMatrix4( trans ).add( from )

//C++, GLSL
vec4 next = trans * perp + from

It’s often actually worse. three.js is optimized for not creating new objects on all operations since this is actually quite expensive in JavaScript. Like with by reference assignments one must explicitly clone.

1
var next = perp.clone().applyMatrix4( trans ).add( from )

One could argue this is a library issue; the operations could create new values. Here is where JavaScript’s performance becomes a major issue. If every operation allocated new arrays the memory manager would quickly trip over itself and die. In C++ one does however just return copies. There we can rely on proper optimizations that make it cheap to do so.

The lack of types, and thus overloads on types also makes it difficult to write generic utility functions. For example, there is no way to write a generic dot-product function that operates on different vector sizes. Each size requires its own version. It is possible to store the dimensions of vectors and matrices in their respective objects, and make a generic function, but then performance again becomes a major issue.

C programmers might not have a problem with this, but at least in C you can’t accidentally call the wrong version of the function. Lacking type safety in JavaScript it’s easy to call the wrong function, for example multiply3 when you meant multiply4. Worse, it’s even possible to just not notice. A function which expects a Vector2 will gladly accept at a Vector3 since it has enough matching fields. It won’t do the correct calculation, but it will merrily proceed as though everything is fine.

Going Forward

I think I do need to mention performance, though it’s not the aspect that bothers me the most, it certainly doesn’t help anything. Even after managing to get the code working it just won’t perform very well in JavaScript. The lack of typing and value semantics makes the job of the optimizer too difficult. Even for minimal graphics code there is a definite need to optimize. It’s not like it’s just a tad slower than C++ either, it’s unbelievably slower. A lot of graphics algorithms, and thus applications, are simply not possible if they have to be done in JavaScript.

I looked at the emscripten project and asm.js. This uses the LLVM toolchain to compile C/C++ code into JavaScript and then execute it efficiently. What comes out of this basically signals the death of JavaScript in this arena. There’s simply no value in coding in a language which is both harder and slower. Once this toolchain matures a bit I will definitely go only this route for my graphics code. Indeed, it’s likely my next avenue of pursuit for Leaf: compiling to asm.js.

Please join me on Discord to discuss, or ping me on Mastadon.

JavaScript and 3d graphics don’t mix

A Harmony of People. Code That Runs the World. And the Individual Behind the Keyboard.

Mailing List

Signup to my mailing list to get notified of each article I publish.

Recent Posts