Fuzzy Meshes

I’m a big fan of everything fuzzy and furry, and for a while now I’ve been thinking about a way to implement some of that goodness in WebGL. Recently, as part of a series of code experiments at DPDK, I finally found an excuse to dig into this.

In this post, I will walk though my process and the (mis)steps I took to get to a result that worked out surprisingly well.


Before I got started, I had the following outline in mind:

  • Take a geometry (any geometry would do, but I wanted to start with the DPDK 3D plus model we’ve been using for our other experiments).
  • For each vertex of the geometry, create a “hair”. This would be a thin and tall THREE.ConeGeometry. The geometry would have multiple height segments to support bending.
  • Give each hair a default growth direction. This could be based on the vertex normal.
  • Apply a force (gravity) to the hair geometries so they would curve downward.

I didn’t want to go down the simulation route. This would involve physics and more math than I can handle. Particularly hairy geometries would also likely be too intensive to compute in real time. Instead, I wanted to go for a convincing fake, using Three.Bas to manipulate the hair geometries with maximum performance.

A cone (gray) with gravity applied to it (black).

At first, I wanted to use data textures in the vertex shader to calculate and store positions for each vertex of the hair geometry. However, this would involve additional render passes, and it felt a little too complicated for my use case.

After putting some more thought into it, I decided to try using quaternions to rotate each hair vertex. The amount of rotation would depend on the vertex y position (between 0.0 and the length of the hair). This way vertices at the base of the hair would be affected less than vertices at the end, resulting in a nice curve.

The first result of this approach can be seen below.

Version 1

Implementing Version 1

The Three.js Quaternion class has an unassuming but very powerful method:

Quaternion.setFromUnitVectors(from, to);

This method creates a quaternion needed to transform a unit vector from (often representing a direction) to another unit vector to.

All the vertices in a Cone geometry are spread around the Y axis, so the default “direction” for the cone is up, or vec3(0.0, 1.0, 0.0). To achieve the curvature I was looking for, I added two quaternion attributes to the geometry:

  • rMin, representing the rotation from UP to the neutral hair direction, based on the vertex normal.
  • rMax, representing the rotation from UP to the hair direction + gravity. Since gravity points downward, this boils down to a vector with a lower y value.

Below is a simplified implementation of this. If you are unfamiliar with Three.bas and its API, please refer to my previous tutorial series.

geometry.createAttribute('rMin', 4, (data, i) => {
const direction = directions[i];
  quat.setFromUnitVectors(UP, direction);
quat.toArray(data);
});
geometry.createAttribute('rMax', 4, (data, i) => {
const direction = directions[i];

direction.y -= GRAVITY;
direction.normalize();

quat.setFromUnitVectors(UP, direction);
quat.toArray(data);
});

Inside the vertex shader, I use these quaternions to rotate each vertex. The amount of rotation depends on position.y / hairLength. Vertices at the base of the hair are rotated to the neutral rotation. Vertices towards the end are rotated more downward, because gravity affects them more.

float frc = position.y / hairLength;
vec3 pMin = rotateVector(rMin, position);
vec3 pMax = rotateVector(rMax, position);
transformed = mix(pMin, pMax, frc);
transformed += hairPosition;

The hairPosition at the end is simply the position of the hair on the underlying model. It’s the same for each vertex in a single hair.

With this wired up and working, I added the growth animation logic along with some randomness throughout for the fuzzy look. Below you can see a version of the pen without these frills.

Version 1, simplified. No fuzz, no animation.

Once more, but with forces

Next I wanted to implement some sort of faux physics logic so the hair could actually react to the mesh being moved around. This is where things get more complicated.

Freeing up gravity

In the version above, the rotation change created by the gravity is calculated on the CPU and stored in an attribute. We do not want to update attribute buffers each frame (this is incredibly slow), so the gravitational force is essentially stuck, baked into the attribute data. To get it unstuck (without changing the approach entirely), I decided to move the quaternion calculation logic to GLSL.

The first step was to port the Three.js Quaternion.setFromUnitVectors method from JavaScript.

vec4 quatFromUnitVectors(vec3 from, vec3 to) {
vec3 v;
float r = dot(from, to) + 1.0;

if (r < 0.00001) {
r = 0.0;

if (abs(from.x) > abs(from.z)) {
v.x = -from.y;
v.y = from.x;
v.z = 0.0;
}
else {
v.x = 0.0;
v.y = -from.z;
v.z = from.y;
}
}
else {
v = cross(from, to);
}

return normalize(vec4(v.xyz, r));
}

As you can see, the method itself isn’t that complicated. There are more conditional steps than I’d like to see in GLSL, but I don’t dare remove them.

With this logic available on the GPU, I moved the gravity into a uniform, so it could be used in the shader.

uniform vec3 gravity;
attribute vec3 baseDirection;
...
float frc = position.y / hairLength;
vec3 to = normalize(baseDirection + gravity * frc);
vec4 quat = quatFromUnitVectors(UP, to);
transformed = rotateVector(quat, transformed);

In the code above, baseDirection is the neutral hair direction (the one based on the vertex normal). Gravity is stored as 3D vector (with a negative y value). Adding a fraction of the gravity to the baseDirection yields a new direction. Then we can use the quatFromUnitVectors method to rotate the vertex (which is still facing UP) to match the desired direction.

At this point, the fuzzy mesh looks pretty much the same as before (though I am now using the actual normals in stead of normalized positions like before).

Moving quaternion calculation to the GPU.

With the quaternion calculations on the GPU, it was time to start figuring out how to apply additional forces based on movement and rotation.

Movement

Since the gravity was already stored in a 3D vector, I simply added a movement-based directional force to it. I renamed gravity to globalForce to reflect this change. To determine how much the mash moved, I stored a previous position alongside the current position, and used the difference between the two.

FuzzyMesh.prototype.setPosition = function(p) {
this.previousPosition.copy(this.position);
this.position.copy(p);

this.material.uniforms.globalForce.value
.subVectors(this.previousPosition, this.position);
  this.material.uniforms.globalForce.value.y -= this.gravity;
}

While this worked, the resulting hair movement was quite sluggish, sometimes a little choppy.

Movement force version 1.

I tried various ways to smooth this out, even going so far as to use short TweenMax animations to change the values. Finally I settled on creating an update function, which would be called each frame regardless of animation. There, I add a fraction of the movement delta to a movement force vector. This movement force vector is scaled down each frame, eventually reaching close to 0 if no new forces are applied.

const movementDecay = 0.7;
const movementForceFactor = 0.5;
this.movementForce.multiplyScalar(movementDecay);
this.movementForce.x += this.positionDelta.x * movementForceFactor;
this.movementForce.y += this.positionDelta.y * movementForceFactor;
this.movementForce.z += this.positionDelta.z * movementForceFactor;
this.material.uniforms.globalForce.value.set(
this.movementForce.x,
this.movementForce.y - this.config.gravity,
this.movementForce.z
);

Note that the values for movementDecay and movementForceFactor are completely arbitrary (though the decay should be less than 1.0). As you can see below, the movement is now much smoother. Nice.

Movement force version 2.

Rotation

As for rotation, my goal was to apply a centrifugal force to the hair. I imagine the actual math for this can be pretty complicated. But it should be easy enough to approximate for this use case, right?

I broke the rotation down into an axis and an angle, which together form the quaternion for the mesh itself (remember that all Three.js objects have both a Euler and a Quaternion describing their rotation, which are kept in sync internally).

Having a separate angle makes it easier to measure rotation speed. I did this similarly to movement; storing a previousAngle alongside the current angle, and using the difference between the two to determine speed. This gave me the centrifugalForce, but I still needed a direction to apply this in.

If you spin an object around the y axis, the centrifugal force gets applied on the x and z axes, pushing away from the center of the object, like Homer so elegantly demonstrates below.

Centrifugal force.

If you spin it around the x axis, the force should push away on y and z. For rotation around the z axis, the force should push away on x and y. My initial implementation was as simple as that.

FuzzyMesh.prototype.setRotationAxis = function(axis) {
switch(axis) {
case 'x':
this.rotationAxis.set(1, 0, 0);
this.centrifugalDirection.set(0, 1, 1);
break;
case 'y':
this.rotationAxis.set(0, 1, 0);
this.centrifugalDirection.set(1, 0, 1);
break;
case 'z':
this.rotationAxis.set(0, 0, 1);
this.centrifugalDirection.set(1, 1, 0);
break;
}
}

While I could clearly see there was a pattern here, I could not figure out a way to make this work with an arbitrary rotation axis. This led me down a somewhat roundabout search involving perpendicular vectors, normals, binormals, tangents, and planes.

This was a classic case of overcomplicating the problem. It look me longer than I care to admit to realize that I could use the same method I had been using all along: Quaternion.setFromUnitVectors.

FuzzyMesh.prototype.setRotationAxis = function(axis) {
const ra = this.rotationAxis;
const cd = this.material.uniforms.centrifugalDirection.value;
const q = this._quat;
  ra.set(0, 1, 0);
cd.set(1, 0, 1);
  q.setFromUnitVectors(ra, axis);

cd.applyQuaternion(q);
cd.normalize();
cd.x = Math.abs(cd.x);
cd.y = Math.abs(cd.y);
cd.z = Math.abs(cd.z);

ra.copy(axis);
};

In the method above, the rotation axis and centrifugal direction are first reset to their default values (rotation around the y axis). Then I calculate the quaternion to rotate the default rotation axis to the new rotation axis. Then I use this quaternion to rotate the centrifugal direction by the same amount. Finally I make sure the centrifugal direction values are positive (always pushing away from the center). I also normalize the value (since this is a direction), which I had forgotten to do before.

Inside the shader, the rotation force is calculated based on centrifugalDirection and rotation speed (centrifugalForce). I went through several iterations of how to actually apply this force. In the end settled on the code below (though I may still need to change things up).

// start with the global force calculated in JS
vec3 totalForce = globalForce;
// add centrifugal force for this hair / vertex
totalForce +=
hairPosition * centrifugalDirection * centrifugalForce;

I use hairPosition there because hair away from the center of the mesh should be more strongly affected by rotational force.

There was one more step to take to finalize the rotation logic. If the mesh is rotated, the hair geometries are rotated along with it. To compensate for this, I had to rotate the globalForce in the opposite direction. Luckily, this is easily achieved by using the conjugate of the meshes quaternion. The conjugate of a quaternion is like the inverse of a matrix; it describes the opposite transformation. Luckier still, the Three.js Quaternion class has this method build in.

this.conjugate.copy(this.quaternion).conjugate();
this.material.uniforms.globalForce.value
.applyQuaternion(this.conjugate);
Weee!

Adding some noise

There was one last thing bothering me. When the mesh stops moving, the hair stops moving a little while later. This is good, but all of it stops at the same time. That doesn’t look quite right.

To solve this, I decided to use the trusty sine wave to add a little oscillation inside the shader. I based this on a global settleTime uniform and a settleOffset attribute, randomized for each hair.

totalForce *= 
1.0 - (sin(settleTime + settleOffset) * 0.05 * settleScale);

settleTime is incremented each frame. settleScale is increased when force is applied (either movement or rotation), and slowly scaled back to 0 each frame.

All of these changes lead to the result below. The bouncy mesh animation is controlled by TweenMax, and the hair simply reacts to the change in position and rotation. Sweet.

Version 2

Too much rotation

I was pretty stoked about the result, and how well it all seemed to work, but as I was experimenting with different value ranges, I soon realized that I had a serious problem. If the forces, especially gravity, applied to the mesh are strong, the hair that stands close to upright gets noticeably deformed and elongated.

Version 2. Stronger forces cause deformities.

No good.

To try to make sense of why this was happening, I eventually took to Illustrator to help visualize the problem. Below you can see an image showing how the quaternion approach rotates each point on the line.

Approximation of how each vertex is rotated based on gravity.

As you can see, the distances between the vertices (the black dots) become longer the more a vertex is displaced, while they should stay the same. After some deep thought, I realized this was happening because I was rotating each vertex around the same origin (0, 0, 0).

First I tried to mitigate this problem by changing how I interpolate the length fractions, which didn’t really solve anything. I also tried simply scaling down the rotated position based on the amount of displacement, but this just lead to different deformities because the origin was still wrong.

Finally, I figured that there was no way around it: I had to calculate the rotations recursively. I was a little reluctant to go down this path, since I thought it would add a lot of overhead. Luckily, it turned out that I had plenty of leeway there.

The actual implementation did take some of trial and error. The result can be seen below.

defines: {
'HAIR_LENGTH': (hairLength).toFixed(2),
'SEGMENT_STEP': (hairLength / heightSegments).toFixed(2),
'FORCE_STEP': (1.0 / hairLength).toFixed(2)
}
...
// accumulator for position
vec3 finalPosition;
// get height fraction between 0.0 and 1.0
float f = position.y / HAIR_LENGTH;
// determine target position based on force and height fraction
vec3 to = normalize(baseDirection + totalForce * f);
// calculate quaterion needed to rotate UP to target rotation
vec4 q = quatFromUnitVectors(UP, to);
// only apply this rotation to position x and z
// position y will be calculated in the loop below
vec3 v = vec3(position.x, 0.0, position.z);

finalPosition += rotateVector(q, v);

// recursively calculate rotations using the same approach as above
for (float i = 0.0; i < HAIR_LENGTH; i += SEGMENT_STEP) {
if (position.y <= i) break;

float f = i * FORCE_STEP;
vec3 to = normalize(baseDirection + totalForce * f);
vec4 q = quatFromUnitVectors(UP, to);
// apply this rotation to a 'segment'
vec3 v = vec3(0.0, SEGMENT_STEP, 0.0);
// all segments are added to the final position
finalPosition += rotateVector(q, v);
}

Instead of rotating the vertex around the origin, the loop rotates smaller vectors, equal to the length of the hair divided by the number of height segments, and adds the resulting vectors to the final position of the vertex. The loops breaks when the y coordinate of the vertex is reached. I had to rely on defines here, since GLSL does not support non-static loops.

While this computation is obviously more expensive, the performance didn’t seem to suffer too much.

Version 3. Looking good.

Final Thoughts

The code for this demo can be found here. It’s still a work in progress, but you should be able to take src/FuzzyMesh.js for a spin.

Currently there are a lot of magic numbers that define how much the various forces impact the hair, and you may need to tweak their values depending on your animation. There may also be ways to simplify some of the rotation math. If you have any suggestions, I’m all ears!

I will continue playing around with this, so expect to see more fuzzy (or perhaps leafy?) WebGL meshes in the near future.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.