Hello world! To celebrate my new position at basement.studio I created this small experiment.

To keep the post simple, I will focus on the shader code. Feel free to check the complete source code on Github.

Let's start with a simple shader that renders a white mesh. This shader will be applied to the logo model using a ShaderMaterial

// Vertex Shader
varying vec3 vNormal;
varying vec2 vUv;
varying vec3 wPos;

void main() {
  vUv = uv;
  vNormal = normal;
  wPos = (modelMatrix * vec4(position, 1.0)).xyz;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(wPos, 1.0);
}
// Fragment shader

varying vec3 vNormal;
varying vec2 vUv;
varying vec3 wPos;

void main() {
  gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}

The first thing we need is a mask for our dissolve effect. To solve this, I implemented a couple of uniforms that can be used to create a circle mask.

The uniforms are:

  • vec3 mousePosition
  • float transitionSize
  • float radius

To create the mask, I'm using this function:

float getOffsetFactor(vec3 pos) {
  float x = length(pos - mousePosition) - radius;
  x = x / transitionSize;
  x = clamp(x, 0.0, 1.0);

  return x;
}

Let's visualize it:

// Fragment shader

varying vec3 vNormal;
varying vec2 vUv;
varying vec3 wPos;

// Load our uniforms
uniform vec3 mousePosition;
uniform float transitionSize;
uniform float radius;

// utils
// getOffsetFactor...

void main() {

  float offsetFactor = getOffsetFactor(wPos);

  vec3 color = mix(
    vec3(1.0, 1.0, 1.0),
    vec3(1.0, 0.0, 0.0),
    offsetFactor
  );

  gl_FragColor = vec4(color, 1.0);
}

In red, we can see the area that will be affected by the effect.

Now that we have our mask, we can start working on the dissolve effect. In order to "dissolve" something, a possible approach is to simply deform the object at the same time that we apply a noise texture to it. A cheap Thanos effect.

Transforming the mesh

On the vertex shader, we can calculate the offsetFactor for each vertex and use it to translate the vertex in the direction of the normal.

// Fragment shader

varying vec3 vNormal;
varying vec2 vUv;
varying vec3 wPos;

uniform vec3 mousePosition;
uniform float transitionSize;
uniform float radius;
uniform float offsetDistance;

// utils
// getOffsetFactor...

void main() {
  vUv = uv;
  vNormal = normal;

  // calculate world position
  wPos = (modelMatrix * vec4(position, 1.0)).xyz;

  // calculate offset
  float offsetFactor = getOffsetFactor(wPos);
  vec3 offsetDirection = normalize(position - mousePosition);
  vec3 offset = normal * offsetFactor * offsetDistance;

  // transform pos with offset
  vec3 pos = position;
  pos += offset;

  // recalculate world pos after transform
  wPos = (modelMatrix * vec4(pos, 1.0)).xyz;

  gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}

Now, the vertices that are affected by the dissolve effect are moving in the direction of the normal:

Applying the noise texture

To create a mask for the dissolve effect, we can use a noise texture. I used the Simplex 4D Noise (by Ian McEwan, Ashima Arts).

I also added a noiseSize uniform to control the scale of the noise texture.

// Fragment shader

varying vec3 vNormal;
varying vec2 vUv;
varying vec3 wPos;

uniform float time;
uniform vec3 mousePosition;
uniform float transitionSize;
uniform float radius;
uniform float noiseSize;

// utils
// noise4d1...

void main() {
  // The noise takes a vec4 as parameter,
  // so we can use the time as the 4th dimension to animate it
  vec4 noiseParam = vec4(wPos / noiseSize, time * 0.5);

  // Generate the noise
  float noiseFactor = noise4d1(noiseParam);

  // Remap the noise to a range between 0 and 1
  noiseFactor = noiseFactor * 0.5 + 0.5;

  gl_FragColor = vec4(vec3(noiseFactor), 1.0);
}

Mask

Now that we have the noise and offset factors, we can use it to create a mask for the dissolve effect.

// Fragment shader

varying vec3 vNormal;
varying vec2 vUv;
varying vec3 wPos;

uniform float time;
uniform vec3 mousePosition;
uniform float transitionSize;
uniform float radius;
uniform float noiseSize;

// Utils
// noise4d1
// rgb
// valueRemap
// getOffsetFactor

void main() {
  // Recalculate offset factor with new world position
  float offsetFactor = pow(getOffsetFactor(wPos), 0.4);

  // Calculate noise
  vec4 noiseParam = vec4(wPos / noiseSize, time * 0.5);
  float noiseFactor = noise4d1(noiseParam);
  noiseFactor = noiseFactor * 0.5 + 0.5;

  // Create mask
  float maskFactor = valueRemap(offsetFactor, 0.0, 1.0, -0.01, 1.01);
  float noiseMask = smoothstep(maskFactor, maskFactor + 0.3, noiseFactor);
  noiseMask = clamp(noiseMask / (fwidth(noiseFactor) * 2.0), 0., 1.);
  noiseMask = offsetFactor > 0.99 ? 0.0 : noiseMask;
  noiseMask = offsetFactor < 0.1 ? 1.0 : noiseMask;

  // Discard pixel if noiseMask is too low
  if (noiseMask < 0.1) {
    discard;
  }
  noiseMask = clamp(noiseMask, 0.2, 1.0);

  gl_FragColor = vec4(vec3(noiseMask), noiseMask);

}

The noise can be transformed using the uniforms. A higher offsetDistance with a low noiseSize will create a more granular dissolve effect:

A bigger noiseSize will create visible bubbles:

Transition effect

Finally, by shifting the noiseMask we can create a transition effect.

This is the final shader code:

// Vertex shader

varying vec3 vNormal;
varying vec2 vUv;
varying vec3 wPos;

uniform vec3 mousePosition;
uniform float transitionSize;
uniform float radius;
uniform float offsetDistance;

// Utils
// getOffsetFactor...

void main() {
  vUv = uv;
  vNormal = normal;

  // calculate world position
  wPos = (modelMatrix * vec4(position, 1.0)).xyz;

  // calculate offset
  float offsetFactor = getOffsetFactor(wPos);
  vec3 offsetDirection = normalize(position - mousePosition);
  vec3 offset = normal * offsetFactor * offsetDistance;

  // transform pos with offset
  vec3 pos = position;
  pos += offset;

  // recalculate world pos after transform
  wPos = (modelMatrix * vec4(pos, 1.0)).xyz;

  gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
// Fragment shader

varying vec3 vNormal;
varying vec2 vUv;
varying vec3 wPos;

uniform float time;
uniform vec3 mousePosition;
uniform float transitionSize;
uniform float radius;
uniform float noiseSize;

// Utils
// noise4d1...
// rgb...
// valueRemap...
// getOffsetFactor...

void main() {
  // Recalculate offset factor with new world position
  float offsetFactor = pow(getOffsetFactor(wPos), 0.4);

  // generate a 4D vector for noise, use time as 4th dimension
  vec4 noiseParam = vec4(wPos / noiseSize, time * 0.5);

  // Calculate noise and remap it from -1, 1 to 0, 1
  float noiseFactor = noise4d1(noiseParam);
  noiseFactor = noiseFactor * 0.5 + 0.5;

  // Create mask
  float maskFactor = valueRemap(offsetFactor, 0.0, 1.0, -0.01, 1.01);
  float noiseMask = smoothstep(maskFactor, maskFactor + 0.3, noiseFactor);
  noiseMask = clamp(noiseMask / (fwidth(noiseFactor) * 2.0), 0., 1.);
  noiseMask = offsetFactor > 0.99 ? 0.0 : noiseMask;
  noiseMask = offsetFactor < 0.1 ? 1.0 : noiseMask;

  // Discard pixel if noiseMask is too low
  if (noiseMask < 0.1) {
    discard;
  }
  noiseMask = clamp(noiseMask, 0.2, 1.0);

  // Border
  float borderSize = 0.2;
  float borderMask = noiseFactor - borderSize > maskFactor ? 1.0 : 0.0;
  borderMask = smoothstep(maskFactor, maskFactor + 0.01, noiseFactor - borderSize);
  borderMask = offsetFactor < 0.01 ? 1.0 : borderMask;

  // Color transition
  vec3 orange = rgb(255.0, 77.0, 0.0);
  vec3 white = vec3(0.95);
  vec3 result = mix(orange, white, borderMask);

  gl_FragColor = vec4(result, noiseMask);
}

Final result

Checkout the live demo and the source code