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.
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:
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);
}
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:
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);
}
Checkout the live demo and the source code