Finding Home

Case Study

Michael Anthony
5 min readDec 8, 2014

I was very excited to participate in this year’s Christmas Experiments, based on seeing the awesome talent who participated in the past two editions.

The finished product turned out to be a lot like I originally saw in my mind, but not without overcoming some obstacles.

While looking for a concept, I stumbled on the design work of Enormous and the idea for flying over a “pastel-ish” geometric landscape became clear. After a concepting session with Anthony Goodwin, I was ready to start coding.

The concept written in Evernote before coding

When working on creative coding projects, layers of iteration are required to make something that looks and feels great. I like to create a “playground” section of code to quickly hack together ideas with complete disregard for strict coding practices. This lets me quickly get things out of my head and onto the screen, specifically to iterate on motion design. When things are looking or feeling good, I can re-write into production code. This has the added benefit of already knowing how something needs to be structured, and saves a bit of refactoring later on.

Terrain

The first attempt took a plane and re-positioned the Y values based on a noise image generated with photoshop.

This seemed to work, so I moved on to the shader which takes in 2 colors: one for the “lit” side, and one for the “dark” side of the terrain.

Although the shading turned out how I had hoped, there was a problem with the terrain. This technique had too many vertices, creating the appearance of very small and defined triangles. Reducing the vertices eroded the shape of the mountains so after some YouTube videos, I jumped into Cinema 4D and created geometry using the terrain tool and a polygon reducer, which achieved the look of distinct triangles.

After a few (dozen) test runs, I generated 12 terrain geometries which get placed in rows, 3 across, in code.

To create the snow on top of the peaks, I check the vertex position in a shader and treat it as snow if it is greater than the corresponding attribute value for height. The attribute values (vSnowHeight) are randomly generated within a range so the snow doesn’t form as a hard line.

if (vPos.y > vSnowHeight * snowMult) {
color = lightColor;
color = mix(color, vec3(1.0), 0.4);
}
This version has a hard line across the snow at its bottom

To create the interaction of the mountain springing up when the orb passes over it, a few steps are taken:

  1. Unproject the 2D mouse position into the 3D world. This is used throughout — such as for the orb position.
  2. Use a Raycaster to check for intersections on the geometries and then check the distance of that vertex from the point of intersection (localized to model space).
  3. If the distance is within the desired range, set the height value and then interpolate to it in a render loop.
_vector.copy(point);
_mesh.worldToLocal(_vector);

var v = _vertices.start();
while (v) {
var dist = _vector.distanceTo(v);
if (!v.triggered && v.canMove && dist < brush) {
_updating = true;
v.triggered = Data.Timing.terrainHeight;
}
v = _vertices.next();
}

All that was left was to color the terrain so I got some help from my close friend and Active Theory’s creative director, Andy Thelander, who designed a set of colors that we used for the day, dusk, and night settings.

Water

The water is created with THREE.Planes and vertex shader code that moves the Y position of each vertex on a sine wave, each with it’s own attribute for speed and movement range.

pos.y = sin(startAngle + (time * speed)) * range;

Since the vertex shader is manipulating vertices, the normals need to be recalculated for the light shading. John Iacoviellio pointed me to the standard derivative GLSL extension which allows you to calculate new normals in the fragment shader.

//vertex shader
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
vViewPosition = -mvPosition.xyz;

//fragment shader
vec3 normal = normalize(cross(dFdx(vViewPosition), dFdy(vViewPosition)));

To create the reflection, I used the THREE.Mirror plugin to render the scene from the perspective of the water and pass it as a uniform to my shaders to blend with the water colors.

vec3 texel = texture2DProj(reflection, mirrorCoord).rgb;
vec3 mixColor = mix(lightColor, texel, length(texel));
lightColor = mix(lightColor, mixColor, 0.25);

Space Particles

About a year ago I made this experiment which resembled a galaxy. I wanted to build on it and create something that made it feel like you were painting a galaxy and then flying through it.

The principles of the particle motion are:

  1. Spawn 15 particles per frame
  2. At initialization set the position as the position of the orb, and create a force that is the difference between the current orb position and the position from the last mouse movement. This is clamped and finessed, as well as a little randomness added.
  3. Each frame, add that initial force as well as push the star in z-space based on some calculated factors. The basic idea is that most particles should fly outwards away from you into space. A few should come flying forward towards the camera.
  4. Get the angle between orb and the particle and apply some attraction based on the distance of the particle from the orb.
//the code is a little ugly, but it is an experiment after all :)this.init = function(position, force) {
_vertex.copy(position);
_position.copy(position);
_initialForce.copy(force).multiplyScalar(0.25);
_initialForce.x = Utils.clamp(_initialForce.x, -10, 10);
_initialForce.y = Utils.clamp(_initialForce.y, -10, 10);

_velocity.add(new Vector3(0, 0, 10));
if (!Utils.doRandom(0, 5)) _zoom.z = 1;

_vec.set(Utils.doRandom(-10, 10) / 5, Utils.doRandom(-10, 10) / 5, Utils.doRandom(-10, 10) / 5);
_initialForce.add(_vec);
}

this.update = function(mouse, delta) {
if (_position.z < -_max) return;
_position.add(_initialForce);
_position.add(_zoom);
_position.add(_velocity);
_vertex.copy(_position);

_zoom.z *= 1.0095;

if (_initialForce.z > -10) _initialForce.z -= 0.009;

_velocity.multiplyScalar(0.9);

_vec.subVectors(mouse, _position);
var dSq = _vec.lengthSq();

var f = dSq / 40000;
f = f < 0 ? 0.1 : f > 1 ? 1 : f;

var a = Math.atan2(_vec.y, _vec.x);

_fx.set(0, 0, 0);
_fx.x += Math.cos(a) * _amount;
_fx.y += Math.sin(a) * _amount;
_velocity.add(_fx);
}

A fun bug that popped up while working on the space background

Optimization and Mobile Devices

With WebGL now available on every platform I wanted to make sure this ran smoothly across not just desktop browsers but mobiles as well.

Instead of raycasting intersections on the geometries with hundreds of vertices, a simple 4 vertex plane is used which was a huge performance improvement.

I also use performance.now() to time render calls, in order to remove effects (depth of field or particle count) when they are taking too long to render in order to maintain a smooth framerate.

An early touch interaction recording

Questions?

Twitter @michaeltheory

Michael Anthony is a co-founder and interactive director at Active Theory.

--

--

Michael Anthony

Co-founder and Interactive Director at @active_theory