Visualising Data in 3D

thumbnail

header

Interactive infographics are becoming an increasingly popular medium for presenting sophisticated research in an engaging way. IIGs help consumers make sense of large data sets by curating data, making it mutable, and bringing a degree of play to the process of parsing data. Univers Labs has a lot of experience building these; one of our most popular creations is an interactive infographic called Western Dance Music.

Interactive infographics are becoming an increasingly popular medium for presenting sophisticated research in an engaging way. IIGs help consumers make sense of large data sets by curating data, making it mutable, and bringing a degree of play to the process of parsing data. Univers Labs has a lot of experience building these; one of our most popular creations is an interactive infographic called Western Dance Music.

We are investigating ways to revitalise and refresh the IIG concept as a medium for delivering data and hooking viewers. IIGs frequently use technologies like Flash, or HTML5 Canvas, to produce high-performance, interactive 2D graphics, but, for our latest infographic, we wanted to push the envelope, and deliver something really cutting-edge.

The concept design called for fluid, shifting shapes, resembling a 3D area chart. In order to implement this concept design, we chose to use WebGL, implemented via three.js, stepping out of our comfort zone in order to pursue an awesome design. The idea was to allow viewers to control the visualisation by traversing one axis of the chart, with the chart morphing and undulating in response to the user’s control.

Creating a limited color shader

Part of the concept design’s charm was its blocky patterning and discreet colour palette. In order to reproduce this effect, we had to make a shader that drew with a limited palette, but also integrated well into the lighting system offered by three.js. We constructed a bare minimum Lambert shader from the default shader chunks, and used the light intensity value to index into a palette texture. To create the blocky effect, we used the THREE.NearestFilter texture sampling mode, instead of linear texture sampling, and also sampled a noise texture to translate the y-coordinate, giving a floating effect. This would be fairly intensive to do per-vertex on the CPU.

    //Output for the chosen color
    varying vec3 vQuantColor; 

    uniform vec3 pointLightColor[ MAX_POINT_LIGHTS ];
    uniform vec3 pointLightPosition[ MAX_POINT_LIGHTS ];
    uniform float pointLightDistance[ MAX_POINT_LIGHTS ];
    uniform sampler2D paletteTexture;
    uniform sampler2D noiseTexture;
    uniform float time;

    void main()
    {
        vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );

        //Add a noise texture to the position over time to make everything wavy
        vec2 noiseUV = vec2((mvPosition.x/200.0)+time, (mvPosition.z/200.0)+time);
        mvPosition.y += texture2D(noiseTexture, noiseUV).x * 10.0;

        gl_Position = projectionMatrix * mvPosition;

        vec3 objectNormal;
        objectNormal = normal;
        vec3 transformedNormal = normalMatrix * objectNormal;
        transformedNormal = normalize( transformedNormal );

        for( int i = 0; i < MAX_POINT_LIGHTS; i ++ ) {
          //Lambert calculation from three.js
          vec4 lPosition = viewMatrix * vec4( pointLightPosition[ i ], 1.0 );
          vec3 lVector = lPosition.xyz - mvPosition.xyz;
          lVector = normalize( lVector );
          float dotProduct = dot( transformedNormal, lVector );
          vec3 pointLightWeighting = vec3( max( dotProduct, 0.0 ) );
          vec3 lightFront = pointLightColor[ i ] * pointLightWeighting;

          //Sample palette using light intensity and output to frag shader
          vQuantColor = texture2D(paletteTexture, vec2(-lightFront.x, 1.0)).xyz;
    }

A basic noise texture was generated using GIMP and mirrored to make it repeatable:

We also created several carefully designed colour palettes. Ideally, these could be procedurally generated to allow for an arbitrary number of graphs rather than a hardcoded quantity:

Making a fluid graph

To produce the flowing fluid effect, we modelling the y-coordinate of each vertex as a spring, using the equation F = -kx - bV, where k = spring constant, x = displacement, b = dampening, and v = relative velocity. The final code looked like this:

    for(var i = 0; i < meshSize; ++i) {
        var inputVelocity = (-this.sprintLastInput[i] + this.springInput[i]) / delta;
        this.sprintLastInput[i] = this.springInput[i];

        var x = -this.springInput[i] + this.springOutput[i];

        var outputForce = 
            (x * -this.springConst) + 
            ((this.springVelocity[i] - inputVelocity) * 
            -this.springDampening);

        //F = ma    //a = F/m
        //m = 1     //therefore a = F
        this.springVelocity[i] += outputForce * delta;
        var translation = this.springVelocity[i] * delta;

        this.springOutput[i] += translation;
    }

Fake depth of field and transparency

At first, we used a DOF shader to attempt to highlight the selected graph, but this produced a lot of artifacts. It was also difficult to single out any specific graph due to the angle of the camera:

Instead, we used multi-pass rendering, drawing the blurred background, the highlighted graph, and the blurred foreground in order. The result of each pass was drawn straight to the screen over the top of the last pass, rather than using three.js' effect composer to combine all the passes. This saves GPU cycles, and also allows the built-in anti-aliasing in Google Chrome to continue working.

Finally...

See the completed project here.