노이즈 구체

Noise sphere

3D 노이즈로 구의 표면을 정점 방향으로 밀고 당겨서, 행성이나 유기적인 덩어리처럼 울퉁불퉁하게 만드는 기법.

다른 이름: 3D noise displacementOrganic blobPlanet/asteroid shader
···
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(0x070812);
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camera.position.set(0, 0, 4.8);
camera.lookAt(0, 0, 0);

const vertexShader = [
  'uniform float uTime;',
  'varying vec3 vNormal;',
  'varying float vN;',
  '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(position * 1.8 + uTime * 0.3);',
  '  vec3 p = position + normal * n * 0.35;',
  '  vN = n;',
  '  vNormal = normalize(normalMatrix * normal);',
  '  gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);',
  '}',
].join('\n');

const fragmentShader = [
  'varying vec3 vNormal;',
  'varying float vN;',
  'void main() {',
  '  vec3 light = normalize(vec3(0.5, 0.8, 0.6));',
  '  float diff = max(dot(normalize(vNormal), light), 0.0);',
  '  vec3 low = vec3(0.15, 0.2, 0.55);',
  '  vec3 high = vec3(0.5, 0.85, 0.95);',
  '  vec3 base = mix(low, high, clamp(vN, 0.0, 1.0));',
  '  gl_FragColor = vec4(base * (0.35 + diff * 0.85), 1.0);',
  '}',
].join('\n');

const geo = new THREE.SphereGeometry(1.3, 128, 128);
const mat = new THREE.ShaderMaterial({ uniforms: { uTime: { value: 0 } }, vertexShader, fragmentShader });
const mesh = new THREE.Mesh(geo, mat);
scene.add(mesh);

function resize() { renderer.setSize(innerWidth, innerHeight); camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); }
addEventListener('resize', resize); resize();
renderer.setAnimationLoop((t) => {
  mat.uniforms.uTime.value = t * 0.0006;
  mesh.rotation.y = t * 0.0003;
  renderer.render(scene, camera);
});

vertex-displacement 항목이 평면을 2D 노이즈로 흔들었다면, 이 데모는 구를 3D 노이즈로 흔듭니다. 핵심 한 줄은 p = position + normal * noise(position) * amount — 각 정점을 아무 방향이나 임의로 옮기면 표면이 찢어지므로, 반드시 그 정점의 법선(바깥을 향하는 방향)을 따라서만 밀거나 당겨야 구가 울퉁불퉁해지면서도 매끈하게 이어진 하나의 표면을 유지합니다.

노이즈는 x, y, z 세 좌표를 모두 입력받는 3D 해시 기반 값 노이즈(dissolve-shader와 같은 함수)를 씁니다. 2D 노이즈로 구 표면을 흔들면 좌표계가 접히는 극(pole) 부근에서 이음매가 보이지만, 3D 노이즈는 애초에 표면의 좌표 접힘과 무관하게 3차원 공간 자체에 정의돼 있어서 이런 문제가 없습니다. uTime을 노이즈 입력에 더해 표면 전체가 천천히 꿈틀거리게 만들었습니다.

이 데모도 vertex-displacement처럼 정점을 옮긴 뒤 법선을 다시 계산하지 않고 원래 구의 법선을 그대로 조명 계산에 씁니다 — 굴곡이 크지 않을 땐 눈에 띄지 않는 근사지만, 요철이 아주 심해지면 음영이 실제 표면과 어긋나 보일 수 있습니다.

언제 쓰나

절차적 행성/소행성, 유기적인 캐릭터/생명체 표면, 로딩 스피너 대신 쓰는 살아있는 듯한 구체에.