Subsurface scattering

서브서피스 스캐터링

Light that doesn’t bounce off a surface but seeps in, scatters around inside, and exits somewhere else — why skin, wax, and leaves glow softly when lit from behind.

Also known as: SSSTranslucency
···
html
<div class="sub-caps"><span>no SSS</span><span>SSS approx · backlit glow</span></div>
css
.sub-caps{position:absolute;left:0;right:0;bottom:6%;display:flex;justify-content:space-around;font-size:clamp(8px,2.4vmin,12px);color:#dfe3ffcc;text-align:center;padding:0 3%}
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(0x0b0b14);
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
camera.position.set(0, 0.4, 6.2);

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

const fragmentShader = [
  'uniform vec3 uLightDir;',
  'uniform vec3 uColor;',
  'uniform float uSss;',
  'varying vec3 vNormal;',
  'varying vec3 vViewDir;',
  'void main() {',
  '  vec3 N = normalize(vNormal);',
  '  vec3 L = normalize(uLightDir);',
  '  float wrap = uSss * 0.6;',
  '  float diff = clamp((dot(N, L) + wrap) / (1.0 + wrap), 0.0, 1.0);',
  '  float back = uSss * pow(clamp(dot(-N, L), 0.0, 1.0), 1.4) * clamp(dot(vViewDir, -L), 0.0, 1.0);',
  '  vec3 col = uColor * (0.12 + diff * 0.9) + vec3(1.0, 0.25, 0.15) * back * 1.4;',
  '  gl_FragColor = vec4(col, 1.0);',
  '}',
].join('\n');

function makeSphere(x, sss) {
  const mat = new THREE.ShaderMaterial({
    uniforms: { uLightDir: { value: new THREE.Vector3(1, 1, 1) }, uColor: { value: new THREE.Color(0xffb1a0) }, uSss: { value: sss } },
    vertexShader, fragmentShader,
  });
  const mesh = new THREE.Mesh(new THREE.SphereGeometry(1.05, 64, 64), mat);
  mesh.position.x = x;
  return mesh;
}
const plain = makeSphere(-1.7, 0.0);
const sss = makeSphere(1.7, 1.0);
scene.add(plain, sss);

function resize() { renderer.setSize(innerWidth, innerHeight); camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); }
addEventListener('resize', resize); resize();
renderer.setAnimationLoop((t) => {
  const a = t * 0.0006;
  const dir = new THREE.Vector3(Math.cos(a) * 1.4, 0.5, Math.sin(a) * 1.4 - 0.6);
  plain.material.uniforms.uLightDir.value.copy(dir);
  sss.material.uniforms.uLightDir.value.copy(dir);
  plain.rotation.y = a * 0.4; sss.rotation.y = a * 0.4;
  renderer.render(scene, camera);
});

Ordinary shading (Lambert, PBR) assumes light hits a surface and bounces straight back from that exact point. But somewhat translucent materials — skin, marble, wax, leaves — let light seep just under the surface, scatter around inside, and exit somewhere else entirely. The result is the soft red glow you see when a thin part (an ear, a fingertip, a leaf’s veins) is lit from behind.

A true SSS simulation computes how far light spreads under the surface per wavelength (color), which is expensive. Real-time graphics reach for far cheaper approximations — this demo’s “wrap lighting” pushes the lit terminator past its usual edge by adding an offset to N·L, and adds a term that lets light from behind the object bleed through more strongly where the shape is thin, faking the sense that light is passing straight through.

Blender’s Principled shader, Maya’s aiStandardSurface, and C4D’s subsurface channel all lean on approximations like this for real-time preview, then switch to a slower, more accurate scattering model for the final render.

When to use

Use it for thin or translucent organic materials — skin, wax, fruit, leaves. Skip it for opaque materials like metal or stone that light can’t pass through at all.