mardi 29 mai 2012

Improvements on plyrenderer

Since the last post, a few improvements have been made that either improve the performances either add added values.

Let me try to describe some of the most useful that can be added easily in other projects... 

First, you can see here an example of a 3d model of an hand skeleton. You can go here for the application only: http://plyrenderer.appspot.com?ply=hand&normal=true



Browser cache

As you can see on the sample above, there are more than 100'000 points on the picture. This is a huge number and downloading them every time from the server is cumbersome as it uses a lot of bandwidth.

In order to avoid it, I store the scene in the browser HTML5 storage in order to retrieve it locally if the page is reloaded. How does it work?

The modification in order to get this feature are the following:


  • Change the scene download in order to support chunks
  • Check if the HTML5 storage is available
  • Store and load the data in the HTML5

Split the scene

The first modification to perform is that the scene has to be split in chunks. When the browser starts the GWT application, it does not download the whole scene anymore, but will ask for the scene information which contains:
  • the number of points
  • the chunk size
When done, the browser, will download multiple chunks in parallel to fill the scene. As the download process can be very long, I decided to render the scene from time to time. Please note that the scene can not be rotated, translated or zoomed during the scene creation. It is a design decision.

Check if HTML5 storage is available

On recent browser, HTML5 storage is often enabled but do not expect to store thousands of GB, the limit is usually around 5MB. So browsers will kindly propose the user to increase this bound, other will trigger an exception. 

To check if the HTML5 storage is available. You have the following code in GWT:

storage = Storage.getLocalStorageIfSupported();

If the local storage is not supported, then the storage object would be null and you can trigger an exception. For plyrenderer, I decided to continue the application. The storage would not be able to store or load and thus would always download the scene.

Store and load the data

Here it becomes interesting. Now that we have chunks and storage available, we can start to use it in order to store the points for future usage...

When a batch of points has been downloaded, before rendering it, I store it directly into the HTML5 storage...


            public void onSuccess(Point[] result) {
                //Let's save the points in the storage...
                StringBuilder builder = new StringBuilder("[");
                for (Point p : result) {
                    builder.append(p.toJson());
                    builder.append(",");
                }
                builder.append("]");
                try {
                    storage.save(POINTS + offset, builder.toString());
                } catch (Exception e) {
                    logger.warning("Impossible to save: probably out of memory");

                }          

Let me describe this code:
  • The function onSuccess is a callback that is called when the points have been successfully downloaded from the server
  • A StringBuilder is created in order to convert the points in JSON format. Why? Because the storage only supports to store data as string. I am lazy: JSON is easy to generate and more important, it is easy to parse with JSNI
  • Last but not least, the data is stored in the database, please note that you may get an exception because the memory has exceeded, so we catch the exception, display it and continue... The storage is a key/value storage system, the function save accepts two arguments: the key as a string and the value as a string.

Now, that the data has been stored, what will happen next time we restart the application. The code must check if the data has already been stored and load it from cache instead from server. Here is an example:


                    String pointsString = storage.load(POINTS + offset);
                    if (pointsString != null) {
                        JsArray<JsonPoint> points = asJSPointArray(pointsString);
                        for (int i = 0; i < points.length(); i++) {
                            JsonPoint p = points.get(i);
                            cloud.addPoint(new Point(p.getX(), p.getY(), p.getZ(), ...));
                        }
                        update(numPoints);
                    } else {
                        downloadPoints(numPoints, offset);
                    }

The algorithm is the following:

  1. Check the points exists
  2. if they exists, convert the string in a JSArray
  3. If not download the points
As the data is stored in a JSON format, it becomes easy to load it with JSNI... The function asJSPointArray is the following:


private static native JsArray<JsonPoint> asJSPointArray(String json) /*-{
eval('var res = ' + json);
return res;
}-*/;


It receives a string, call the "eval" javascript function to convert it into an object and returns it to the java layer...

You have probably seen that we need to perform an extra step: convert the JSonPoint parsed into Point... I have not found good solution yet but to parse JSON in GWT, a class must extends JavaScriptObject which is not supported on the server side, that is why objects loaded from a cache and downloaded are not the same.

The code to parse the JSON string is thanks to JSNI incredibly simple:


    private static class JsonPoint extends JavaScriptObject {
        protected JsonPoint() {
        }
        public native final double getX() /*-{
return this.x;
}-*/;
        public native final int getRed() /*-{
return this.r;
}-*/;
        public native final double getNX() /*-{
return this.nx;
}-*/;
...
    }

That's all, we have now a mechanism to store and load data from the HTML5 storage.

AnimationScheduler

A limitation that occurs when the data is loaded is that javascript is not multi-threaded. What does it mean, when the points are loaded from the HTML5 storage, the javascript thread is busy and thus updates to the canvas will not be repainted and you will not see any animation during refresh...

In order to avoid it, javascript provides a useful function called requestAnimationFrame(). The same exists in GWT in the class AnimationScheduler...


        private class MyCallback implements AnimationScheduler.AnimationCallback {
            public void execute(double timestamp) {
                int simultaneousQueries = 0;
                long start = System.currentTimeMillis();
                for (; offset < numPoints; offset += chunkSize) {
                    
                     ...LOAD OR DOWNLOAD THE POINTS...
                    //If the time is more than 1 second, let's stop and ask for an animation frame
                    if (System.currentTimeMillis() - start > REFRESH_MILLISECONDS || simultaneousQueries > MAX_SIMULTANEOUS_QUERIES) {
                        offset += chunkSize;
                        if (offset < numPoints) {
                            AnimationScheduler.get().requestAnimationFrame(this);
                            break;
                        }
                    }
                }
            }
        }

What happens here...
  • The class MyCallback has a function called execute that will be called when the javascript thread has free time slot and could run a CPU intensive task
  • The execute functions will load or download all the points in the scene but if 2 seconds have been spent in the function, it asks to for another animation frame and leave the function. So the javascript thread will have time to display the canvas and will load the function afterward...