11/09/12

WebGL/GWT Fundamentals/Elemental

A couple of weeks ago we started experimenting with the new "to the metal" library of GWT 2.5 (https://developers.google.com/web-toolkit/articles/elemental) and, being computer graphics enthusiasts, we targeted WebGL as our main focus.

Learning deeply WebGL (WebGL - OpenGL ES 2.0 for the Web) was unfortunately a too time-consuming task four our purposse, so we decided to start translating some basic examples from javascript to GWT/elemental, just to understand how far Elemental will bring us.

We read carefully one of the best introductory articles on WebGL we found (WebGL Fundamentals), and we started coding.

Transliterating the first example was, as usual, the most hard task, actually our final "testOne.java" program reads as follows:


- - - Code 1 - - -
public void draw(WebGLRenderingContext ctx3d, CanvasElement canvas) {
   // setup a GLSL program
   WebGLShader vs  = 
    Utilities.createShader(WebGLRenderingContext.VERTEX_SHADER,
     "attribute vec2 a_position;void main() {gl_Position = vec4(a_position, 0, 1);}",
     ctx3d);
   WebGLShader fs = 
    Utilities.createShader(WebGLRenderingContext.FRAGMENT_SHADER,
     "void main() {gl_FragColor = vec4(0,1,1,1); }", ctx3d);
  
   WebGLProgram program = Utilities.createAndUseProgram(Arrays.asList(vs,fs), ctx3d);
   
   // look up where the vertex data needs to go.
   int positionLocation = ctx3d.getAttribLocation(program, "a_position");
   
   // Create a buffer and put a single clipspace rectangle in
   // it (2 triangles)
   WebGLBuffer vertexPositionBuffer = ctx3d.createBuffer();
   ctx3d.bindBuffer(WebGLRenderingContext.ARRAY_BUFFER, vertexPositionBuffer);
   ctx3d.bufferData(WebGLRenderingContext.ARRAY_BUFFER,    
          Utilities.createArrayBufferOfFloat32(
               -1.0,-1.0,
               1.0,-1.0,
               -1.0,1.0,
               -1.0,1.0,
               1.0,-1.0,
               1.0,1.0), WebGLRenderingContext.STATIC_DRAW);
   ctx3d.enableVertexAttribArray(positionLocation);
   ctx3d.vertexAttribPointer(positionLocation, 2, 
               WebGLRenderingContext.FLOAT, false, 0, 0);
   
   // draw
   ctx3d.drawArrays(WebGLRenderingContext.TRIANGLES, 0, 6);
}

where canvas and ctx3d are easly created (and pushed into the document) by
 CanvasElement canvas = Browser.getDocument().createCanvasElement();
 Browser.getDocument().getBody().appendChild(canvas);
 WebGLRenderingContext ctx3d  = 
     (WebGLRenderingContext)canvas.getContext("experimental-webgl");
 if (ctx3d == null)
 ctx3d = (WebGLRenderingContext)canvas.getContext("webgl");
 if (ctx3d == null) {
 Browser.getWindow().alert("WebGL not supported ?!?!?!");
 }

[demo, select example 1 from the top menu; it works with Chrome and Safari, we are still working to let Firefox to execute all the samples :(  it works with Chrome and Firefox, here the update]

The java code is quite similar to the original snippet on ‚"WebGL Fundamentals".
Most notably we chosen to provide to the system the code of the fragment and vertex shaders (lines XX and YY) as strings instead of placing the shaders in the html page and using dom to get back the text.
When shaders grow in size inline strings in java are a mess (no multilne strings at all :() but we can easily use ClientBundle's TextResource in GWT to let the compiler to embed shaders in the compiled code and leave the shaders in separate files, that seems a better choice (see next examples).

Using a TextResource has the advantage of having the resource available syncronously (so we do not need to handle the asyncronous loading of the shader) that helps keeping the example simple but has the drawback of forcing a compilation when for any change in the shaders code.


Actually ‚"Code 1" cannot be used without some helper functions. A few are very similar to the helper functions used in the html5rocks post (i.e. createShaderFromScriptElement and createProgram are not part of WebGL but are defined in an external js file included in the examples,).

Our implementation is:
public static WebGLShader createShader(int type,String code,WebGLRenderingContext ctx) {
  WebGLShader shader = ctx.createShader(type);
  ctx.shaderSource(shader,code );
  ctx.compileShader(shader);
  testShaderStatus(ctx,shader);
  return shader;
}
and
  public static WebGLProgram createAndUseProgram(List shaders, WebGLRenderingContext ctx) {
    WebGLProgram program = ctx.createProgram();
    for(WebGLShader s : shaders)
      ctx.attachShader(program, s);
    ctx.linkProgram(program);
    testProgramStatus(ctx,program);
    ctx.useProgram(program);
    return program;
  }
More interesting for the Elemental's user we had to write an helper function to create the Float32Array at line XXYYZZ, in fact bufferData has a quite obscure signature:


void elemental.html.WebGLRenderingContext
.bufferData(int target, ArrayBufferView data, int usage)


target and usage are simply available as static constants in the context but creating an ArrayBufferView appears not possible using "just java" (well we did'nt found how to but if someone has exeperience please let us know).

ArrayBufferView itself is an interface and Float32Array too is an interface implementing ArrayBufferView.
Maybe there is a better choice but our workaround was simply:
private native static ArrayBufferView 
      createFloat32ArrayBuffer(JsArrayOfNumber vertices) /*-{
  return new Float32Array(vertices);
}-*/;
Elemental's JsArrayOf* are very helpful here so we can use arrays in jsni. To let us to create ArrayBufferView easily we added another helper:
 public static ArrayBufferView
      createArrayBufferOfFloat32(double ... nums) {
   JsArrayOfNumber vertices = JsArrayOfNumber.create();

    for(double d: nums)
      vertices.push(d);
    return Utilities.createFloat32ArrayBuffer(vertices);
}
that is more confortable to call from java.

 That's all for the first example ‚ "Not very exciting :-p", so we proceeded with more interesting samples.


The rectangles test is straightforward with Utilities in place.


 public void run( WebGLRenderingContext ctx3d, CanvasElement canvas) {
   WebGLShader vs = Utilities.createShader(WebGLRenderingContext.VERTEX_SHADER,    
      Shaders.INSTANCE.vertexShader2D().getText(), ctx3d);
   WebGLShader fs = Utilities.createShader(WebGLRenderingContext.FRAGMENT_SHADER, 
      Shaders.INSTANCE.fragmentShader2D().getText(), ctx3d);
   WebGLProgram program = Utilities.createAndUseProgram(Arrays.asList(vs,fs), ctx3d);

   int positionLocation = ctx3d.getAttribLocation(program, "a_position");

   //Uniforms are perfectly mapped to the java WebGLUniformLocation cless

   WebGLUniformLocation resolutionLocation = 
       ctx3d.getUniformLocation(program, "u_resolution");

   ctx3d.uniform2f(resolutionLocation, canvas.getWidth(), canvas.getHeight());

   WebGLBuffer vertexPositionBuffer = ctx3d.createBuffer();

   ctx3d.bindBuffer(WebGLRenderingContext.ARRAY_BUFFER, vertexPositionBuffer);

   ctx3d.enableVertexAttribArray(positionLocation);

   ctx3d.vertexAttribPointer(positionLocation, 2, 
       WebGLRenderingContext.FLOAT, false, 0, 0);

   WebGLUniformLocation colorLocation = ctx3d.getUniformLocation(program, "u_color");

   // draw 50 random rectangles in random colors
   for (int ii = 0; ii < 50; ++ii) {
   
   // Setup a random rectangle

    ArrayBufferView rect = createRectangleVertices(randomInt(300), 
                        randomInt(300), randomInt(300), randomInt(300));

    ctx3d.bufferData(WebGLRenderingContext.ARRAY_BUFFER, rect,  
          WebGLRenderingContext.STATIC_DRAW);

    // Set a random color.

    ctx3d.uniform4f(colorLocation, 
        (float)Math.random(), (float)Math.random(), (float)Math.random(), 1);

    // Draw the rectangle.
    ctx3d.drawArrays(WebGLRenderingContext.TRIANGLES, 0, 6);

   }
 } 

private static ArrayBufferView 
       createRectangleVertices(int x, int y, int w, int h) {
  int x1 = x;
  int x2 = x + w;
  int y1 = y;
  int y2 = y + h;
  return Utilities.createArrayBufferOfFloat32(x1,y1,x2,y1,x1,y2,x1,y2,x2,y1,x2,y2);
}

private int randomInt(int range) {
  return (int) Math.floor(Math.random() * range);
}

[demo, select Exampe 2 from the menu]

A little bit more interesting is the image drawing example where we have to handle the asyncronous loading of the external resource. Well, using Elemental's ImagElement and an EventListner we can follow the same pattern we will follow in javascript (actually tis is what Elemental can provide to GWT developers, an api similar if not idential to the javascript one for many browser features):
 
 ...
SafeUri uri= Images.INSTANCE.img().getSafeUri();

final ImageElement imgElement = Browser.getDocument().createImageElement();
Browser.getDocument().getBody().appendChild(imgElement);
imgElement.setSrc(uri.asString());

imgElement.setOnload( new EventListener() {
  @Override
  public void handleEvent(elemental.events.Event evt) {
  imgElement.setHidden(true);
  run_afterSetup(imgElement);
  }
});

public void run_afterSetup(ImageElement imageData) {
  //Create shaders ecc ...
  // Create a texture.
  WebGLTexture texture = ctx3d.createTexture();
  ctx3d.bindTexture(WebGLRenderingContext.TEXTURE_2D, texture);
  // Set the parameters so we can render any size image.
  ctx3d.texParameteri(WebGLRenderingContext.TEXTURE_2D,   WebGLRenderingContext.TEXTURE_WRAP_S, WebGLRenderingContext.CLAMP_TO_EDGE);
  //... identical to the javascript code snippet
  // Upload the image into the texture.

ctx3d.texImage2D(WebGLRenderingContext.TEXTURE_2D, 0, WebGLRenderingContext.RGBA, WebGLRenderingContext.RGBA, WebGLRenderingContext.UNSIGNED_BYTE, imageData);

[demo, select example 3 from the top menu]


Even if with some kludge, possibly due just to lack of documentation of elemental, working with WebGL in GWT appear very close to using javascript, with the advantage (our opinion, do not flame us please) of being able to write the code in java and, maybe, reuse our algorithms. 
 
 Next ... 3D and animations (well, some are in the demo page but we will describe the remaining samples in a future post).

Ciao e grazie, 
      Alberto e Francesca (jooink.com) 

The content of this post is licensed under the Creative Commons Attribution 3.0 License, and code samples are licensed under the Apache 2.0 License.