Infinite tunnel

무한 터널

Flying the camera through a chain of rings while wrapping its position with a modulo, so the flythrough never seems to end.

Also known as: Endless flythroughLooping corridorModulo camera travel
···
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(0x03040a);
scene.fog = new THREE.FogExp2(0x03040a, 0.05);
const camera = new THREE.PerspectiveCamera(70, 1, 0.05, 60);
camera.position.set(0, 0, 0);

const RINGS = 26;
const SPACING = 2.2;
const group = new THREE.Group();
const ringMeshes = [];
for (let i = 0; i < RINGS; i++) {
  const hue = i / RINGS;
  const mat = new THREE.MeshBasicMaterial({ color: new THREE.Color().setHSL(hue, 0.9, 0.6), wireframe: true, transparent: true });
  const ring = new THREE.Mesh(new THREE.TorusGeometry(1.6, 0.03, 8, 24), mat);
  ring.position.z = -i * SPACING;
  ring.rotation.z = i * 0.35;
  group.add(ring);
  ringMeshes.push(ring);
}
scene.add(group);
const totalLen = RINGS * SPACING;
// 링 반지름(1.6)이 카메라와의 거리보다 커지면 그 링은 화면 규격을 벗어날 만큼 거대해져
// 가장자리 몇 조각만 화면 구석에 어지럽게 걸린다. 투명도만 낮추면 그 조각이 옅게라도 남으므로,
// 아예 렌더링에서 뺀다(visible = false) — 어차피 그만큼 가까우면 카메라를 그 링이 감싸고
// 지나가는 순간이라 안 보여도 어색하지 않다.
const HIDE_DIST = 2.0;

function resize() { renderer.setSize(innerWidth, innerHeight); camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); }
addEventListener('resize', resize); resize();
renderer.setAnimationLoop((t) => {
  const time = t * 0.001;
  const z = (time * 3.2) % totalLen;
  camera.position.z = -z;
  camera.lookAt(0, 0, camera.position.z - 5);
  ringMeshes.forEach((r, i) => {
    r.rotation.z += 0.004 * (i % 2 === 0 ? 1 : -1);
    const d = Math.abs(camera.position.z - r.position.z);
    r.visible = d > HIDE_DIST;
  });
  renderer.render(scene, camera);
});

Building a tunnel that feels endless doesn’t require actually infinite geometry. This demo lines up 26 neon rings (TorusGeometry) at a fixed spacing and keeps advancing the camera’s z position — once it exceeds the total length (totalLen = RINGS × SPACING), a modulo (%) wraps it back to the start: z = (time * speed) % totalLen.

The camera technically teleports at the wrap, but fog (scene.fog = new THREE.FogExp2(...)) fades the rings ahead into the background color, the same trick discussed in the fog entry, so the cut is never visible. Each ring gets its own rotation speed and color (cycling hue by ring order in HSL) so passing through feels rhythmic rather than static.

This "wrap position with modulo" pattern isn’t just for tunnels — it’s the same trick behind infinite-scrolling backgrounds and recycled floor/obstacle geometry in endless-runner games. The key is resetting coordinates rather than creating or destroying geometry.

When to use

Intro sequences, endless-runner backgrounds, or a "warp" moment flying past data or portfolio items.