суббота, 28 декабря 2013 г.

Javascript: Plotting Rössler Attractor on HTML5 Canvas in 3D

In this blog I demonstrated how one can programmatically explore several chaotic systems: logistic map, Barnsley fern and Lorenz system. While doing this I focused mainly on producing plots or, at least, generating trajectories of these systems with different programming languages. At the same time, on my personal web page I have a section fully devoted to this same topic, where one can plot trajectories of simple chaotic systems right in their browser. Until recently I had only 1- and 2-dimensional systems, but last week I added the Rössler attractor and in this post I am going to show how one can easily produce 3-dimensional plots using only HTML5 canvas and a very handy three.js library.

Creating 3D images in the browser is quite different from plotting something in 2D – since HTML5 we have the canvas element, which allows one to draw whatever they want on a plane. On the other side, in case 3 spatial coordinates are required there are several alternatives, which might look less friendly. The first thing that comes to mind is WebGL, although there are problems with it – most notably, this technology is not supported by some browsers. Since we are dealing with quite a simple issue – we need neither complex coordinate transformations nor shaders for making graphs glamorous – imposing WebGL’s strict constraints on clients is a bit of an overkill. Another option is to implement the mechanism of transforming 3D coordinates of the model into 2D canvas coordinates ourselves. Even though I love to build something that was created by others long before, implementing a part of WebGL myself with a purpose of merely producing some plots is not a good idea as well. Fortunately, there is the third option – that is to use a Javascript library which helps to render 3-dimensional objects in the traditional canvas element. Three.js is a library of exactly this kind. To be more precise, it includes quite a few renderers allowing to use different lower level technologies to produce images in the browser, but we will try the CanvasRenderer only.

Let’s say a couple words about what we are actually going to render. Rössler attractor is another chaotic system of three differential equations. The same way as with Lorenz attractor we will bring the attractor to the discrete space by means of finite difference method. What makes me want to plot the trajectories of the Rössler system is their interesting form. If we take a long enough trajectory it will look like a closed tape in the XY-plane with one section of it leaving the plane to rise to much higher values of Z-coordinate and then returning to the original plane. An interesting detail is that with some values of parameters the trajectory looks like a more or less simple tape, but with other values it gets somewhat reminiscent of the Möebius strip due to strange bents in its structure. To see this we need to actually produce the plots, so let us stop talking and get to programming.

At the first stage we can leave the chaotic systems aside and set up the routines required to produce 3-dimensional lines on the canvas. For me this exercise was particularly interesting because creating a plotter is a good use-case to try some object-oriented Javascript. Our Plotter3d object will encapsulate the details of talking to three.js library required to produce graphs and here it is the constructor:

function Plotter3d(canvasElement) {
    this._scene = new THREE.Scene();

    var w = canvasElement.width * 0.1;
    var h = canvasElement.height * 0.1;
    this._camera = new THREE.OrthographicCamera(
        -w/2, w/2, h/2, -h/2, -100.01, 5000);
    this._camera.position.z = 20.0;

    this._renderer = new THREE.CanvasRenderer({ canvas : canvasElement });
    this._renderer.setSize(canvasElement.width, canvasElement.height);

    this._plots = [];
};

The code above sets up the scene, camera and renderer, which are the basic objects responsible for creating the image on the screen. The scene in our case serves as a simple container for the rendered objects. Three.js actually makes our life so simple that to create a fully functional scene we don’t do any tuning and need only to instantiate the object. The process of setting up a camera is a bit trickier. First of all, there are two key types of camera available in three.js: PerspectiveCamera and OrthographicCamera. Since we are plotting trajectories of a mathematical system and not a real 3D scene, we can hardly make any use of perspective – thus, the OrthographicCamera is our choice. To set it up we need to specify 6 numbers: 4 for the left, right, top and bottom boundaries of its viewport and 2 for the positions of the near and far clip planes. Since we use a canvas as a rendering target, the bounds of the viewport are most naturally determined with its width and height. As for the positions of clipping planes, we just need to specify the figures, which will allow us to see the entire trajectories – that is no single section of a trajectory should fall behind the planes. (In addition to basic instantiation of the camera, I also move it along the Z-axis – with the orthographic projection this does not change anything, but this is kind of legacy from my first attempts with PerspectiveCamera.)  If you want more details on the orthographic camera check out this Udacity video lesson and three.js documentation. Finally, we have to create a renderer passing it the canvas element and setting its size based on the size of the latter. One last step is to add a member variable to store the trajectories, which are displayed by the plotter so that we can alter them if needed – _plots array will be responsible for that. After these preparation steps we can actually add some objects to the scene.

function ptsToGeometry(points) {
    var g = new THREE.Geometry();

    for(i = 0; i < points.length; ++i) {
        p = points[i];
        v = new THREE.Vector3( p[0], p[1], p[2] );
        g.vertices.push(v);
    }
    return g;
}

function createLine(points) {
    var g = ptsToGeometry(points);
    var m = new THREE.LineBasicMaterial( { color : 0xff0000 } );
    var line = new THREE.Line(g, m);
    return line;
}

Plotter3d.prototype.addPlot = function(points,color) {
    var l = createLine(points,color);

    this._plots.push(l);
    this._scene.add(l);

    this._renderer.render(this._scene,this._camera);
};

First, we want plotter to take a sequence of 3-dimensional points and display a connected line. This means that the addPlot method should take one argument – an array of arrays. Internally though the things are slightly more complicated. To make our points renderable we have to transform them to the form, which three.js will understand. To represent a set of vertices the library uses the notion of Geometry, which holds a collection of 3D vectors – points. It may also contain additional stuff like normals and other useful things, but we don’t need them now. This said, we transform each of our points from a simple array to a Vector3 object and add the latter to a new instance of Geometry object – that’s what ptsToGeometry function is responsible for. However, this is only a part of the story because to render something three.js needs not only vertices but also some idea about the way they are connected to each other.

In our case, this information is communicated by means of the Line. By default three.js treats Line objects as those representing a line strip – a mere sequence of line segments connecting the vertices from the first to the last one. Beside the information on how to treat the underlying geometry, Line also holds a material instance, which informs the renderer on some visual aspects of our line. We will use the LineBasicMaterial created with a single parameter – color. Actually, materials are more powerful than just something storing color information, but for the purpose of plotting graphs being able to set line colors is just enough. Now, the createLine function does the following things: calls ptsToGeometry to transform an array of arrays into an instance of Geometry, sets up a simple material and constructs the Line object, which combines the two. This function is called from the Plotter3d.addPlot method that stores the resulting Line in the plotter’s collection of graphs, adds it to the scene and asks renderer to update the scene – it’s that simple. Next thing to do is to create a trajectory of the Rössler system.

function nextRossler(p,a,b,c,dt) {
    var x = p[0];
    var y = p[1];
    var z = p[2];

    var nx = x - dt * (y + z);
    var ny = y + dt * (x + a*y);
    var nz = z + dt * (b + z * (x - c));

    return [nx,ny,nz];     
}

function rosslerGenerator(a,b,c,dt) {
    var r = function(p) {
        return nextRossler(p,a,b,c,dt);
    };
    return r;
}

function trajectory(g,n,p0) {
    var ps = [p0];
    for(i = 0; i < n; ++i) {
        ps.push(g(ps[i]));
    }
    return ps;
}

The nextRossler function above generates a point on the trajectory using the same method of finite differences that I described in the post about Lorenz system. Because it actually represents the Rössler system approximation we have to feed it three parameters of the system (a, b and c) and the dt discretization parameter. Moreover, to obtain some point we have to provide our function the preceding one. To avoid the need to manually handle all these arguments on each step of the trajectory we have another routine – rosslerGenerator, which takes the parameters once and returns a function wrapping the nextRossler with these arguments. Finally, we use the trajectory function to produce a sequence of points from a single starting point. Its first parameter is supposed to be a function returned by rosslerGenerator and the second one is the origin of the trajectory. Now we are only one step away from seeing some chaos in the browser – the only thing that we lack is the HTML page.

<!DOCTYPE html>
<html>
<head>

<link rel="stylesheet" type="text/css" href="style.css">

<script src="three.min.js"></script>
<script src="jquery-1.10.2.min.js"></script>
<script src="rossler.js"></script>
<script src="Plotter3d.js"></script>
<script>

    plotter = null;

    $(document).ready(function() {
        plotter = new Plotter3d($('#plotter')[0]);
    });

    function addPlot() {
        var r = rosslerGenerator(
            getp("#a"),getp("#b"),getp("#c"),getp("#dt"));
        var ps = trajectory(
            r,getp("#l"),[getp("#x0"),getp("#y0"),getp("#z0")]);
        plotter.addPlot(ps);
    }

    function getp(n) {
        return parseFloat($(n).val());
    }

</script>
</head>

    <body>

        <canvas id="plotter" width="600" height="400">
        </canvas>

        <div>
            <input type="text" id="a" value="0.2" />
            <input type="text" id="b" value="0.2" />
            <input type="text" id="c" value="5.7" />
            <br />
            <input type="text" id="x0" value="-10.0" />
            <input type="text" id="y0" value="0.0" />
            <input type="text" id="z0" value="0.0" />
            <br />
            <input type="text" id="l" value="1000"/>
            <input type="text" id="dt" value="0.05"/>
            <button onclick='addPlot();' >Go!</button>
            <br />
        </div>
    </body>
</html>

The HTML is fairly simple – we only have a canvas and a number of inputs for Rössler system parameters and initial conditions. To collect the values from these inputs I use a trivial getp function, which basically wraps the jQuery selector and parses the input’s value into a floating point number. Another important component is the addPlot function that handles clicks on the Plot button. Its purpose is to collect the parameters, create a proper Rössler system model, use it to produce a trajectory and finally ask the Plotter3d instance to render it. The only thing that I didn’t cover yet is creation of the plotter. I do it right after the page is loaded so that the canvas element can be passed to its constructor. Here it is: the plot of the Rössler attractor in the browser!


Wait, didn’t we miss something? Right at the beginning of this post I promised that we will plot the Rössler trajectory in 3D, while what we see now doesn’t really look like a 3-dimensional plot. The problem, though, is easily identified: we just look on our plot from top to bottom and in this case we can’t see anything else than the projection of the trajectory on the XY plane. Let’s go back to the Plotter3d constructor and rotate our camera a bit.

function Plotter3d(canvasElement) {
    //...

    this._camera = new THREE.OrthographicCamera( 
        -w/2, w/2, h/2, -h/2, -100.01, 5000);
    this._camera.rotation.x = 3.14 / 3;

    //...
};

If we try to plot something without further changes we’ll notice that the trajectory, which does look like 3-dimensional plot thanks to the rotation, now fails to fit into the viewport. To make the picture more appealing we can add some scaling to the camera.

function Plotter3d(canvasElement) {
    //...

    var scaleFactor = 1.3;
    this._camera.scale.x = scaleFactor;
    this._camera.scale.y = scaleFactor;
    this._camera.scale.z = scaleFactor;
    this._camera.position.z = 20.0;

    //...
};

There is no special magic to the 1.3 thing above – I just guessed it picking some initial number and then trying to adjust it so that the plot looks the way I want it to. However, in real applications we would have to do something more intelligent and adaptive. Usually one can derive the desired parameters from the relations between the size of the canvas (more generally – viewport) and the dimensions of different objects in the scene, although for the purpose of this example the result obtained through a number of intelligent guesses is good enough.


For me the picture above is a great achievement by itself, but let’s go further and animate it, so that it looks more vivid and presents us the Rössler system’s trajectory in finer detail – we will make the plot slowly rotate around the Z-axis. To do this we have to provide three.js a routine that will be responsible for modifying the scene for each animation frame. I define it as another method of the Plotter3d object. Additionally, we will call this function in the plotter constructor to start animating the scene.

Plotter3d.prototype.animate = function() {
    requestAnimationFrame(this.animate.bind(this));

    this._camera.rotation.z += 0.01;

    this._renderer.render(this._scene,this._camera);
}

function Plotter3d(canvasElement) {
    //...

    this._plots = [];
    this.animate();
};

The animate function is very simple and does only three things. First, it asks three.js to call it when time comes to do the next animation step. After that, it increases the camera’s rotation angle around the Z-axis by a little number so that on each successive frame the scene appears rotated a bit more. Finally, animate renders the current frame and that’s the moment when we see the changes on the screen. However, if we try to launch the program now, we’ll see that the plot’s rotation doesn’t look that nice – the problem is that the most reasonable axis to rotate the plot around is the vertical one. At the same time the camera’s Z axis points into the screen, so the plot rotates in the latter’s plane. To fix this we might try to change the animate method so that it rotates camera around the Y-axis instead of Z, although this won’t bring us to the result we want. So how do we tackle this problem? Well, thanks to storing the plotted trajectories in our object the task is relatively easy.

Plotter3d.prototype.animate = function() {
    requestAnimationFrame(this.animate.bind(this));

    for(i = 0; i < this._plots.length; i++) {
        this._plotRotationZ += 0.01;
        this._plots[i].rotation.z = this._plotRotationZ;
    }

    this._renderer.render(this._scene,this._camera);
}

function Plotter3d(canvasElement) {
    //...

    this._plots = [];
    this._plotRotationZ = 0.0;
    this.animate();
};

The idea is that instead of rotating the camera, who’s position and orientation we have already changed, we can rotate the plots. Because we neither moved them nor rotated around any axis, the plots' own Z axis still points from their XY plane (where most of the Rössler trajectory lies) to the top. That said, we only have to replace the camera.rotation.z += 0.01 line in the animate function with a loop that will run through the list of plots and set the desired rotation for each of them. A little complication required here is to add a member variable that will store the current rotation angle – _plotRotationZ. If we miss this and simply increase the rotation angle of plots on each animation step they won’t be synchronized, i.e. the plot which was created earlier will appear with different angle than the one created later. Although sometimes this might be what one wants, for now I’d better try to preserve at least some measure of mathematical accuracy, leaving the experiment with independent angles to you.

So now we can launch the created page in a browser and spawn a number of nicely rotating trajectories of the Rössler system there. To make the thing better and more interesting to play with one can introduce lots of different improvements. The first to come to my mind are to color different trajectories differently to make them distinguishable and to grant user more control of the image: for example to give them the ability to scale and rotate the plots the way they want. I have partially implemented both these minor features on my site – feel free to go there and look through the source code. However, beware of possible inefficiency and dirt – I didn’t put enough effort into improving my implementation of the plotter and removing obsolete parts.

As for moving further with three.js, there are dozens of ways to go. If one really wants to make up a nice browser-based 3D plotter the good place to start is to introduce coordinate grid and give explorers more options to control and tune the graphs. Good plotting applications should also include such things as legends, export capabilities and more. On the other side, those interested in 3D graphics would likely want to see some beautiful 3D scenes in their browser and could begin with spawning some solid primitives like cubes, spheres and pyramids, texturing them and playing with camera settings – maybe even swapping the orthographic one with the PerspectiveCamera. Whatever you chose there are dozens of impressing examples on the three.js website ranging from quite simple things to absolutely amazing 3D applications. Graphics always seemed to me one of the most fascinating and emotionally rewarding programming applications, no matter are you a beginner or a mature programmer. So go and grab some reward!

As usual, the code of our plotter is available at github. Feel free to get it and play to your pleasure. I will also appreciate feedback in any form: advice, suggestions, questions and even mere muttering!

Комментариев нет:

Отправить комментарий