WebGL shader experiment

thumbnail-webglraytracer

For a while we have been reading about WebGL and decided to start a small experiment. WebGL is a Javascript API, based on OpenGL ES, for interactive 2D & 3D within a compatible web browser. Unfortunately not all browsers and platforms support WebGL properly. IE doesn’t support it at all, Google Chrome and Firefox do support it, but as the experiment shows, the last 2 browsers also have some issues.

On chromeexperiments you can see lots of web 3D experiments, but most of them do not use complex shaders. So we had some OpenGL 3 shaders and decided to convert them to WebGL. WebGL is a subset of OpenGL. Without reading the specification in depth, our first impression was that not all OpenGL and GLSL functions are available in WebGL. We started with a realtime raytracer, where the raytracing is completely calculated in the fragment shader.

Challenge 1: Loops

The first thing we stumbled upon were for and while loops. It seems that there are limited possibilities when using loops. WebGL has to know beforehand how many loops will be calculated.

The following OpenGL shader code:
while (distance < max_distance && ! hit) { ... }

will give the following error message when you compile it:
ERROR: 0:37: 'while' : This type of loop is not allowed

WebGL follows the OpenGL ES specification. This specification is designed to produce same results on desktop and mobile devices. Not sure why but there are some rules when creating for and while loops.

  • one loop index
  • index has type int or float
  • for statement must have the form
    for (type_specifier identifier = constant_expression ; loop_index operation constant_expression ; loop_expression ) 

    • where operation is
      > >= < <= == or !=
    • loop_expression is of the form
      • loop_index++
      • loop_index--
      • loop_index += constant_expression
      • loop_index -= constant_expression

Also be aware that constants can be:

  • a literal value (e.g., 5 or true)
  • a global or local variable qualified as const excluding function parameters
  • an expression formed by an operator on operands that are constant
  • expressions, including getting an element of a constant vector or a constant matrix, or a field of a constant structure
  • a constructor whose arguments are all constant expressions
  • a built-in function call whose arguments are all constant expressions, with the exception of the texture lookup functions.

Basically this means that the compiler wants to know how many loops it can expect. In order to get our shader to work we did the following trick.

Gives error:
for (float dist=threshold2; dist < md; ) {...} //dist is modified

No error:
float dist = threshold2;
for (int i = 0 ; i < 64 ; i ++) { if (dist >= md) { break; }
...
}

This seems to be a good way to make sure that you will never write an endless loop in a shader. At forehand the developer needs to decide how many times a loop can take place. This can limit some algorithms, but most of the time this works out well.

Challenge 2: Windows

Our realtime raytracer seemed to work fine on Google Chrome and Firefox on Linux, Mac OS and Android. Unfortunately it doesn’t on the Windows platform. It takes Firefox ages of time to compile the shader and to generate the result, while Google Chrome constantly looses its GL- context. On Linux and MacOs the shaders are compiled within a second.

Windows uses HLSL by default. Both Google Chrome and Firefox use ANGLE, an API, developed by Google, that translates OpenGL calls to DirectX and generates HLSL. It seems that unrolling the loops creates huge shaders and increasing compile time on Windows. It’s quite obvious that something goes wrong in the translations. You can bypass that and use OpenGL on Windows by changing some configurations in Firefox (about:config -> change setting webgl.prefer-native-gl to true) or Chrome (start Chrome with the option —use-gl=desktop). This solved our problems with Chrome.

Splitting up shaders: lazy compilation

It was obvious that on Windows the shader was the bottleneck, as it contains many for loops. So we tried splitting up the shader into multiple smaller shaders. The shader is only compiled when the corresponding pass is selected. This seemed to improve the performance.

The final result of the shader can be viewed here. It also works on mobile devices, but is best viewed on Firefox on a Linux or Mac OS system with a good Video-card. On Windows you’ll have to start Chrome with the —use-gl=desktop option.

Note 1: GPU are not capable of doing recursive functions
Note 2: Raytracing is a recursive algorithm

Comments are closed.