Noise sphere

노이즈 구체

Pushing and pulling a sphere’s surface along its normals with 3D noise to sculpt it into a lumpy planet or organic blob.

Also known as: 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);
});

Where vertex-displacement shakes a plane with 2D noise, this demo shakes a sphere with 3D noise. The key line is p = position + normal * noise(position) * amount — nudging each vertex in an arbitrary direction would tear the surface, so pushing and pulling strictly along that vertex’s normal (its outward direction) keeps the sphere lumpy while staying one continuous, unbroken surface.

The noise takes all three of x, y, z as input — the same hash-based 3D value noise used in dissolve-shader. Displacing a sphere with 2D noise shows a visible seam near the poles, where the UV coordinate system folds; 3D noise is defined in space itself rather than on the surface’s folded coordinates, so it has no such seam. Adding uTime into the noise input makes the whole surface writhe slowly.

Like vertex-displacement, this demo doesn’t recompute normals after moving vertices — lighting still uses the original sphere’s normals. That’s an invisible approximation at modest bump depth, but shading can visibly disagree with the actual surface once the displacement gets extreme.

When to use

Procedural planets or asteroids, organic character or creature surfaces, or a living-looking sphere in place of a loading spinner.