Z-파이팅

Z-fighting

두 면이 거의 같은 깊이(z값)에 겹쳐 있을 때, 어느 쪽이 앞인지 매 픽셀·매 프레임 다르게 판정되면서 표면이 지글거리며 깜빡이는 렌더링 결함.

다른 이름: Depth fightingStitching artifact
···
html
<span class="zf-cap">same depth · colors flicker every frame</span>
css
.zf-cap{position:absolute;left:0;right:0;bottom:5%;text-align:center;font-size:clamp(9px,2.4vmin,12px);color:#dfe3ffcc}
js
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: false });
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(35, 1, 0.1, 100);
camera.position.set(0, 3.6, 4.8);
camera.lookAt(0, 0, 0);

const vertexShader = [
  'varying vec2 vUv;',
  'void main() {',
  '  vUv = uv;',
  '  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);',
  '}',
].join('\n');
const fragmentShader = [
  'uniform float uTime;',
  'varying vec2 vUv;',
  'float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }',
  'void main() {',
  '  vec2 cell = floor(vUv * 30.0);',
  '  float flick = hash(cell + floor(uTime * 8.0));',
  '  vec3 colA = vec3(1.0, 0.36, 0.48);',
  '  vec3 colB = vec3(0.37, 0.66, 1.0);',
  '  gl_FragColor = vec4(mix(colA, colB, step(0.5, flick)), 1.0);',
  '}',
].join('\n');

const mesh = new THREE.Mesh(
  new THREE.PlaneGeometry(7, 7),
  new THREE.ShaderMaterial({ uniforms: { uTime: { value: 0 } }, vertexShader, fragmentShader })
);
mesh.rotation.x = -Math.PI / 2 + 0.05;
scene.add(mesh);

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

GPU는 어느 면이 카메라에 더 가까운지를 z버퍼(깊이 버퍼)라는 유한한 정밀도의 숫자로 판정합니다. 두 면이 거의 똑같은 깊이에 겹쳐 있으면, 그 좁은 차이가 부동소수점 반올림 오차보다도 작아질 수 있습니다 — 그러면 어느 픽셀에서는 A가 이기고 바로 옆 픽셀에서는 B가 이기는 식으로 승부가 무작위처럼 뒤집히고, 카메라가 눈곱만큼만 움직여도 그 판정이 다시 뒤집혀서 표면이 지글지글 깜빡이는 것처럼 보입니다.

흔한 원인은 두 개의 평면을 실수로 정확히 같은 위치에 겹쳐 두거나(스티커를 벽에 붙이듯 장식 메시를 벽 메시 위에 바로 얹는 경우), 카메라의 near/far 평면을 너무 멀리 벌려서 깊이 버퍼의 정밀도가 거리 전체에 걸쳐 희석되는 경우입니다.

해결책은 근본적으로 "완전히 같은 깊이"를 피하는 것입니다 — 한쪽을 아주 살짝 띄우거나(오프셋), material.polygonOffset으로 렌더링 시점에만 깊이를 미세하게 밀어주거나, near 평면을 카메라에 더 가깝게 당겨 깊이 버퍼 정밀도를 회복시킵니다. Blender·Unity·Unreal도 같은 원인·같은 해법을 공유합니다.

언제 쓰나

데칼·스티커형 메시를 표면 위에 겹칠 때, 또는 카메라 far 값을 불필요하게 크게 잡았을 때 이 문제를 의심하세요.