디졸브(소멸) 셰이더

Dissolve shader

노이즈 값이 기준선보다 낮은 픽셀을 통째로 지워서, 표면이 타들어가듯 사라지고 다시 나타나게 만드는 기법.

다른 이름: 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);
});

이 효과의 핵심은 discard 키워드 하나입니다. 프래그먼트 셰이더에서 노이즈 값 n이 uProgress(0~1로 움직이는 진행도)보다 작으면 discard로 그 픽셀 자체를 그리지 않습니다 — 알파를 0으로 낮추는 것과 달리 discard는 깊이(depth)도 안 쓰기 때문에 뒤에 다른 물체가 있으면 그대로 비쳐 보입니다. uProgress를 0에서 1로 올리면 노이즈가 낮은 영역부터 먼저 사라져서, 표면 전체가 균일하게 옅어지는 대신 "구멍이 뚫리며 퍼지는" 소멸처럼 보입니다.

경계를 그냥 딱 자르면 밋밋하므로, smoothstep(uProgress, uProgress + edgeWidth, n)으로 경계 폭만큼 얇은 띠를 만들고 그 띠에 다른 색(주황 글로우)을 입혀 "타들어가는 가장자리"를 표현합니다. 노이즈는 vertex-displacement·noise-sphere와 같은 해시 기반 3D 값 노이즈를 그대로 재사용해서 정점이 아니라 표면 위치(vPos)를 입력으로 씁니다.

uProgress를 사인파로 오가게 하면 소멸과 재생성이 끊임없이 반복되는 루프가 됩니다. 이 데모는 완전히 사라지지 않도록 최댓값을 0.45로 제한해서, 카드 미리보기처럼 아무 때나 스크린샷을 찍어도 형태가 남아 있도록 했습니다 — 실무에서는 스킬 발동, 오브젝트 스폰/디스폰, 피격 이펙트에 보통 한 번만 재생합니다.

언제 쓰나

캐릭터/아이템 소환·소멸, 스킬 이펙트, 화면 전환 연출에. 한 번만 재생할 땐 uProgress를 0→1로 한 번 애니메이션하세요.