Dissolve shader

디졸브(소멸) 셰이더

Discarding every pixel whose noise value falls below a threshold, so a surface appears to burn away and re-form.

Also known as: Noise threshold discardBurn-away effectAlpha clip dissolve
···
js
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x07050c);
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camera.position.set(0, 0.6, 5);
camera.lookAt(0, 0, 0);

const vertexShader = [
  'varying vec3 vPos;',
  'varying vec3 vNormal;',
  'void main() {',
  '  vPos = position;',
  '  vNormal = normalize(normalMatrix * normal);',
  '  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);',
  '}',
].join('\n');

const fragmentShader = [
  'uniform float uProgress;',
  'varying vec3 vPos;',
  'varying vec3 vNormal;',
  'float hash(vec3 p) { return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453); }',
  'float noise(vec3 p) {',
  '  vec3 i = floor(p); vec3 f = fract(p); f = f * f * (3.0 - 2.0 * f);',
  '  float n000 = hash(i), n100 = hash(i + vec3(1.0,0.0,0.0)), n010 = hash(i + vec3(0.0,1.0,0.0)), n110 = hash(i + vec3(1.0,1.0,0.0));',
  '  float n001 = hash(i + vec3(0.0,0.0,1.0)), n101 = hash(i + vec3(1.0,0.0,1.0)), n011 = hash(i + vec3(0.0,1.0,1.0)), n111 = hash(i + vec3(1.0,1.0,1.0));',
  '  float nx00 = mix(n000, n100, f.x), nx10 = mix(n010, n110, f.x), nx01 = mix(n001, n101, f.x), nx11 = mix(n011, n111, f.x);',
  '  float nxy0 = mix(nx00, nx10, f.y), nxy1 = mix(nx01, nx11, f.y);',
  '  return mix(nxy0, nxy1, f.z);',
  '}',
  'void main() {',
  '  float n = noise(vPos * 2.2);',
  '  if (n < uProgress) discard;',
  '  float edge = smoothstep(uProgress, uProgress + 0.08, n);',
  '  vec3 base = vec3(0.25, 0.5, 0.95);',
  '  vec3 glow = vec3(1.0, 0.65, 0.25);',
  '  vec3 color = mix(glow, base, edge);',
  '  float rim = pow(1.0 - abs(dot(normalize(vNormal), vec3(0.0, 0.0, 1.0))), 2.0);',
  '  gl_FragColor = vec4(color + rim * 0.15, 1.0);',
  '}',
].join('\n');

const mat = new THREE.ShaderMaterial({ uniforms: { uProgress: { value: 0 } }, vertexShader, fragmentShader });
const mesh = new THREE.Mesh(new THREE.IcosahedronGeometry(1.3, 3), mat);
scene.add(mesh);

function resize() { renderer.setSize(innerWidth, innerHeight); camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); }
addEventListener('resize', resize); resize();
renderer.setAnimationLoop((t) => {
  const time = t * 0.001;
  mat.uniforms.uProgress.value = 0.25 + 0.2 * Math.sin(time * 0.4);
  mesh.rotation.y = time * 0.25;
  mesh.rotation.x = Math.sin(time * 0.15) * 0.3;
  renderer.render(scene, camera);
});

This effect hinges on a single keyword: discard. In the fragment shader, if noise value n is below uProgress (a value animating 0 to 1), discard drops that pixel entirely — unlike fading alpha to 0, discard also skips depth, so anything behind it shows through immediately. Ramping uProgress from 0 to 1 makes the lowest-noise regions vanish first, so the whole surface doesn’t fade evenly — it reads as holes opening up and spreading.

A hard cutoff looks flat, so smoothstep(uProgress, uProgress + edgeWidth, n) carves out a thin band at the boundary and tints it a different color (an orange glow) to read as a "burning edge." The noise itself reuses the same hash-based 3D value noise as vertex-displacement and noise-sphere, just fed the surface position (vPos) instead of a vertex.

Driving uProgress with a sine wave turns dissolve-and-reform into an endless loop. This demo caps it at 0.45 so the shape never fully disappears, meaning a screenshot taken at any random moment — like the card preview — still shows something; in production this usually plays once, for a skill activation, an object spawning or despawning, or a hit effect.

When to use

Character/item spawn or despawn, skill effects, scene transitions. For a one-shot play, animate uProgress from 0 to 1 once.