08/11/12

WebGL with GWT/Elemental, Going 3D


In a previous post we described how to setup a first program to use WebGL with GWT's Elemental,
library, now we will elaborate our code introducing some basic 3-D graphics.

The bad: WebGL has not support for 3D graphics.  "WebGL is a 2D library" (http://games.greggman.com/game/webgl-fundamentals/).

The good: we can provide projections operators to the graphics library and let the vertex shader(s) apply them to the data and we can, quite easily, provide a fragment shader that, using eventually values computed vertex-per-vertex by the vertex shader, will do some basic shading and lighting on  3-D objects.



DEMO


Of course we need a canvas in our DOM and a WebGLRenderingContext
     
  CanvasElement canvas = 
    Browser.getDocument().createCanvasElement();
  canvas.setWidth(640);
  canvas.setHeight(480);
  Browser.getDocument().getBody().appendChild(canvas);
  String ctxString = "experimental-webgl";
  WebGLRenderingContext ctx3d  =    
     (WebGLRenderingContext)canvas.getContext(ctxString);       

 ctx3d.viewport(0, 0, 640, 480);


We should configure the context to do depth test when drawing 

   ctx3d.enable(WebGLRenderingContext.DEPTH_TEST);
   ctx3d.depthFunc(WebGLRenderingContext.LEQUAL); 

Then we have to setup shaders (the so called 'program') and provide the model.

Shaders.

  WebGLShader vs  = 
    createShader(WebGLRenderingContext.VERTEX_SHADER,    
      Shaders.INSTANCE.vertexShader().getText(), ctx3d);
  WebGLShader fs = 

    createShader(WebGLRenderingContext.FRAGMENT_SHADER
      Shaders.INSTANCE.fragmentShader().getText(), ctx3d);
  WebGLProgram program = 

    createAndUseProgram(Arrays.asList(vs,fs), ctx3d);

(the code of the three 'create' methods is available in the source code attached to this post or in Going3D.java, the entrypoint class of our GWT project)

 We use the ClientBundle  Shaders to keep the shaders in separate files

public interface Shaders extends ClientBundle {
    public static Shaders INSTANCE = GWT.create(Shaders.class);
    @Source("vertexShader.txt")
    public TextResource vertexShader();
    @Source("fragmentShader.txt")
    public TextResource fragmentShader();

}



vertexShader.txt is

attribute highp vec3 aVertexNormal;
attribute highp vec3 aVertexPosition;

uniform highp mat4 uNormalMatrix;
uniform highp mat4 uMVMatrix;
uniform highp mat4 uPMatrix;

uniform highp vec3 uAmbientLight;
uniform highp vec3 uLightColor;
uniform highp vec3 uLightDirection;
  
varying highp vec3 vLighting;

void main(void) {
   gl_Position = uPMatrix*uMVMatrix*vec4(aVertexPosition, 1.0);
   
   highp vec4 transformedNormal = 
            normalize(uNormalMatrix * vec4(aVertexNormal, 0.0));
   highp float directional = 
           max(dot(transformedNormal.xyz, uLightDirection), 0.0);
   vLighting = uAmbientLight + (uLightColor * directional);
}

and fragmentShader.txt is simply

precision mediump float;
varying highp vec3 vLighting;

uniform vec4 uColor;

void main(void) {
  vec4 texelColor = uColor;
  gl_FragColor = vec4(texelColor.rgb * vLighting, texelColor.a);
}

Do not take 'my'  shaders as good shaders they just do what we need for this experiment, the net is full of samples you can use,  here is important just to see how uniforms, attributes and varying interact.

We have now to hook on uniforms and attributes.

        WebGLUniformLocation pMatrixUniform = 
           ctx3d.getUniformLocation(program, "uPMatrix");
        WebGLUniformLocation mvMatrixUniform = 

           ctx3d.getUniformLocation(program, "uMVMatrix");
        WebGLUniformLocation  nmMatrixUniform  = 

           ctx3d.getUniformLocation(program, "uNormalMatrix");
        WebGLUniformLocation colorUniform = 

           ctx3d.getUniformLocation(program, "uColor");

        WebGLUniformLocation ambientColorUniform = 

           ctx3d.getUniformLocation(program, "uAmbientLight");
        WebGLUniformLocation lightColorUniform = 

            ctx3d.getUniformLocation(program, "uLightColor");
        WebGLUniformLocation lightDirectionUniform = 

            ctx3d.getUniformLocation(program, "uLightDirection");

        int vertexPositionAttribute = 
            ctx3d.getAttribLocation(program, "aVertexPosition"); 
        int vertexNormalAttribute = 

            ctx3d.getAttribLocation(program, "aVertexNormal"); 

prepare buffers


  WebGLCube cube = new WebGLCube();

  WebGLBuffer verticesPositionsBuffer = ctx3d.createBuffer();
 

  ctx3d.bindBuffer(
         WebGLRenderingContext.ARRAY_BUFFER, 
         verticesPositionsBuffer);
  ctx3d.bufferData(WebGLRenderingContext.ARRAY_BUFFER, 

       createFloat32Array(cube.getVerticesArray()),  
       WebGLRenderingContext.STATIC_DRAW);

  ctx3d.vertexAttribPointer(vertexPositionAttribute, 3,                 WebGLRenderingContext.FLOAT, false, 0, 0);
  ctx3d.enableVertexAttribArray(vertexPositionAttribute);

  //note: buffer operations are referred to the 
  //currently binded buffer



  WebGLBuffer verticesNormalsBuffer = ctx3d.createBuffer();
  

  ctx3d.bindBuffer(WebGLRenderingContext.ARRAY_BUFFER, 
       verticesNormalsBuffer);
  ctx3d.bufferData(WebGLRenderingContext.ARRAY_BUFFER, 
       createFloat32Array(cube.getNormalsArray()),  
       WebGLRenderingContext.STATIC_DRAW);
 
ctx3d.vertexAttribPointer(vertexNormalAttribute, 3,
             WebGLRenderingContext.FLOAT, false, 0, 0);        
  ctx3d.enableVertexAttribArray(vertexNormalAttribute);


  WebGLBuffer indexesBuffer = ctx3d.createBuffer();   

  ctx3d.bindBuffer(WebGLRenderingContext.ELEMENT_ARRAY_BUFFER, 
        indexesBuffer);
  ctx3d.bufferData(WebGLRenderingContext.ELEMENT_ARRAY_BUFFER, 

        createUint16Array(cube.getIndexesArray()), 
        WebGLRenderingContext.STATIC_DRAW);

   //this is not an attribute ARRAY_BUFFER so we do not need

   //attributes stuff


  int numIndicies = cube.getNumIndices();


where the WebGLCube class is longer than interesting and  reads as

public class WebGLCube {
    private static final double[] vertices = {
            // Front face
            -1.0, -1.0,  2.0,
           ....
    };
    private static final double[] normals = {
        // Front face
        0.0, 0.0,  1.0,
        ...

    };
    private static final int[] triangles = {
            0, 1, 2,      0, 2, 3,    // Front face
           ...

     };
    private static final double[] texture_coords = {
            // Front face
            0.0, 0.0,
           ...

    };
    private static native 

       JsArrayOfNumber fromDoubleArray(double[] a) /*-{
        return a;
    }-*/;
    private static native 

       JsArrayOfInt fromIntArray(int[] a) /*-{
        return a;
    }-*/;   
    public JsArrayOfNumber getVerticesArray() {
        return fromDoubleArray(vertices);
    }
    public JsArrayOfInt getIndexesArray() {
        return fromIntArray(triangles);
    }
    public int getNumIndices() {
        return triangles.length;
    }
    public JsArrayOfNumber getTextureCoordinatesArray() {
        return fromDoubleArray(texture_coords);
    }
    public JsArrayOfNumber getNormalsArray() {
        return fromDoubleArray(normals);
    }
}



And finally (hopefully) uniforms initialization

ctx3d.uniformMatrix4fv(pMatrixUniform, false,
    createArrayOfFloat32(perspectiveMatrix));

ctx3d.uniformMatrix4fv(mvMatrixUniform, false,
    createArrayOfFloat32(modelViewMatrix));

ctx3d.uniformMatrix4fv(nmMatrixUniform, false,
            createArrayOfFloat32(normalTransformMatrix);


ctx3d.uniform4f(colorUniform, .8f,0f,0f,1f);
ctx3d.uniform3f(ambientColorUniform, .2f, .2f, .2f);
ctx3d.uniform3f(lightColorUniform, 0.8f, 0.8f, 0.8f);
ctx3d.uniform3f(lightDirectionUniform, 0.0f, 
       1.0f/(float)Math.sqrt(2.0),
         1.0f/(float)Math.sqrt(2.0));

and drawing

// we left this buffer binded above so this is not strictly needed
// but we left this line as a reminder
//ctx3d.bindBuffer(
//       WebGLRenderingContext.ELEMENT_ARRAY_BUFFER, 
//             indexesBuffer);
     
ctx3d.clear(
   WebGLRenderingContext.COLOR_BUFFER_BIT |
                WebGLRenderingContext.DEPTH_BUFFER_BIT);     

ctx3d.drawElements(WebGLRenderingContext.TRIANGLES, 
              numIndicies,
              WebGLRenderingContext.UNSIGNED_SHORT, 0);


  Now the last part: matrices.

perspectiveMatrix is the 4x4 matrix that 'moves the eye point to the infinity'
(code almost directly translated from https://github.com/toji/gl-matrix):


/*
* double[] perspective
* Generates a perspective projection matrix with the given bounds
*
Params:
fovy - scalar, vertical field of view
* aspect - scalar, aspect ratio. typically viewport width/height
* near, far - scalar, near and far bounds of the frustum
dest - Optional, mat4 frustum matrix will be written into
*
* Returns:
dest if specified, a new mat4 otherwise
*/
public static double[] 
      perspectiveMatrix(double fovy, double aspect, 
                          double near, double far) {
  double top = near*Math.tan(fovy*Math.PI / 360.0);
  double right = top*aspect;
  return frustumMatrix(-right, right, -top, top, near, far);
};



/*
* double[] frustum
* Generates a frustum matrix with the given bounds
*
* Params:
* left, right - scalar, left and right bounds of the frustum
* bottom, top - scalar, bottom and top bounds of the frustum
* near, far - scalar, near and far bounds of the frustum
* dest - Optional, mat4 frustum matrix will be written into
*
* Returns:
* dest if specified, a new mat4 otherwise
*/
public static double[] 
      frustumMatrix(double left, double right, 
                double bottom, double top, 
                  double near, double far) {

  double dest[] = new double[16];
  double rl = (right - left);
  double tb = (top - bottom);
  double fn = (far - near);
  dest[0] = (near*2.0) / rl;
  dest[1] = 0.0;
  dest[2] = 0.0;
  dest[3] = 0.0;
  dest[4] = 0.0;
  dest[5] = (near*2.0) / tb;
  dest[6] = 0.0;
  dest[7] = 0.0;
  dest[8] = (right + left) / rl;
  dest[9] = (top + bottom) / tb;
  dest[10] = -(far + near) / fn;
  dest[11] = -1.0;
  dest[12] = 0.0;
  dest[13] = 0.0;
  dest[14] = -(far*near*2.0) / fn;
  dest[15] = 0.0;
  return dest;
}



modelViewMatrix, transforms the model space to the eye space and is usually obtained composing the matrix that transforms the model space into the scene space (the transformation that places objects in the right position in the scene) and the so called lookAt matrix (still from matrix.js): 


/*
* double[] lookAt
* Generates a look-at matrix with the given eye position, focal point, and up axis
*
* Params:
* eye - vec3, position of the viewer
* center - vec3, point the viewer is looking at
* up - vec3 pointing "up"
* dest - Optional, mat4 frustum matrix will be written into
*
* Returns:
* dest if specified, a new mat4 otherwise
*/

public static  double[] lookaAtMatrix(double[] eye, double[]   
    center, double[] up) {
  double eyex = eye[0];
  double eyey = eye[1];
  double eyez = eye[2];
  double upx = up[0];
  double upy = up[1];
  double upz = up[2];

  double z0,z1,z2,x0,x1,x2,y0,y1,y2,len;

  z0 = eyex - center[0];
  z1 = eyey - center[1];
  z2 = eyez - center[2];

  len = 1.0/Math.sqrt(z0*z0 + z1*z1 + z2*z2);
  z0 *= len;
  z1 *= len;
  z2 *= len;

  x0 = upy*z2 - upz*z1;
  x1 = upz*z0 - upx*z2;
  x2 = upx*z1 - upy*z0;
  len = Math.sqrt(x0*x0 + x1*x1 + x2*x2);
  if (len==0) {
    x0 = 0;
    x1 = 0;
    x2 = 0;
  } else {
    len = 1.0/len;
    x0 *= len;
    x1 *= len;
    x2 *= len;
  }

  y0 = z1*x2 - z2*x1;
  y1 = z2*x0 - z0*x2;
  y2 = z0*x1 - z1*x0;

  len = Math.sqrt(y0*y0 + y1*y1 + y2*y2);
  if (len==0) {
    y0 = 0;
    y1 = 0;
    y2 = 0;
  } else {
    len = 1.0/len;
    y0 *= len;
    y1 *= len;
    y2 *= len;
  }

  double[] dest = new double[16];
  dest[0] = x0;
  dest[1] = y0;
  dest[2] = z0;
  dest[3] = 0.0;
  dest[4] = x1;
  dest[5] = y1;
  dest[6] = z1;
  dest[7] = 0.0;
  dest[8] = x2;
  dest[9] = y2;
  dest[10] = z2;
  dest[11] = 0.0;
  dest[12] = -(x0*eyex + x1*eyey + x2*eyez);
  dest[13] = -(y0*eyex + y1*eyey + y2*eyez);
  dest[14] = -(z0*eyex + z1*eyey + z2*eyez);
  dest[15] = 1.0;

  return dest;



normalTransformMatrix is the matrix that is used to transform normals (that live into model space) into eye space (where lights are given) and is the transpose of the inverse of the modelview matrix:


double[] normalTransformMatrix 
           =transpose(inverseMatrix(modelViewMatrix));


That's all !! (full source, well just the entrypoint class, use a new GWT project as created by the eclipse plugin as a skeleton if you want and do not forget to add elemental to build path as here, for instance) 

Of course once created a program that may render a cube it is quite straightforward  to change the shape as in the following picture where Wavefront-obj files are loaded ... but it is the subject of our next post ! 


sample shapes

Ciao,
   Alberto

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.