홀로그램 셰이더

Hologram shader

프레넬 림 라이트 + 스캔라인 + 깜빡임을 더해서 영화 속 파란 홀로그램 영상을 흉내 내는 셰이더.

다른 이름: Scanline fresnelSci-fi hologramRim-light flicker
···
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(0x040810);
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camera.position.set(0, 0.4, 4.6);
camera.lookAt(0, 0, 0);

const vertexShader = [
  'varying vec3 vNormal;',
  'varying vec3 vViewDir;',
  'varying vec2 vUvV;',
  'void main() {',
  '  vNormal = normalize(normalMatrix * normal);',
  '  vec4 mv = modelViewMatrix * vec4(position, 1.0);',
  '  vViewDir = normalize(-mv.xyz);',
  '  vUvV = uv;',
  '  gl_Position = projectionMatrix * mv;',
  '}',
].join('\n');

const fragmentShader = [
  'uniform float uTime;',
  'varying vec3 vNormal;',
  'varying vec3 vViewDir;',
  'varying vec2 vUvV;',
  'float hash(float x) { return fract(sin(x) * 43758.5453); }',
  'void main() {',
  '  vec3 n = normalize(vNormal);',
  '  vec3 v = normalize(vViewDir);',
  '  float fresnel = pow(1.0 - max(dot(n, v), 0.0), 2.2);',
  '  float scan = sin((vUvV.y * 60.0) - uTime * 6.0) * 0.5 + 0.5;',
  '  scan = pow(scan, 6.0);',
  '  float flicker = 0.85 + 0.15 * hash(floor(uTime * 14.0));',
  '  vec3 base = vec3(0.15, 0.75, 0.95);',
  '  vec3 color = base * (0.35 + fresnel * 1.3 + scan * 0.5) * flicker;',
  '  float alpha = clamp(fresnel * 0.9 + scan * 0.3 + 0.12, 0.0, 1.0);',
  '  gl_FragColor = vec4(color, alpha);',
  '}',
].join('\n');

const mat = new THREE.ShaderMaterial({
  uniforms: { uTime: { value: 0 } },
  vertexShader, fragmentShader,
  transparent: true, side: THREE.DoubleSide, blending: THREE.AdditiveBlending, depthWrite: false,
});
const mesh = new THREE.Mesh(new THREE.IcosahedronGeometry(1.15, 3), mat);
scene.add(mesh);

const ring = new THREE.Mesh(new THREE.RingGeometry(1.0, 1.3, 48), new THREE.MeshBasicMaterial({ color: 0x2ad8ff, transparent: true, opacity: 0.5, side: THREE.DoubleSide }));
ring.rotation.x = -Math.PI / 2;
ring.position.y = -1.3;
scene.add(ring);

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.uTime.value = time;
  mesh.rotation.y = time * 0.4;
  mesh.rotation.x = Math.sin(time * 0.2) * 0.15;
  ring.rotation.z = time * 0.3;
  renderer.render(scene, camera);
});

홀로그램 룩은 세 가지 재료를 섞어 만듭니다. 첫째는 fresnel = pow(1 - dot(normal, viewDir), n) — 표면을 스치듯 보는 가장자리일수록 밝아지는 림 라이트로, 홀로그램 특유의 "속은 비어 있고 테두리만 빛나는" 인상을 만듭니다. 둘째는 UV의 y좌표에 시간에 따라 흐르는 sin()을 걸어 만드는 가로줄(scanline)로, 브라운관·프로젝터 특유의 줄무늬를 흉내 냅니다.

셋째는 깜빡임(flicker)입니다. hash(floor(uTime * 14.0))처럼 시간을 정수 스텝으로 끊어(floor) 해시에 넣으면, 부드럽게 변하는 게 아니라 14분의 1초마다 뚝뚝 끊기며 랜덤한 밝기로 튀는 값이 나옵니다 — 이 "계단식" 노이즈가 자연스러운 흔들림보다 훨씬 인공적이고 전자기기다운 깜빡임을 만듭니다.

머티리얼은 blending: THREE.AdditiveBlending과 depthWrite: false로 설정해 빛이 서로 겹쳐 더 밝아지고 뒤 물체가 비쳐 보이게 합니다. side: THREE.DoubleSide로 안쪽 면도 그려서 뒤집힌 표면에서도 홀로그램이 끊기지 않게 한 것도 포인트입니다.

언제 쓰나

SF UI, AR 프로젝션, 캐릭터/제품의 홀로그램 프리뷰처럼 "빛으로 된 영상" 느낌이 필요할 때.