GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

레이 마칭 9종 — 캔버스 픽셀 계산으로 그리는 3D 장면 (WebGL 없이)

레이 마칭은 화면 픽셀마다 눈 쪽으로 선을 쏘아, 숫자로 적어 둔 장면의 거리를 선을 따라 걸어가 처음 닿는 표면의 색을 정하는 3D 렌더링 기법입니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 왜 이 순서인가

순서는 화려함의 서열이 아니라 거리 함수가 복잡해지는 순서입니다. 먼저 구 하나의 거리식으로 전진 규칙 자체를 배우고(01), 같은 규칙을 방 안쪽과 도넛에 적용해 봅니다(02·03). 다음은 거리를 더 이상 정확히 아는 것이 아니라 추정하는 단계 — 만델벌브와 멩거 스펀지가 프랙탈의 근사 거리로 넘어갑니다(04·05). 여섯째부터는 거리끼리 섞는 법(06), 삼각함수로 적는 무한 구조(07), 아예 표면을 버리고 밀도를 쌓는 법(08)으로 장면 정의 자체가 넓어지고, 마지막에 그림자·안개·반사를 한 무대에 얹어 부품을 완성된 장면으로 묶습니다(09). 이 canvas 애니메이션 모음 전체를 관통하는 것은 다각형 조각이 아니라 거리 하나이고, 픽셀을 직접 굽는 이웃인 노이즈 지형과 유체 격자가 같은 계보입니다.

항목별 코드

01구 하나 — 최소 레이 마처가 만드는 첫 3D

히어로 배경에 얹는 조명 받는 구체, 3D 소개 화면의 첫 장면에 쓰는, 바닥에 부드러운 그림자가 닿는 가장 작은 레이 마처입니다 — 64스텝 상한, 누적면적 13.794퍼센트 · 강도 평균 19.1로 23칸 프레임이 움직였습니다(실측).

구 하나 — 최소 레이 마처가 만드는 첫 3D
새 창에서 열기

01 · 구 하나 — 최소 레이 마처가 만드는 첫 3D — 측정 누적면적 13.794% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

거리만큼 안전하게 전진하는 루프

// ── march 루프 — 아홉 항목의 원형. 거리만큼 안전하게 전진 ──
var t = 0.0, hit = 0, px = ex, py = ey, pz = ez;
for (var s = 0; s < STEPS; s++) {
  var d = map(px, py, pz);
  if (d < HIT_EPS) { hit = 1; break; }      // 표면 도달
  if (t > 12.0) break;                      // 멀어지면 하늘로 놓아준다
  t += d;
  px = ex + dx * t; py = ey + dy * t; pz = ez + dz * t;
}

map 이 돌려주는 값 d 는 그 점에서 가장 가까운 표면까지의 거리라서, 그 길이만큼 앞으로 가면 어떤 물체도 스치지 않는 것이 보장됩니다. 한 걸음 전진해 다시 재고, d 가 판정값 HIT_EPS 밑으로 떨어지면 거기가 화면에 그릴 픽셀의 색입니다.

02둥근 박스 방 — 안쪽에서 보는 교집합 장면

실내 미리보기, 제품을 놓고 보는 각도 안내에 쓰는, 카메라가 박스 안에 들어가 벽을 보는 실내 렌더입니다.

둥근 박스 방 — 안쪽에서 보는 교집합 장면
새 창에서 열기

02 · 둥근 박스 방 — 안쪽에서 보는 교집합 장면 — 측정 누적면적 52.589% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

안에서 바깥을 재는 교집합 거리

// 실내 거리 — 카메라가 박스 안에 있을 때 둥근 벽까지의 거리.
// 코어(반폭 - 둥글기)의 면까지 여유 거리가 곧 안전 이동 거리다
function distToWall(px, py, pz) {
  var hx = 3.0 - ROUND, hy = 1.5 - ROUND, hz = 3.0 - ROUND;   // 둥글기를 뺀 코어 반폭
  var dx = hx - Math.abs(px), dy = hy - Math.abs(py), dz = hz - Math.abs(pz);
  return Math.min(dx, Math.min(dy, dz));
}

// ── march 루프(실내판) — 경계까지의 거리만큼 안쪽에서 전진 ──
var t = 0.06, hit = 0, px = ex, py = ey, pz = ez;
for (var s = 0; s < STEPS; s++) {
  px = ex + dx * t; py = ey + dy * t; pz = ez + dz * t;
  var d = distToWall(px, py, pz);
  if (d < HIT_EPS) { hit = 1; break; }       // 벽에 도달
  t += d;
  if (t > 9.0) break;
}

밖에서 보는 물체의 거리는 min, 안에서 보는 방의 거리는 세 면까지의 여유 중 가장 작은 값이 안전 이동 거리가 된다는 점만 다르고 루프는 같습니다. 천장 램프가 반경 0.85 로 원을 그으며 돌아 벽 밝기가 흘러가는데, 면이 커서 56스텝 · 56px 해상도로도 누적면적 52.589퍼센트, 강도 평균 37.1을 기록했습니다(실측).

03토러스 링 — 도넛의 거리식과 가장자리 빛

로고 링, 로딩 장면의 회전 도넛에 쓰는, 두 반지름의 차 하나로 정의되는 입체입니다.

토러스 링 — 도넛의 거리식과 가장자리 빛
새 창에서 열기

03 · 토러스 링 — 도넛의 거리식과 가장자리 빛 — 측정 누적면적 11.927% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

두 반지름의 차이가 만드는 입체

// 도넛(토러스) 거리 — XY 평면에 눕혔다. 두 반지름의 차가 전부다
function sdTorus(px, py, pz) {
  var q = Math.sqrt(px * px + pz * pz) - TOR_R;   // 가운데 축까지 거리 - R
  return Math.sqrt(q * q + py * py) - TOR_r;      // 튜브 중심에서 r 만큼 안쪽
}

// rim — 법선이 시선과 수직에 가까울수록(가장자리) 1 에 가까워진다
var facing = Math.max(0, n[0] * -dx + n[1] * -dy + n[2] * -dz);
var rimK = Math.pow(1 - facing, 2.2);
var rimLit = Math.max(0, n[0] * rbx + n[1] * rby + n[2] * rbz);
var rim = rimK * (0.35 + 0.65 * rimLit);
// 시간 위상 — 링 표면을 도는 옅은 광점(살아 있음 한 겹)
var ang = Math.atan2(pz, px) - tSec * 1.1;
var sweep = 0.5 + 0.5 * Math.cos(ang * 2);

가운데 축까지 거리에서 큰 반지름 R(1.05)을 빼고, 다시 세로 성분과 묶어 튜브 반지름 r(0.42)을 빼면 도넛 전체가 겨우 두 줄이 됩니다. 구멍 안쪽 골을 파고드는 픽셀이 많아 스텝은 72로 올렸고, 화면을 채우는 면적은 11.927퍼센트지만 rim 빛이 가장자리를 계속 스치며 22.9의 강도를 유지했습니다(실측).

04만델벌브 — 거리 추정이 여는 프랙탈

히어로 배경의 프랙탈 조형, 커버 아트의 씨앗에 쓰는, 복소 평면의 만델브로를 3차원으로 올린 거듭제곱 반복입니다.

만델벌브 — 거리 추정이 여는 프랙탈
새 창에서 열기

04 · 만델벌브 — 거리 추정이 여는 프랙탈 — 측정 누적면적 33.917% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

거듭제곱 반복과 거리 추정식

var zx = dx, zy = dy, zz = dz;                 // z = c 에서 출발
var dr = 1.0, r = bc, trap = 2.0;
for (var i = 0; i < ITER; i++) {
  r = Math.sqrt(zx * zx + zy * zy + zz * zz);
  if (r < trap) trap = r;
  if (r > 1.6) break;                          // 탈출 — 이 시점의 추정치를 쓴다
  // 구면 좌표로 8제곱 — 세 축이 함께 비틀린다
  var theta = Math.acos(zy / r);
  var phi = Math.atan2(zx, zz);
  var zr = Math.pow(r, POWER);
  var drn = Math.pow(r, POWER - 1) * POWER * dr + 1.0;
  var t8 = theta * POWER, p8 = phi * POWER;
  var st = Math.sin(t8);
  zx = zr * st * Math.sin(p8) + dx;
  zy = zr * Math.cos(t8) + dy;
  zz = zr * st * Math.cos(p8) + dz;
  dr = drn;
  if (r < 0.01) break;                         // 원점 근처 수렴 — 이론상 무한 깊이
}
lastTrap = trap;
return 0.5 * Math.log(Math.max(r, 1.0001)) * r / dr;   // DE = 0.5·r·log(r)/|dr|

반복마다 점을 구면 좌표로 옮겨 8제곱으로 비틀고 원래 점을 더하는데, 여기서 거리를 아는 것이 아니라 미분값 dr 로 추정합니다 — 마지막 줄의 0.5·r·log(r)/|dr| 이 추정식입니다. 프랙탈 표면은 주름 깊이마다 밝기가 다른데, 48px · 80스텝 예산으로 누적면적 33.917퍼센트, 강도 평균 44.6을 냈습니다(실측).

05멩거 스펀지 도시 — 재귀 구멍의 규칙

게임 배경의 도시 블록, 기하 패턴 배경에 쓰는, 정육면체를 아홉 칸으로 나눠 가운데를 뚫는 규칙의 반복입니다.

멩거 스펀지 도시 — 재귀 구멍의 규칙
새 창에서 열기

05 · 멩거 스펀지 도시 — 재귀 구멍의 규칙 — 측정 누적면적 52.859% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

좌표를 접어 얹는 재귀 관통

function map(px, py, pz) {
  var cy = py - 1.0;            // 스펀지 중심 (0,1,0) · 반폭 1
  var d = sdBox(px, cy, pz, 1.0);
  var s = 1.0;
  var cell = 0;
  for (var n = 0; n < LEVELS; n++) {
    var ax = mod2(px * s) - 1.0;
    var ay = mod2(cy * s) - 1.0;
    var az = mod2(pz * s) - 1.0;
    s *= 3.0;
    var rx = Math.abs(1.0 - 3.0 * Math.abs(ax));
    var ry = Math.abs(1.0 - 3.0 * Math.abs(ay));
    var rz = Math.abs(1.0 - 3.0 * Math.abs(az));
    var da = Math.max(rx, ry);              // 십자 관의 세 방향 성분
    var db = Math.max(ry, rz);
    var dc = Math.max(rz, rx);
    var c = (Math.min(da, Math.min(db, dc)) - 1.0) / s;
    d = Math.max(d, c);
    cell = (ax + 1) * 4 + (ay + 1) * 2 + (az + 1);   // 0..8 셀 코드 — 색 변조용
  }
  lastCell = cell;
  return d;
}

각 단계에서 좌표를 mod 2 로 접어 십자 관의 거리를 max 로 얹는데, 이 값은 실제 거리보다 작거나 같은 conservative 근사라 안전하게만 쓰면 형태가 정확합니다. 셀 코드로 블록마다 색이 갈라져 단지 느낌이 나고, 48px 저해상에서도 구멍이 선명해 누적면적 52.859퍼센트 · 강도 평균 51.6, 아홉 판 중 가장 밝게 살아 있습니다(실측).

06메타볼 합체 — 매끈한 합집합의 매끄럽게 잇기

로고 애니메이션, 유기적 형태의 배지에 쓰는, 두 덩어리가 닿는 순간 목이 늘어나며 섞이는 장면입니다.

메타볼 합체 — 매끈한 합집합의 매끄럽게 잇기
새 창에서 열기

06 · 메타볼 합체 — 매끈한 합집합의 매끄럽게 잇기 — 측정 누적면적 32.783% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

부드러운 최솟값이 목을 만든다

// 다항식 smooth min — k 반경 안에서 두 거리를 이어 붙인다(목이 만들어진다)
function smin(a, b, k) {
  var h = 0.5 + 0.5 * (b - a) / k;
  if (h < 0) h = 0; else if (h > 1) h = 1;
  return b + (a - b) * h - k * h * (1 - h);
}

// 장면 거리 = 구 셋의 smin 합집합(바닥 포함)
function map(px, py, pz) {
  var dA = sdSphere(px, py, pz, centers[0], centers[1], centers[2], 0.72);
  var dB = sdSphere(px, py, pz, centers[3], centers[4], centers[5], 0.72);
  var dC = sdSphere(px, py, pz, centers[6], centers[7], centers[8], 0.42);
  lastMix = dA < dB ? 0 : 1;                       // 어떤 구 쪽 표면인지
  var dAB = smin(dA, dB, K);
  var dS = smin(dAB, dC, K * 0.8);                 // 작은 구는 더 가는 목으로 붙는다
  var dF = py;                                     // 바닥 평면 y=0
  return dS < dF ? dS : dF;
}

smin 은 두 거리의 최솟값을 k(0.35) 반경 안에서 다항식으로 이어 붙여, 물체가 스치기 전부터 목이 자라나게 하는 한 줄짜리 기교입니다. 두 큰 구가 3.2초 주기로 모였다 벌어지는 동안 목이 이어졌다 끊어지고, smin 거리도 정확한 거리라 안전 계수 없이 그대로 전진합니다 — 누적면적 32.783퍼센트 · 강도 평균 35.6(실측).

07자이로이드 껍질 — 삼각함수로 적는 무한 구조

텍스처 배경, 조형 아트 씬에 쓰는, 사인과 코사인의 곱이 만드는 주름진 껍질입니다.

자이로이드 껍질 — 삼각함수로 적는 무한 구조
새 창에서 열기

07 · 자이로이드 껍질 — 삼각함수로 적는 무한 구조 — 측정 누적면적 33.915% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

등가면을 두 겹으로 자른 껍질

// 자이로이드 밀도 — 세 축의 sin·cos 곱의 합. 값이 0 인 면이 무한 주름 등가면
function gyroid(px, py, pz) {
  return Math.sin(px) * Math.cos(py) + Math.sin(py) * Math.cos(pz) + Math.sin(pz) * Math.cos(px);
}

function map(px, py, pz) {
  var cy = py - 1.0;
  // 등가면 위상 이동 — 좌표를 통째로 흐르게 하면 주름이 기어간다
  var g = gyroid(px + phase, cy * 1.0, pz - phase * 0.6);
  lastBand = g / 0.5;
  var lip = (Math.abs(g) - SHELL_W) / 1.8;   // 껍질 두껍게 자르기 — 근사 거리
  var dR = Math.sqrt(px * px + cy * cy + pz * pz) - 1.78;
  return lip > dR ? lip : dR;                // max = 교집합(껍질을 구 안으로 자른다)
}

셋의 곱의 합이 0이 되는 면이 무한히 주름진 등가면인데, 절댓값에서 껍질 두께(SHELL_W 0.34)를 빼고 1.8로 나눠 근사 거리로 삼은 뒤 구(반지름 1.78)로 잘라냅니다. 좌표에 위상을 더해 통째로 흘리면 주름이 구름처럼 기어가고, 누적면적 33.915퍼센트 · 최대 면적 22.628퍼센트로 화면 곳곳이 살아 움직입니다(실측).

08구름 부피 — 닫힌 표면이 아니라 쌓이는 안개

하늘 배경, 몽환적 히어로 장면에 쓰는, 경계 없이 솜처럼 쌓이는 구름입니다.

구름 부피 — 닫힌 표면이 아니라 쌓이는 안개
새 창에서 열기

08 · 구름 부피 — 닫힌 표면이 아니라 쌓이는 안개 — 측정 누적면적 45.965% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

고정 스텝으로 쌓는 밀도 누적

// 구름 밀도 — 높이로 다듬고(위 아래로 흐려진다) fbm 과 곱한다. 구름 밑은 비어 있다
function density(px, py, pz, tSec) {
  var cy = py - 1.55;                       // 구름 층 중심 높이
  var hFade = 1.0 - (cy * cy) / 0.85;       // 높이 잎(0 이하는 구름 없음)
  if (hFade <= 0) return 0;
  var n = fbm(px * 0.9 + tSec * DRIFT, cy * 1.5, pz * 0.9 - tSec * DRIFT * 0.7);
  var d = (n - 0.44) * hFade * DENSITY;     // 문턱 0.44 로 솜뭉치만 남긴다
  return d > 0 ? d : 0;
}

표면을 찾지 않고 레이가 지나는 길목마다 밀도를 샙니다. 40개의 고정 구간으로 경로를 나누고 매 구간에서 1 - Math.exp(-d * STEP_LEN) 의 불투명도를 앞이 열린 만큼만 누적하는 front-to-back 합성인데, 앞이 0.98 이상 찬 픽셀은 뒤를 계산하지 않습니다 — 가장 부드럽게 움직여 강도 평균은 17.3으로 낮지만 누적면적 45.965퍼센트를 유지합니다(실측).

09체스판 조명 — 그림자·안개·반사까지 한 장면에

제품 무대, 인터랙티브 아트 씬에 쓰는, 여태 부품을 전부 얹은 완성형 무대입니다.

체스판 조명 — 그림자·안개·반사까지 한 장면에
새 창에서 열기

09 · 체스판 조명 — 그림자·안개·반사까지 한 장면에 — 측정 누적면적 52.859% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

그림자 레이는 같은 루프의 재사용

// 부드러운 그림자 — 표면 점에서 광원 방향으로 다시 마칭한다(01 과 같은 구조).
// 스치듯 가까우면 클램프 완화로 반그림자가 된다
function softShadow(px, py, pz, lx, ly, lz) {
  var t = 0.02, res = 1.0;
  for (var i = 0; i < SHADOW_STEPS; i++) {
    var d = map(px + lx * t, py + ly * t, pz + lz * t);
    if (d < -0.001) return 0.0;
    res = Math.min(res, 10.0 * d / t);   // 가장자리가 부드럽게 — 계수 10 이 완화 폭
    if (res < 0.005 || t > 7.0) break;
    t += Math.max(d, 0.02);
  }
  return res < 0 ? 0 : (res > 1 ? 1 : res);
}

바닥 점에서 광원 쪽으로 같은 march 루프를 한 번 더 돌리면 그림자가 나옵니다 — 스치는 거리는 10.0 * d / t 로 완화해 반그림자가 되고, 젖은 바닥 반사도 법선으로 되튕긴 레이를 32스텝만 더 마칭해 0.38 비율로 섞으며, 지평선은 exp2 감쇠 안개로 녹입니다. 화면 전체가 골고루 움직여 누적면적 52.859퍼센트(실측)로, 시선 어디를 두어도 살아 있습니다.

레이 마칭은 어떻게 3D 가 되나? — 거리장을 걸어가는 눈

부호 거리 함수가 장면을 정의한다

장면은 다각형 목록이 아니라 함수 하나입니다. 점 (x, y, z) 를 넣으면 그 점에서 가장 가까운 표면까지의 거리가 돌아오는데, 물체 안쪽에서는 음수가 돌아오는 것이 부호 거리 함수(SDF)입니다. 구는 length(p - c) - r 이 전부고, 두 물체의 합집합은 거리의 min, 교집합은 max 로 조립됩니다 — 02의 방이 min 의 실내판이고 07의 껍질이 max 로 잘린 교집합입니다. 거리 함수 목록이 이 조립법의 사전 역할을 합니다.

전진 규칙의 수치 반복

눈에서 화면 픽셀 하나로 향하는 선을 따라 이 규칙만 반복합니다: 그 자리에서 거리를 재고, 재친 만큼만 앞으로 간다 — t += d. 01의 구 장면에서 화면 한가운데 픽셀을 따라가면 눈은 구 중심에서 4.2 만큼 떨어져 있고 반지름이 1 이니 첫 재기는 d = 3.2, 한 걸음에 t = 3.2, 그 자리는 이미 표면 위라 다음 재기가 판정값 0.005 밑으로 떨어져 두세 걸음 만에 색이 정해집니다. 모서리를 비스듬히 스치는 픽셀은 d 가 줄곧 작은 양수만 돌려주어 수십 걸음을 걷다가 스텝 상한에서 하늘로 놓아납니다. 멀리 있는 픽셀일수록 걸음이 많아지는 이 성질이 해상도와 스텝 수를 예산으로 다루는 이유입니다.

법선은 거리장의 기울기다

거리가 0 인 곳이 표면이므로, 거리장의 기울기는 표면에서 바깥을 향하는 화살표 — 법선이 됩니다. 여덟 판이 전부 같은 전방 차분으로 축마다 두 번 map 을 불러 기울기를 잰 뒤 정규화해 씁니다. 조명·rim·반사가 전부 이 법선 하나에서 나오니, 장면을 새로 짓는 일은 곧 map 하나를 새로 쓰는 일이 됩니다.

저해상 버퍼와 확대 보간

픽셀마다 스텝을 밟는 계산은 비싸서, 아홉 판은 48~64px 폭의 작은 버퍼에 [ImageData](https://developer.mozilla.org/en-US/docs/Web/API/ImageData) 로 픽셀을 굽고 [putImageData](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/putImageData) 로 옮긴 뒤 화면 크기로 부드럽게 확대합니다. 계산량이 창 크기와 갈라져 이 canvas 렌더링의 예산이 고정되는 구조이고, 프레임은 [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame) 이 넘기는 시간값으로만 흐릅니다. 포인터를 따라 도는 폴리곤 3D 오브제가 다각형을 투영해 쌓았다면 이쪽은 다각형 없이 거리 하나로 장면 전체를 정의하고, 스크린 공간에서 굴절을 굽던 캔버스 특수효과와 달리 눈에서 씬으로 들어가는 깊이가 있습니다.

어디서 깨지는가 — 함정

근사 거리가 실제보다 크면 생기는 터널링

map 이 돌려주는 값이 참 거리보다 크게 나오는 순간 레이는 표면을 뚫고 지나갑니다. t += d 는 d 가 참일 때만 안전하다는 보장이므로, 정확한 거리를 모르는 장면에는 안전 계수를 곱해 전진해야 합니다 — 만델벌브는 추정식이라 0.9를, 멩거 스펀지는 conservative 근사라 여유를 둔 0.85를 곱합니다. 자이로이드가 가장 극단적인데, 등가면의 그래디언트 크기는 최대 약 1.8이라 참 거리로 환산하려면 그 역수보다 작아야 하므로 07은 SAFETY 를 0.5까지 눌러 전진 속도를 반 토막냅니다. 계수를 1에 가깝게 올리면 주름과 구멍 속으로 레이가 순간이동해 표면에 구멍이 숭숭 뚫린 그림이 됩니다 — 프랙탈 판을 고칠 때마다 이 값부터 다시 봤습니다.

표면이 없는 장면의 고정 스텝

08의 구름에는 닿을 표면이 없어 t += d 규칙 자체가 성립하지 않습니다. 밀도가 0보다 큰 영역은 경계가 아니라 안개처럼 퍼져 있으므로, 이 판은 재기를 포기하고 STEP_LEN 0.16 의 고정 구간 40개로 경로를 자릅니다. 같은 이유로 누적이 없는 다른 판의 트릭(판정값 조임, 스텝 상한 조정)이 여기선 아무 소용이 없고, 구간 길이와 밀도 배율만이 화면을 바꿉니다. 아홉 장면의 바닐라 판과 React 판은 글 밑의 암호 zip 에 통째로 들어 있고, 여는 암호는 x8z9uaga 입니다.

변형 세 가지와 접근성

값 하나를 바꾼 세 변형

이름 바꾼 값 느낌 한 줄
스텝 수 01 구 씬 최대 스텝 48 → 96 가장자리 실루엣이 두꺼지고 프레임이 무거워진다
해상도 03 토러스 렌더 타깃 48px → 96px 같은 장면이 선명해지는 대가로 계산량이 네 배
색 $rm-lilac → $rm-peach 차가운 보라 조명이 따뜻한 복숭아 톤으로

셋 다 소스 맨 위의 변형 상수 블록에서 값 하나만 고치는 축입니다. 03은 화면 안의 버튼으로 48/96 을 바로 전환해 계산량 차이를 눈으로 볼 수 있습니다.

움직임을 줄인 사용자에게 남는 한 장

prefers-reduced-motion: reduce 를 켠 사용자에게는 [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame) 루프를 아예 시작하지 않고, 각 장면의 구조가 가장 읽히는 각도의 정적 한 장만 그립니다 — 01은 그림자가 읽히는 heading 0.55 · pitch 0.3, 02는 코너가 읽히는 0.78 · 0.1, 06은 목이 잇는 순간, 09는 그림자와 반사가 같이 보이는 각도입니다. [모션 최소화 미디어쿼리](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion)가 꺼져 있을 때와 같은 계산으로 그린 한 장이므로 품질은 그대로입니다.

키보드 방향키로 도는 카메라

아홉 판 전부 role="application" 과 tabindex="0" 을 가진 실험판에 포인터와 방향키를 함께 받습니다. 화면을 탭으로 옮겨 초점을 주면 방향키로 heading · pitch 를 움직일 수 있고, reduced-motion 사용자는 키를 누르는 족족 그 각도의 한 장만 다시 그려 카메라를 돌리며 구조를 살필 수 있습니다.

FAQ

html5 canvas 애니메이션으로 레이 마칭을 돌려도 속도가 견딜까요?

견딥니다. 아홉 판은 버퍼 폭을 48~64px 로 고정해 계산 픽셀 수가 창 크기와 무관하게 상수로 묶여 있고, 확대는 보간에 맡깁니다. 실측에서 아홉 판 전부 23칸 프레임이 움직였고 넘침은 0건이었으며, 프레임은 시간값으로 재생되어 느린 기기에서는 느리게 돌 뿐 계산이 쌓이지는 않습니다.

레이 마칭과 three.js 같은 3D 라이브러리는 어떻게 다른가요?

three.js 는 다각형 메시를 GPU 에 올려 그리는 도구이고, 레이 마칭은 장면을 거리 함수 하나로 정의해 픽셀마다 표면을 찾아 내려가는 렌더링 방법입니다. 메시로는 만들기 어려운 프랙탈·무한 구조·안개 부피가 거리식 몇 줄로 나오는 대신, 정확한 SDF 가 아니면 터널링이라는 고유의 함정을 관리해야 합니다. 라이브러리 설치 없이 파일 하나를 붙여 넣는 것이 이 편의 자리입니다.

붙여 넣은 코드가 제 페이지에서 바로 돌아갈까요?

돌아갑니다. 아홉 판 모두 라이브러리 · 빌드 · 서버 없이 캔버스와 순수 자바스크립트만으로 짜여 있어, 글 밑의 암호 zip 에서 바닐라 판을 꺼내 붙이면 즉시 움직입니다. React 판도 같이 들어 있고, 색은 각 판 scss 맨 위의 파스텔 토큰 변수로 갈아 끼웁니다.

더 읽을거리

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.