Night Eyes: case study

Clément Chenebault
12 min readJan 10, 2017

--

For the 4th edition of Christmas Experiment, Wen and I were invited by David Ronai to create a digital experiment for the opening of the Calendar. We received some great help from Bertrand Carrara for the designs, and we ended up with this WebGL experience:

You can try it by clicking on the following link:

If you’re interested in making some [really cool] WebVR, you should check out Wen’s case study here!

If you’re interested in how we made the lines (and how they draw into animals), you should continue to read :)

For the purpose of this experience, we used javascript / native WebGL. Being fairly new to WebGL, I hope this case study won’t be full of bad practices/misconceptions!

Summary:

  • Concept
  • Draw a WebGL 3d Line (draw a line, draw a curve, animate it)

And the following parts will focus on the “Night Eyes” experiment itself:

  • Different Line behaviours (wandering around, draw into an animal, leaving the animal shape)
  • Final Scene
  • Optimisations
  • How to ease with no libs

Notes: All the original sources are available here, and the examples developed for the case study can be found here!

Concept / Idea

What we wanted for our experiment can be summarised by: “beautiful” and “peaceful” scenery (modestly speaking…). To add some christmassy atmosphere and animation into the scene, we used an idea from the studio Differantly :

animals drawn with a single line

Draw a [good looking] 3D Line

1. Draw a line

Finding sources about how to draw a WebGL line is really hard. Two people however made things easier and I can’t thank them enough for that:

Matt DesLauriers (https://mattdesl.svbtle.com/drawing-lines-is-hard) and Jaume Sanchez Elias (https://github.com/spite/THREE.MeshLine),

MERCI BEAUCOUP!

The basic idea:

http://codeflow.org/entries/2012/aug/05/webgl-rendering-of-solid-trails/

And here comes the class Line.js (heavily inspired by the Spite meshline):

This class will pass the following attributes to the vertex shader:

  • a_position (different points composing the line) (“Current” in schema above)
  • a_previous (“Last” in schema above)
  • a_next (“next” in schema above)
  • a_width (width of the line for each position, by default equals to .1)
  • a_direction (the green arrows on the scheme, -1 and 1 alternatively)

(and some other attributes we didn’t need in this experiment)

Notes: the lines are in fact quads, that’s why there are 6 vertices (two triangles) ([n, n+1, n+2],[n+2, n+1, n+3])

TADAA (/ FAIL)
Few hours[sss..] later: “PUTAIN” (French exclamation of triumph)

2. Draw a curve

Now I have a 3D line

We still need to make it pretty… Therefore we must draw some curves between the points!

a) First: Bezier Curve!

I can’t remember if I wrote all these following functions :O please poke me if you feel like you deserve a credit on these ones!

I ended up with these following functions :

let myPoints = [] // to fill yourself with... points...let finalPoints = this.getBezierPoints(myPoints, myPoints.length * 3); // the bezier curve!

The “bezier process” itself :

level(i) {
if(i==0) return 1;
else return i*this.level(i-1);
}
binCoefficient(n, i) { // THIS IS FREAKING EXPENSIVE
return this.level(n) / ( this.level(i) * this.level(n-i) );
}
getBezierPoints(points, numSeg) {
let bezierPoints = [];
let numPoints = points.length;
let t, tx, ty, tz;
let bc, pow1, pow2;

for(let i=0; i<numSeg; i++) {
t = i/numSeg;
tx = ty = tz = 0;

for(let j=0; j<points.length; j++) {
bc = this.binCoefficient(numPoints-1, j);
pow1 = Math.pow((1-t), numPoints-j-1);
pow2 = Math.pow(t, j);
tx += bc * pow1 * pow2 * points[j][0];
ty += bc * pow1 * pow2 * points[j][1];
tz += bc * pow1 * pow2 * points[j][2];
}
let p = [tx, ty, tz];
let index = Math.floor(points.length * t);

if(index == points.length) index = points.length-1;
bezierPoints.push(p);
}
return bezierPoints;
}
Hallelujah! Wait… What!?

This is looking good, so what’s the heck!? This is really expensive… Specially the part to get the binomial coefficient. Calculating it each frame to animate the points would make it very hard to use… :/

b) Second: Catmull-Rom Spline!

You can find plently of implementations of the Catmull-Rom Spline over the internet, this is the one I used (shamelessly inspired by the Three.js one):

// really I should remove this empty array!
let spline = new Spline([]);
let myPoints = [] // to fill...let finalPoints = this.getPoints(myPoints);// -----------------------------------getPoints(pts){
spline.points = pts;

// n_sub is the number of subdivision between two points
// the more the merrier (and the more expensive too)

let indexArray, n_sub = 3;
let newPoints = [];
for (let i = 0; i < pts.length * n_sub; i ++ ) {
indexArray = i / ( pts.length * n_sub );
// we pass newPoints as the 'out' array
spline.getPoint( indexArray, newPoints);
}
return newPoints;
}
Starring the catmull-rom Spline!

Good looking and way faster :O

This is a great link to get your head into if you want to learn more about Bezier Curves.

3. Animate the line

Animating the line is fairly simple. The main idea is to get the first point (head of the line) and animate it… Then make the other points follow!

Following the previous example:

// simplified, pass a list of points to the line
let line = new Line(finalPoints);

When the mouse moves:

mouseMove(x, y) {
this.currentPos = [x, y]; // the head of the line will follow that point
// depth of the line follows a cosinus wave
this.tick++;
this.z = Math.cos(this.tick/25) * 1000 - 700;
}

In the render function, called each frame:

render(){
... // webgl stuff, binding the right shaders etc.
var newPoints = this.createPoints(line);
line.render(newPoints);
... // draw the line, etc.
}

And finally, we move the points:

createPoints(line) {   // move the first point aka line's 'head'
let pt0 = line.points[0];
pt0[0] = this.currentPos[0];
pt0[1] = this.currentPos[1];
pt0[2] = this.z;
// then, make the other points follow
for (let i = 1; i < line.points.length; i++) {
// get the direction between two points
let dir = Matrices.normalize(Matrices.subtractVectors(line.points[i], line.points[i-1]));
line.points[i][0] = line.points[i-1][0] + dir[0] * 20;
line.points[i][1] = line.points[i-1][1] + dir[1] * 20;
line.points[i][2] = line.points[i-1][2] + dir[2] * 20;
}
// finally, make a curve from the points
return getPoints(line.points)
}
Try it here: http://work.goodboydigital.com/codevember/clem/03/

The “Night Eyes” case!

1. Different line behaviours

  • wandering around

The first thing was to make the line move around, like waiting for something to happen.

As explained above, only the first point of each line needs to be animated, and the others will follow… The hard part was to find some maths to make the lines move in an organic way (ish)

We ended up with 3 ways to move the lines around:

// pseudo code// OPTIONS
perlin: this.perlin,
time: Math.random() * 0xFF,
radius: Math.floor(Math.random() * 3) + 2,
targetPoint: this.targetPoint,
xoff: Math.random() * 100, // for perlin noise
yoff: Math.random() * 100 // for perlin noise
// ----- to make it move in a circle
targetPoint[0] = Math.cos(time/20) * radius;
targetPoint[2] = Math.sin(time/20) * radius;
// ----- to make it move in a circle with a perlin noise controlling the y position
targetPoint[0] = Math.cos(time/20) * radius;
targetPoint[2] = Math.sin(time/20) * radius;
xoff += .01 * 1; // perlin noise
yoff += .01 * 1; // perlin noise
let p = perlin.perlin2(xoff, yoff);
targetPoint[1] += p/20;
targetPoint[1] += Math.sin(Math.tan(Math.cos(time/80) * 1.2)) * .01;
// ----- THE SNAAAAKE ONE
targetPoint[0] = Math.cos(time/40) * radius;
targetPoint[2] = Math.sin(time/50) * radius * 1.2 ;
targetPoint[1] = Math.abs(Math.sin(time / 100) * 4) - 2;
targetPoint[0] += Math.cos(Math.pow(8, Math.sin(time/40))) * .5;
targetPoint[1] += Math.sin(Math.pow(8, Math.sin(time/20))) * 1;

You can check out the sources here:

And more particularly, the line motions and the ViewLine.js

Try it here: http://work.goodboydigital.com/christmas/clement/casestudy/line_wandering/
  • Draw into an animal

For this Christmas Experiment, we worked with Bertrand Carrara who made the animal drawings:

❤ Looking sweet ❤

From these 2d drawings, I roughly placed the different points to get the main shape.

Because these are 2d points, we add a depth by applying a cosinus when drawing the shape:

for (var i = 0; i < this.vertices.length; i++) {
...
this.tick++;
this.vertices[i][2] = Math.cos(this.tick/10) * .4;
...
}

Note: Because the “y-axis” is inversed (or is it?) in WebGL, don’t forget to inverse it again or you’ll end up with this:

Mr Bear is looking pretty weird right now

To transform our line into an animal, there are some essential steps:

  1. Get the same number of points for the line and the animal shape (by adding some points on the line).
  2. Add some points in between to generate a path FROM the line TO the shape (blue points in the following drawing)
  3. get the full path (line points + path to go to the shape + animal points) into an array
From the line to the shape, there is the full path.

The idea is to make the line points move to the corresponding animal point.

With the full path filled with 3D points, we now want the line to move and transform into an animal.

To achieve this effect, will use some Tweens. But instead of “tweening” from a position to another, we will tween the points according to the path indices.

0 -> 10 // 1 -> 11 // 2- > 12 // 3 -> 13 // 4 -> 14 // 5 -> 15

For instance on the drawing above: the point at index 0 will go to the point at index 10 by going through all the previous ones. When the tween is updated, we move the current line point accordingly on the curve.

In ViewLine.js, we create the Tweens:

newPoints(){
for (var i = 0; i < line.vert.length; i++) {
var startIndex = ( (line.vert.length -1 ) - i);
var endIndex = (this.path.length - 1) - i;
let obj = { startIndex: startIndex, endIndex: endIndex, currentIndex: startIndex}
let o = Easings.returnVariable(obj, 1, { currentIndex: endIndex });
o.point = startIndex;
this.objectsToTween[index++] = o;

}
}

Note: Read a bit further to see how to update the tweens in an update loop.

The important classes to check out are ViewLine.js, ViewAnimal.js, and Animal.js

Try it here: http://work.goodboydigital.com/christmas/clement/casestudy/line_drawing/
  • Leaving the animal shape

To leave the animal shape, we use two methods:

a) Directly change the state to make the line wander again:

this.objectsToTween = []; // delete the tweens manually
for (var i = 0; i < this.line.points.length; i++) {
this.points[this.points.length - 1- i] = this.line.vert[i*this.sub];
}
this._cutExtraPoints(20);
this.state = STATES.wandering;

b) Use the same idea for the drawing, create a path to leave the shape

I’m being very lazy, and that’s almost use the same code as before.

Note: Remove the extra points to avoid animating a line of 200 points! (and luckily, it looks better too)

_cutExtraPoints(max) {   let nbPtsToSlice = this.points.length - max;
let offset = Math.ceil(this.points.length / max);
let arr = [];
let index = 0;
for (var i = 0; i < this.points.length; i+=offset) {
arr[index++] = this.points[i]
}

this.points = arr;
this.line.points = this.points; // set the new points
this.needsUpdate = true; // will need to upload the buffers
}

2. Final Scene

For this line animation, I can’t recommend the great case study from David Ronai enough, especially if you’re interested in using lines with a predefined path.

About the different paths, they couldn’t be simpler, they all are Helix:

https://en.wikipedia.org/wiki/Helix
// pseudo code
x= x + cos(time/20) * radius;
z = z + sin(time/20) * radius;
y -= .1; // y is only going up

For more information, you can check out the firstLine() and secondLine() method in the following link:

The owl itself is composed of several lines, same process as before:

3. Optimisation

Drawing lines can be very processor intensive, therefore there are some tips about optimisation when using them:

  • the main optimisation: only upload the buffers we need!

In the Line.js geometry, there is a needsUpdate property to know if the line has been changed and needs to be updated.

If the length of the line (its number of points) didn’t change, we don’t need to upload the following buffers: indices, width, directions, uv, counter. We only need to update the positions of the points, and the previous and next attributes.

Note: The needsUpdate property is managed from the ViewLine class itself (set to truewhen the line as been changed).

  • Removing the extra points (with the cutExtraPoints(max) method from earlier)
  • Because we often modify some big arrays, I tend to avoid using the native array functions to modify them. For example, instead of using the push() method, we can do:
let index = 0
let myArray = [];
for (let i = 0; i < values.length; i++) {
myArray[index++] = values[i];
}

Same for the splice() method which can be a bit greedy:

splice(arr, index) {
var len=arr.length;
if (!len) { return }
while (index<len) {
arr[index] = arr[index+1];
index++
}
arr.length--;
}

For more information, see this link!

  • Using pool is a great optimisation.
  • Manually managing the render loop easing can really help with performance (more on this later).
  • Finally, reduce the number of points on mobile.

4. How to ease with no libs

Following the optimisation tricks, here is some explanation about the easing we use in the experiment.

Because we don’t need all features that TweenLite.js provides, there is a simple class to manage the tweens (code is a bit dirty for now, brave yourself!) :

Easings.js

How to use:

import Easings from "Easings";window.Easings = new Easings(); // boouh global variable, ikr...let myObj = {
x: 0
}
Easings.to(myObj, duration || 1, {
x: whatever || 1,
delay: 1,
ease: Easings.easeOutCirc,
onUpdate: ()=>{},
onComplete: ()=>{}
});
// in an update loop, or if you happen to have a ticker manager it would even be better
Easings.update();

While this class already exists, I didn’t use it really (AND I CAN’T REMEMBER WHY…) but still, there is the code to update the Tweens (the ones we set earlier when we draw into an animal shape):

// in the update loop
for (var i = 0; i < this.objectsToTween.length; i++) {
let o = this.objectsToTween[i]
for (var k = 0; k < o.props.length; k++) {
var e = o.props[k];
o.obj[e.var] = Easings.easeOutCirc(o.currentIteration, e.value, e.toValue - e.value, o.duration);
// move the point on the curve (by flooring the index)
let indexFloor = Math.floor(o.obj[e.var]);
this.line.vert[o.point][0] = this.path[indexFloor][0];
this.line.vert[o.point][1] = this.path[indexFloor][1];
this.line.vert[o.point][2] = this.path[indexFloor][2];
}
o.currentIteration += 1; // increment the iteration
if(o.currentIteration > o.duration){
o.delete = true;
}

}
else {
this.splice(this.objectsToTween, i); // splice method for optimisation
i--;
}
}
// and here is the easing.easeOutCirc function
easeOutCirc(t, b, c, d) {
return c * Math.sqrt(1 - (t=t/d-1)*t) + b;
}

In ViewLine.js, you can find this code in therender() method.

Conclusion

If you have any questions (or feedback), you can contact me on twitter @mad_clem :)

I’ll pass on the obvious (how great it was to work on this experience, etc.) to dispatch some huge thanks to:

  • Wen and Bertrand Carrara.
  • David Ronai, for inviting us once again!
  • Tom Jennings for fixing my typos, even though he doesn’t understand anything about code!
  • to all people contributing of making the WebGL world a better place by sharing knowledges:

Last but not least, it’s been great to work on this experience, etc... :)

--

--