Z-fighting

Z-파이팅

A rendering glitch where two nearly-coplanar surfaces flicker because the depth buffer can’t consistently decide which one is in front, pixel by pixel, frame by frame.

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

The GPU decides which face is closer to the camera using a finite-precision number called the z-buffer (depth buffer). When two faces sit at nearly identical depth, that tiny gap can shrink below the buffer’s floating-point rounding error — so one pixel decides face A wins, the pixel right next to it decides B wins, and the tiniest camera movement flips that decision again, making the surface look like it’s flickering or crawling.

Common causes: two planes accidentally placed at the exact same position (a decal mesh stuck directly onto a wall mesh, like a sticker), or a camera’s near/far planes stretched too far apart, which thins out the depth buffer’s precision across the whole distance.

The fix is fundamentally about avoiding “exactly the same depth” — nudge one surface slightly (an offset), use material.polygonOffset to push the depth just a hair at render time, or pull the near plane closer to the camera to recover depth-buffer precision. Blender, Unity, and Unreal all share the same cause and the same fixes.

When to use

Suspect this when overlaying a decal-style mesh on a surface, or whenever a camera’s far value has been set needlessly large.