kanavtwtgg/birds.cafe. 브라우저에서 도는 Three.js 기반 "새 비행 시뮬레이터"입니다. 갈매기 떼를 V자 편대로 조종해 바다 위를 날고, 낮·밤·폭풍·비·번개로 바뀌는 동적 날씨와 앰비언트 사운드를 입혔습니다. 점수도 미션도 없는 "게임이 아닌 힐링 경험" — 대신 그 안에는 Gerstner 파동 · 쿼터니언 slerp · PMREM 환경맵 · UnrealBloom · ACES 톤매핑 같은 실시간 그래픽과 게임 수학이 빽빽하게 들어 있어, WebGL/Three.js 학습 교본으로 손색이 없습니다. 이 문서는 사용법이 아니라 그 안의 그래픽·수학 설계를 해부합니다. (저장소: kanavtwtgg/birds.cafe · 언어 JavaScript 91.9% / HTML 4.2% / CSS 3.9% · 빌드툴 없음(순수 ESM) · 라이선스 없음(미지정) · 소스 ~2,800줄(JS ~2,523) · 2 커밋(일괄 업로드, 2026-06-21) · ★13 · 라이브 데모 birds.cafe · TrendShift Weekly #12)
"게임은 '점수로 이기는 것'을 안다. birds.cafe는 '브라우저에서 바다 위를 나는 기분'을 만든다 — 그 뒤엔 묵직한 그래픽 수학이 숨어 있다."
도메인 이름 그대로 birds.cafe(새들의 카페)입니다. 키보드(또는 모바일 터치)로 갈매기 한 마리를 조종하면, 뒤따르는 갈매기들이 V자 편대를 이뤄 함께 납니다. 발 밑에는 실시간으로 출렁이는 바다, 머리 위에는 대기 산란으로 그려진 하늘, 그리고 시간이 흐르면 새벽→낮→황혼→밤으로, 때로는 폭풍·비·번개로 풍경이 통째로 바뀝니다. 클리어 조건도, HUD 게이지도 없습니다. 그저 "나는 기분" 그 자체가 목적입니다. 그런데 이 단순한 경험을 만들기 위해, 코드 안에서는 바다 물리(Gerstner 파동)·항공기 자세 제어(쿼터니언)·실시간 반사(환경맵)·영화적 후보정(블룸·톤매핑)이 매 프레임 60번씩 돌아갑니다.
index.html에 importmap(import 지도)을 적어 두고, import * as THREE from 'three' 같은 코드가 브라우저에서 곧장 jsDelivr CDN의 Three.js를 가져오게 합니다. 즉 빌드 명령(npm run build)이 필요 없고, 정적 서버 하나면 그대로 돕니다. 진입 장벽이 극도로 낮은 대신, 의존성을 전부 외부 CDN에 의존한다는 트레이드오프가 있습니다.좀 더 구체적으로, birds.cafe는 약 2,800줄(JS는 2,523줄)의 순수 ES 모듈로 짜인 프런트엔드 전용 프로젝트입니다. 백엔드도, 데이터베이스도, 회원가입도, 점수 서버도 없습니다 — 정직하게 말하면 서버 측 코드가 단 한 줄도 없습니다. python -m http.server 같은 정적 서버 하나로 끝나고, Three.js와 텍스처·사운드 같은 에셋은 전부 CDN/외부에서 불러옵니다. 핵심은 js/main.js(1,141줄)라는 오케스트레이터가 씬·렌더러·후보정·날씨 전환·입력·오디오·애니메이션 루프를 총괄하고, 그 아래 flock.js·weather.js·ocean/·clouds/·islands/·birds/ 같은 도메인 모듈이 각자 한 가지 일을 맡는 모듈러 구조입니다.
이 문서가 파고드는 건 "어떻게 조작하나"가 아니라 그 안의 그래픽·수학입니다 — "출렁이는 바다는 어떤 공식으로 만드나(Gerstner 파동)", "갈매기가 회전해도 절대 뒤집히지 않는 이유(월드업 구속 뱅킹 + 쿼터니언 slerp)", "날씨가 매끄럽게 바뀌는 비결(데이터 주도 보간)". WebGL·셰이더·게임 수학을 처음 공부하는 사람에게 작지만 알찬 교본입니다.
"화려한 데모는 많다. birds.cafe가 눈에 띄는 건 '게임이 아닌 분위기'라는 컨셉 + 실제 배포 + 번들러 없이 보여준 완성도, 이 셋의 결합이다."
솔직히 짚고 가겠습니다. birds.cafe는 ★13 · 커밋 2개 · 한 명의 개발자가 만든 작은 프로젝트입니다. 거대한 프레임워크도, 수천 명의 기여자도 없습니다. 그런데도 TrendShift Weekly #12에 오른 이유는 "무엇을 만들었나"가 아니라 "어떻게, 어떤 감성으로 만들었나"에 있습니다.
| 주목 포인트 | 내용 |
|---|---|
| 컨셉의 신선함 | 점수·미션·승패가 없는 "게임 아닌 힐링 경험". "잘하는 것"이 아니라 "기분을 느끼는 것"이 목적인 인터랙티브 아트에 가깝다. |
| 실제 배포 | 깃허브 데모로 끝이 아니라 birds.cafe라는 도메인에 라이브로 떠 있다. 누구나 클릭 한 번으로 체험 가능 — '말'이 아니라 '결과물'로 증명. |
| 그래픽 완성도 | 동적 날씨 전환 · 반사되는 수면 · 블룸 · 8K 밀키웨이 야간 돔 등 상용 게임급 비주얼을 웹에서 구현. |
| 극도로 낮은 진입장벽 | 번들러·Node·빌드 단계가 전부 없다. 정적 서버 하나로 끝. 코드를 열면 그대로 읽히고, 그대로 돈다 — 학습자에게 최고의 조건. |
요즘 프런트엔드는 webpack·Vite·번들러·트랜스파일러로 진입 장벽이 높습니다. 초보자가 node_modules 수만 개 파일과 빌드 에러에 좌절하기 쉽죠. birds.cafe는 그 모든 걸 걷어내고 "브라우저 표준(ES 모듈 + importmap) + Three.js CDN"만으로 풀 3D 경험을 만들었습니다. git clone 후 정적 서버만 띄우면 끝 — 코드 한 줄 한 줄이 브라우저에서 실제로 어떻게 도는지 투명하게 볼 수 있습니다. 학습용으로는 오히려 거대 프로젝트보다 낫습니다.
장점만 보지 맙시다. birds.cafe는 커밋 2개(사실상 일괄 업로드)·★13·단일 개발자의 신생/취미 프로젝트입니다. 테스트 코드·CI·문서화된 기여 가이드가 없고, 무엇보다 LICENSE가 지정돼 있지 않습니다. 라이선스가 없으면 법적으로는 "저작권자가 모든 권리를 보유" — 즉 코드를 그대로 복사·재배포·상업적 사용하는 것은 원칙적으로 허용되지 않습니다(학습·참고는 가능하나 결과물 재사용 전엔 저자에게 확인 필요). 또 백엔드·DB가 없으므로 "멀티플레이"나 "기록 저장" 같은 확장은 처음부터 직접 만들어야 합니다. 이 문서는 birds.cafe를 제품이 아니라 그래픽/게임 수학 학습 자료로 다룹니다.
birds.cafe의 진짜 가치는 "작은 코드 안에 고급 실시간 그래픽 기법이 압축돼 있다"는 점입니다. 보통 이런 기법들(해양 시뮬레이션·후처리 파이프라인·환경맵 베이킹)은 거대한 게임 엔진 소스 깊숙이 숨어 있어 초보자가 따라가기 어렵습니다. 여기서는 2,800줄, 그것도 빌드 없이 읽히는 코드 안에 그 핵심만 교과서처럼 들어 있습니다.
| 관점 | birds.cafe | 일반적인 게임 엔진(유니티 등) |
|---|---|---|
| 그래픽 API | 웹 표준 WebGL + Three.js (브라우저면 끝) | 전용 런타임·설치 필요 |
| 코드 투명성 | 2,800줄, 빌드 없이 그대로 읽힘 | 엔진 내부는 블랙박스 |
| 해양/물리 | Gerstner 파동을 직접 GLSL로 구현 | 플러그인/에셋에 의존 |
| 배포 | 정적 파일 → 어디든 호스팅 | 빌드 산출물·플랫폼 빌드 |
| 규모 | 취미/학습 규모(★13) | 상용·대규모 |
| 확장(서버·멀티) | 없음 — 직접 추가해야 | 네트워킹 내장 |
거대 게임 엔진 = 모든 게 다 갖춰진 종합 주방. 강력하지만, 칼이 어떻게 갈렸는지·불이 어떻게 붙는지는 안 보여줍니다. 버튼만 누르면 요리가 나오죠.
birds.cafe = 셰프가 손으로 모든 걸 보여주는 오픈 키친. "파도는 이 공식(Gerstner)으로 만들고, 새가 안 뒤집히게 이렇게(쿼터니언 slerp) 자세를 보간하고, 반사는 이렇게(PMREM) 굽는다"를 전 과정 눈앞에서 보여줍니다. 배우려는 사람에겐 종합 주방보다 오픈 키친이 훨씬 낫습니다.
"코드의 91.9%가 JavaScript지만, 진짜 무게중심은 'Three.js 애드온 조합'과 'GLSL 셰이더 한 줄'에 있다. 백엔드는 — 없다."
먼저 정직하게 못 박겠습니다. birds.cafe에는 백엔드·데이터베이스·테스트·CI가 전혀 없습니다. 서버 측 언어(Node·Python·Go 등)도 없고, API 엔드포인트도 없습니다. 이건 순수 클라이언트(브라우저) 전용 프로젝트입니다. 따라서 "전체 지도"는 사실상 프런트엔드 그래픽 스택 + 인프라(배포)로 나뉩니다.
이 프로젝트의 심장은 Three.js와, 그 위에 얹은 공식 애드온(addons/examples)들의 조합입니다. importmap이 이들을 전부 jsDelivr CDN에서 직접 끌어옵니다.
| 모듈 (Three.js addon) | 역할 | 한 줄 설명 |
|---|---|---|
| three (r160) | 렌더링 엔진 | 씬·카메라·메시·조명·WebGLRenderer 전부. 모든 것의 토대. |
| Sky | 대기 산란 하늘 | atmospheric scattering 공식으로 하늘 색·태양 위치를 물리 기반으로 그린다. turbidity 등 파라미터로 낮/황혼/폭풍 분위기를 바꾼다. |
| Water | 반사 수면 | 실시간 반사·굴절을 흉내 내는 물 셰이더. 여기에 자체 Gerstner 파동을 주입해 출렁임을 더한다. |
| GLTFLoader | 3D 모델 로더 | .glb(갈매기·종이비행기) 모델을 불러온다. |
| EffectComposer · RenderPass UnrealBloomPass · OutputPass | 후처리(post-processing) | "기본 렌더 → 블룸(빛 번짐) → 최종 출력(톤매핑/감마)"의 포스트프로세싱 파이프라인. 영화 같은 발광 효과의 정체. |
| Line2 · LineGeometry · LineMaterial | 두꺼운 선 | 새의 비행 궤적(트레일)을 화면 픽셀 단위 두께로 그린다(기본 WebGL 선은 두께 조절이 안 됨). |
PMREMGenerator.fromScene(sky)로 하늘을 실시간으로 환경맵으로 구워 수면과 새가 그 하늘을 반사하게 만듭니다. 날씨가 바뀌면 이 환경맵도 다시 구워집니다.| 기법 | 무엇을 / 왜 |
|---|---|
| ACES 필름 톤매핑 | HDR(넓은 밝기 범위)을 화면(LDR)에 자연스럽게 압축. 헐리우드 영화가 쓰는 색 변환이라 화면이 "필름처럼" 보인다. renderer.toneMapping = ACESFilmicToneMapping. |
| PCF 소프트 섀도 | 그림자 경계를 부드럽게(percentage-closer filtering). 계단현상(에일리어싱) 완화. |
| 8K 밀키웨이 큐브 돔 | 야간에 하늘을 은하수 큐브맵 텍스처로 감싼다. 밤하늘의 별이 이 돔. |
| 자체 Gerstner 파동 GLSL | 핵심. 바다 출렁임 공식을 JavaScript 문자열로 GLSL 셰이더 코드를 동적 생성해 Water의 정점 셰이더에 주입한다(아래 4장에서 코드로 해부). |
| 인스턴싱(InstancedMesh) | 수많은 구름을 드로 콜 한 번으로 그린다(같은 모양을 위치만 바꿔 대량 렌더 → GPU 부담 급감). |
| 데이터 주도 보간(lerp) | 날씨 프리셋 ~25개 파라미터를 lerpWeather(a,b,t)로 동시에 부드럽게 섞는다. |
| 항목 | 내용 |
|---|---|
| 3D 모델 | models/seagull.glb(55KB, Poly Pizza 출처)·models/paperplane.glb(3KB) 2개. + 외부 텍스처(하늘/물/은하수)는 CDN/외부 URL. |
| 오디오 | 순수 HTMLAudioElement(<audio> 태그)로 앰비언트 사운드·효과음 재생. Web Audio API를 일부러 안 쓴다 — 모바일 브라우저 호환성과 단순함을 택한 결정. |
| 입력 | 키보드 + 모바일 터치 컨트롤(@media (max-width:900px)로 분기) + 풀스크린 토글. 데스크탑·모바일 모두 지원. |
| 모바일 최적화 | IS_MOBILE 감지로 픽셀비·블룸 강도·그림자·강수량을 동적으로 낮춰 프레임 유지. |
| 항목 | 내용 |
|---|---|
| 빌드 도구 | 없음. webpack·Vite·Babel·번들러 전부 미사용. package.json의 빌드 스크립트조차 없다(순수 ESM). |
| 서빙 | python -m http.server 같은 정적 서버 하나면 끝. (ES 모듈은 file://로 못 열어서 서버는 필요.) |
| 의존성 | 전부 CDN(jsDelivr). node_modules를 받을 필요가 없다. importmap이 버전을 고정. |
| 백엔드 / DB / API | 전무. 서버 측 코드 0줄. 모든 상태는 브라우저 메모리 안에서만 산다(저장 안 됨). |
| 테스트 / CI | 없음. 자동화 테스트·GitHub Actions 없음. |
| 라이선스 | 미지정(없음). LICENSE 파일이 없어 법적으로 모든 권리 저자 보유 — 재사용 주의. |
"핵심은 단 하나의 루프 — 매초 60번, '입력→물리→날씨 보간→렌더(+블룸)'가 돈다. main.js가 지휘자, 나머지는 단원이다."
한 프레임은 이렇게 그려집니다. ① index.html이 importmap으로 Three.js를 잡고 main.js를 모듈로 로드합니다. ② main.js가 씬·렌더러·후보정 파이프라인을 세우고, 도메인 모듈들(weather·ocean·flock·clouds·islands·birds)을 조립합니다. ③ 그 뒤 requestAnimationFrame 루프(animate())가 시작돼, 매 프레임 — 시간 간격(dt)을 재고 → flock.update()로 새들을 조향하고 → 바다·구름·날씨를 보간하고 → 확률적으로 번개를 트리거하고 → composer.render()로 블룸까지 입혀 화면에 출력합니다. 이 한 줄이 초당 60번 돕니다.
가장 우아한 발상입니다. 날씨를 if (밤) {...} else if (폭풍) {...} 식으로 분기하지 않습니다. 대신 5개의 프리셋(dawn / day / dusk / night / storm)을 각각 ~25개의 파라미터 묶음(하늘 색·혼탁도·태양 위치·블룸 강도·안개·물 색…)으로 정의해 두고, 두 프리셋 사이를 t(0~1) 값으로 선형 보간(lerp)해 섞습니다.
// weather.js — 두 날씨 프리셋을 t로 동시에 섞는다 (색·벡터·스칼라 모두)
export function lerpWeather(a, b, t){
const lerpC = (ca, cb) => ca.clone().lerp(cb, t); // 색(Color) 보간
return {
skyTop: lerpC(a.skyTop, b.skyTop), // 하늘 위쪽 색
turbidity: lerp(a.turbidity, b.turbidity), // 대기 혼탁도
bloomStrength: lerp(a.bloomStrength, b.bloomStrength), // 블룸 세기
fogDensity: lerp(a.fogDensity, b.fogDensity),
sunPosition: a.sunPosition.clone().lerp(b.sunPosition, t),
/* …총 ~25개 파라미터를 한꺼번에 보간… */
};
}
만약 날씨마다 if/else로 색·안개·블룸을 따로 코딩했다면, 새 날씨를 추가할 때마다 코드가 폭발했을 겁니다. 게다가 날씨가 뚝뚝 끊기며 바뀌었겠죠. birds.cafe는 "날씨 = 숫자 묶음"으로 정의했기에, 두 묶음을 t로 섞기만 하면 새벽→낮이 매끄럽게 전환됩니다. 새 날씨("노을")를 추가하려면? 파라미터 묶음 하나만 더 적으면 끝입니다. 데이터 주도 설계의 교과서적 사례입니다.
새가 회전할 때 비행기처럼 기울어지는(뱅킹, banking) 느낌을 주되, 절대로 뒤집히지(배가 하늘을 보지) 않게 하는 것이 관건입니다. birds.cafe는 yaw(좌우)·pitch(상하)를 lerp로 부드럽게 따라가게 하고, 기울기는 "월드 업(중력 반대 방향)"을 기준으로 구속(clamp)합니다. 그리고 최종 회전은 쿼터니언 slerp로 부드럽게 보간합니다.
// flock.js — 뱅킹을 주되, 새가 뒤집히지 않게 '월드 업'으로 구속
const finalUp = levelUp.clone().applyAxisAngle(forward, this.bankAngle);
if (finalUp.y < 0.2) this.bankAngle *= 0.7; // 위쪽이 너무 누우면 기울기를 줄임(뒤집힘 방지)
// 현재 회전 → 목표 회전으로 '구면 선형 보간(slerp)' — 8*dt 비율로 부드럽게
this.smoothFlockQ.slerp(flockQ, Math.min(1, 8 * dt));
quaternion.slerp(target, t)입니다.여기에 안정성을 위한 디테일이 더 붙습니다 — V자 편대를 위한 V_POSITIONS 11개 슬롯(리더 뒤로 좌우 번갈아 배치), 자연스러운 흔들림을 위한 난류 오프셋(turbulence), 그리고 보간 중 값이 깨질 때를 대비한 NaN 가드까지. "단순해 보이는 비행"이 사실은 여러 안전장치의 합작입니다.
이 프로젝트에서 가장 "그래픽 프로그래밍다운" 부분입니다. 진짜 바다처럼 출렁이는 파도를 만들기 위해, birds.cafe는 Gerstner 파동(trochoidal wave) 공식을 JavaScript에서 GLSL 셰이더 문자열로 동적 생성해 Water의 정점 셰이더에 주입합니다. 정점을 위아래로만 움직이는 게 아니라 옆으로도(원 운동) 굴려 파도 꼭대기가 뾰족해지는, 사실적인 형태를 만듭니다.
// ocean/GerstnerWaves.js — 8개 파동을 더해 정점 변위(displacement)를 만든다
float k = 6.28318 / wLen; // 파수(wavenumber) = 2π / 파장
float c = sqrt(9.8 / k); // ★ 분산 관계: 위상 속도 c = √(g/k), g=중력가속도
float f = k * (dir.x*wx + dir.y*wz - c * waveTime) + phase;
float a = steep / k; // 가파름(steepness)을 진폭으로 환산
disp.x += dir.x * (a * choppiness * cos(f)); // 수평 변위(파도가 옆으로 굴러 뾰족해짐)
disp.y += a * sin(f); // 수직 변위(높낮이)
잔잔한 호수에 돌을 던지면 동심원이 퍼지죠. 그런데 진짜 바다는 한 방향의 파도 하나가 아니라, 크고 작은 파도가 여러 방향으로 겹쳐 복잡한 무늬를 만듭니다.
Gerstner 파동은 이걸 흉내 냅니다 — 파장·방향·가파름이 다른 파도 8개를 더해서(중첩) 한 점의 출렁임을 계산합니다. 단순히 위아래로 흔드는 게 아니라, 물 입자를 작은 원으로 굴려 파도 꼭대기가 뾰족하고 골이 평평한 — 실제 파도의 비대칭 형태까지 재현합니다. c = √(g/k)라는 한 줄은 "긴 파도가 짧은 파도보다 빠르게 간다"는 실제 해양 물리(분산 관계)를 그대로 코드에 옮긴 것입니다.
수면은 보통 수만~수십만 개의 정점으로 이뤄집니다. 이 모든 정점의 출렁임을 매 프레임 JavaScript(CPU)로 계산하면 60fps는커녕 버벅임의 지옥입니다. 그래서 birds.cafe는 파동 공식을 GPU에서 도는 정점 셰이더(GLSL)에 넣어 수만 정점을 병렬로 한 번에 처리합니다. 다만 트레이드오프가 있습니다 — GPU에서 움직인 정점의 실제 높이를 CPU(JS)가 정확히 알기 어려워, "새가 수면에 정확히 닿게" 하려면 별도의 근사 계산이 필요합니다. 실시간 그래픽에서 "어디까지 GPU로 미루나"는 늘 따라오는 고민입니다.
구름 한 덩이를 그릴 때마다 GPU에 "그려라" 명령(드로 콜, draw call)을 보내면, 구름이 수백 개면 명령도 수백 번 → 느려집니다. clouds/InstancedClouds.js는 InstancedMesh를 써서 같은 구름 모양을 위치·크기만 바꿔 단 한 번의 드로 콜로 전부 그립니다. GPU에게 "이 모양을, 이 위치 목록대로, 한꺼번에 그려"라고 말하는 것 — 대량의 동일 객체를 그릴 때의 정석 최적화입니다.
모바일 GPU는 데스크탑보다 약합니다. birds.cafe는 시작 시 IS_MOBILE을 감지해, 모바일이면 픽셀 비율(해상도)·블룸 강도·그림자 해상도·강수 입자 수를 동적으로 낮춥니다. "같은 코드, 다른 품질"로 한 코드베이스가 데스크탑의 화려함과 모바일의 부드러움을 모두 잡습니다. 반응형 디자인(@media)이 레이아웃을 바꾸듯, 여기선 렌더 품질 자체를 기기에 맞춥니다.
"폴더 이름이 곧 책임이다. js/ 아래 한 폴더 = 한 가지 일. main.js가 이들을 엮는다."
main.js)이 지휘자가 되어 전체 흐름(초기화·루프·이벤트)을 총괄하고, 실제 일은 각자 한 가지만 담당하는 도메인 모듈(flock·weather·ocean…)에 위임하는 구조입니다. 오케스트라에서 지휘자는 직접 악기를 연주하지 않고 단원들의 타이밍을 맞추죠. 이렇게 나누면 "바다가 이상하다 → ocean/만 본다", "새가 뒤집힌다 → flock.js만 본다"처럼 문제를 격리하기 쉽고, 모듈 하나를 통째로 교체하기도 쉽습니다. 작은 프로젝트지만 구조는 어른스럽습니다.| 파일 | 줄 수(약) | 역할 |
|---|---|---|
| js/main.js | 1,141 | 오케스트레이터 — 씬·렌더러·후보정·날씨 전환·입력·오디오·비/번개·애니 루프 |
| js/flock.js | 522 | 편대 비행 물리 — yaw/pitch 평활화·월드업 구속 뱅킹·쿼터니언 slerp·V_POSITIONS·난류·NaN 가드 |
| js/clouds/InstancedClouds.js | 226 | 인스턴싱 구름 — 단일 드로 콜로 다수 구름 |
| js/weather.js | 173 | 5 날씨 프리셋(dawn/day/dusk/night/storm) + lerpWeather 보간 |
| js/ocean/Ocean.js | 151 | 카메라 추적 3×3 타일 수면(Water + Gerstner) |
| js/birds/LowPolyBird.js | 145 | 절차적 저폴리 갈매기 + 날갯짓 |
| js/ocean/GerstnerWaves.js | 98 | 8파 Gerstner GLSL 동적 생성 |
| js/islands/Islands.js | 67 | 절차적 섬 지형 |
무게중심이 main.js(조립·루프)와 flock.js(비행 물리)에 쏠려 있습니다. 즉 이 프로젝트가 가장 공들인 건 "전체를 어떻게 매 프레임 돌리나"와 "새를 어떻게 자연스럽게 날리나"입니다. 그래픽 기법(Gerstner·인스턴싱)은 의외로 짧은 코드에 압축돼 있어, 오히려 학습용으로 읽기 좋습니다.
이 폴더 구조는 영화 촬영장과 같습니다. main.js는 감독(전체 진행·컷 사인), weather.js는 조명·날씨 팀, ocean/은 수조 특수효과 팀, flock.js는 와이어 액션(새의 움직임) 팀, clouds/·islands/는 배경 미술 팀입니다. 각 팀은 자기 일만 하고, 감독이 "액션!"(animate())을 외치면 모두가 동시에 한 프레임을 만듭니다. 팀이 잘 나뉘어 있어, 한 팀의 작업물만 갈아끼우기 쉽습니다.
"birds.cafe 한 레포 안에 셰이더·게임 수학·후처리·최적화·번들러리스 ESM이 다 들어 있다. 작아서 오히려 다 읽힌다."
배울 것: 파동의 중첩(여러 파를 더함)·분산 관계(c=√(g/k))·가파름(steepness)이 파형에 미치는 영향. 정점을 수직만이 아니라 수평으로도 굴려 사실적 파도를 만드는 원리. 실시간 해양 렌더링의 기본기입니다.
실습: GerstnerWaves.js에서 파동 개수(8)를 2개로 줄여 보고, choppiness(뾰족함)와 steep 값을 키워 보며 잔잔한 호수 ↔ 거친 폭풍 바다가 어떻게 갈리는지 눈으로 확인.
배울 것: 오일러 각의 짐벌 락 문제와 쿼터니언이 그걸 어떻게 피하는지. slerp로 회전을 부드럽게 보간하는 법. "월드 업으로 구속"해 캐릭터가 뒤집히지 않게 하는 실전 트릭. 3D 게임/시뮬레이션이라면 어디서나 쓰입니다.
실습: flock.js의 slerp 비율(8*dt)을 2*dt나 20*dt로 바꿔, 새의 선회가 둔해지거나 너무 휙휙 도는 차이를 체감. finalUp.y < 0.2 가드를 지우면 새가 어떻게 뒤집히는지도 관찰.
배울 것: 왜 반사를 위해 환경을 "텍스처로 굽나"(PMREM). PMREMGenerator.fromScene(sky)로 동적 하늘을 환경맵으로 만들어 수면이 그 하늘을 반사하게 하는 법. 거칠기(roughness)별 프리필터링의 개념.
실습: 날씨를 바꿨을 때 수면 반사가 따라 바뀌는지 확인하고, 환경맵 재생성(re-bake) 주기를 늘리거나 줄여 보며 "반사 품질 vs 성능"의 트레이드오프를 느껴 보기.
배울 것: "기본 렌더 → 효과 패스 → 출력"으로 이어지는 후처리 파이프라인 구성법. UnrealBloom으로 빛을 번지게 하는 원리(밝은 부분 추출 → 흐림 → 합성). ACES 톤매핑이 화면을 "필름처럼" 만드는 이유.
실습: main.js에서 UnrealBloomPass의 strength·threshold·radius를 조절해 발광이 과해지거나 사라지는 걸 관찰. toneMapping을 NoToneMapping으로 바꿔 ACES가 있고 없고의 색감 차이 비교.
배울 것: 하늘 색이 그림이 아니라 물리(빛의 산란)로 계산된다는 사실. turbidity(혼탁도)·rayleigh·태양 위치 파라미터가 새벽·정오·황혼·폭풍 분위기를 어떻게 만드는지.
실습: weather.js의 프리셋에서 turbidity와 태양 고도(sunPosition)를 바꿔 보며, 같은 Sky 셰이더로 전혀 다른 하늘이 나오는 걸 확인.
배울 것: InstancedMesh로 드로 콜을 줄이는 법(구름). 코드로 지형·새를 만드는 절차적 생성(Islands.js·LowPolyBird.js). IS_MOBILE 감지로 품질을 동적 조절하는 적응형 렌더링.
실습: 구름 수를 10배로 늘려 보고, 인스턴싱일 때와 (일부러) 개별 메시로 바꿨을 때 프레임 차이를 측정. 모바일 분기에서 낮추는 항목(픽셀비/블룸/섀도)을 하나씩 꺼 보며 영향 비교.
배울 것: 빌드 도구 없이 import로 모듈을 쪼개고, importmap으로 'three' 같은 별칭을 CDN URL에 매핑하는 법. "정적 서버 하나로 끝나는" 배포의 단순함과, CDN 의존이라는 트레이드오프.
실습: index.html의 importmap에서 Three.js 버전(r160)을 다른 버전으로 바꿔 보고, 호환성 문제가 어디서 터지는지(애드온 경로 변경 등) 추적. 의존성을 로컬 파일로 내려받아 importmap을 로컬 경로로 바꿔 보기.
"Node도 DB도 빌드도 필요 없다. WebGL 되는 브라우저와, 정적 서버 하나면 끝 — 데스크탑이든 폰이든."
| 항목 | 요구사항 / 메모 |
|---|---|
| 브라우저 | WebGL을 지원하는 최신 브라우저(크롬·파이어폭스·사파리·엣지). ES 모듈·importmap도 지원해야 함(요즘 브라우저는 전부 OK). |
| 서버 | 정적 파일 서버 하나. python -m http.server 같은 것이면 충분. (ES 모듈은 file:// 직접 열기가 막혀 있어 서버는 필요.) |
| 빌드 / Node | 불필요. npm install·빌드 명령이 없다. 순수 ESM이라 받아서 바로 서빙. |
| 데이터베이스 / 백엔드 | 불필요(없음). 서버 측 코드 0. 모든 게 브라우저 안에서만 돈다. |
| GPU | 통합 그래픽으로도 동작하나, 블룸·반사·다수 정점 때문에 전용 GPU가 있으면 훨씬 부드럽다. 모바일은 자동으로 품질을 낮춤. |
| 네트워크 | 의존성·에셋(Three.js·텍스처·MP3)을 CDN/외부에서 받으므로 첫 로딩엔 인터넷 필요. |
| 입력 장치 | 키보드(데스크탑) 또는 터치(모바일). 풀스크린 토글 지원. |
가장 흔한 초보 실수입니다. index.html을 그냥 더블클릭(file://)하면 ES 모듈이 보안 정책(CORS)에 막혀 로드되지 않습니다. 반드시 정적 서버를 띄우고 http://localhost:...로 접속해야 합니다(python -m http.server 후 브라우저에서 열기). 또 첫 실행 시 CDN에서 Three.js·텍스처를 받느라 잠깐 검은 화면이 보일 수 있는데, 이는 정상입니다. 사운드는 브라우저 정책상 사용자가 한 번 클릭/터치한 뒤에야 재생됩니다.
"띄워서 날려 보기 → 날씨/편대 손대기 → 파동 만지기 → 입력 추가 → 비행 로직 통째로 교체, 난이도별로."
git clone → 프로젝트 폴더에서 python -m http.server 실행 → 브라우저로 http://localhost:8000 접속. 갈매기를 조종해 바다 위를 날아 보고, 날씨가 바뀌는 걸 관찰한다. (검은 화면이면 7장의 file:// 함정을 의심.)
목표: "번들러 없이 정적 서버만으로 3D가 돈다"를 체감. 개발자 도구(F12) Network 탭에서 Three.js·에셋이 CDN에서 로드되는 걸 확인하는 게 포인트.
(a) flock.js의 V_POSITIONS 좌표를 수정해 V자 편대를 W자나 원형으로 바꿔 본다. (b) weather.js에 기존 프리셋을 본떠 "노을(sunset)" 프리셋을 하나 추가하고, 전환 목록에 끼워 넣는다.
목표: "데이터(좌표·파라미터)만 바꾸면 동작이 바뀐다"는 데이터 주도 설계를 손으로 확인. 분기 코드를 건드리지 않고도 새 날씨가 매끄럽게 보간되는 걸 보는 게 핵심.
GerstnerWaves.js의 파동 개수·steepness·choppiness·파장을 조절해, 거울처럼 잔잔한 수면과 거칠게 부서지는 폭풍 바다를 각각 만들어 본다. 값을 날씨 프리셋과 연동해 "폭풍일 때 자동으로 파도가 거세지게" 묶어 본다.
목표: 파동 파라미터가 파형에 미치는 영향을 직관적으로 이해. 분산 관계(c=√(g/k))를 바꾸면 파도 속도가 어떻게 달라지는지도 실험.
모바일에서 DeviceOrientationEvent(폰 기울기)를 받아, 폰을 기울이면 갈매기가 그 방향으로 선회하도록 입력 계층을 확장한다. 기존 터치 컨트롤과 충돌하지 않게 토글로 전환.
목표: 기존 입력 파이프라인(키보드/터치)에 새 입력원을 붙이는 법, 그리고 자이로 값을 yaw/pitch 목표치로 매핑하는 감각을 익히기. 권한 요청(iOS)도 다뤄 보기.
(a) 현재의 "리더 추종 V자 편대"를 고전 boids 알고리즘(분리·정렬·응집)으로 교체해, 새들이 자율적으로 무리를 짓게 한다. (b) seagull.glb를 다른 GLB 모델로 바꾸고, 모델의 기본 방향이 달라 생기는 회전 어긋남을 modelCorrectionQ(보정 쿼터니언)로 맞춘다.
목표: 편대 비행을 떠받치는 물리/수학을 통째로 갈아끼워 보며 진짜 군집 시뮬레이션을 구현. 모델 로딩 후 좌표계·축 보정이라는, 3D에서 늘 만나는 실전 함정을 직접 해결하기.
"Three.js 기초에서 출발해 셰이더·게임 수학·후처리·최적화·배포까지, birds.cafe를 길잡이 삼아."
| 주차 | 주제 | 핵심 학습 + birds.cafe 연결점 |
|---|---|---|
| 1주 | Three.js 기초 | Scene·Camera·Mesh·Light·WebGLRenderer, 애니메이션 루프(rAF), GLTFLoader로 모델 로드 → main.js 초기화·animate()·birds/LowPolyBird.js |
| 2주 | 셰이더 / GLSL 입문 | 정점·프래그먼트 셰이더 개념, uniform/attribute, GLSL 문법, 파동 함수(sin/cos) → ocean/GerstnerWaves.js 읽고 변형 |
| 3주 | 게임 수학(쿼터니언) | 벡터·행렬·오일러 각의 한계·짐벌 락, 쿼터니언과 slerp, 자세 제어/뱅킹 → flock.js의 회전 보간·월드업 구속 |
| 4주 | 포스트프로세싱 + 라이팅 | EffectComposer 파이프라인, UnrealBloom, ACES 톤매핑, PMREM 환경맵·atmospheric scattering → main.js 후보정·Sky/Water |
| 5주 | 최적화 | 드로 콜·인스턴싱(InstancedMesh), 절차적 지오메트리, 적응형 품질(IS_MOBILE), 프로파일링 → clouds/InstancedClouds.js·islands/ |
| 6주 | 배포 | 번들러리스 ESM·importmap, 정적 호스팅(GitHub Pages/Netlify), 에셋·CDN 관리, 모바일 테스트 → index.html importmap·정적 서빙 구조 |
이 로드맵은 "엔진 켜기 → 색칠하는 법(셰이더) → 움직이는 법(수학) → 보정(후처리) → 가볍게(최적화) → 세상에 내놓기(배포)"를 한 줄로 꿴 코스입니다. 거대한 게임 엔진을 통째로 배우는 대신, 2,800줄짜리 살아 있는 실물 코드 하나를 길잡이로 삼아 실시간 그래픽의 핵심 개념을 빠짐없이 훑습니다. "튜토리얼 따라치기"가 아니라 "실제로 배포된 작품을 해부하며 배우기"라, 끝나면 자기 손으로 비슷한 인터랙티브 3D를 만들 수 있게 됩니다.
"이 문서와 저장소에서 반복되는 그래픽·게임 수학 용어를 한곳에 모았다."
| 용어 | 의미 |
|---|---|
| Three.js | WebGL을 쉽게 다루게 해 주는 JS 3D 라이브러리. birds.cafe는 r160 버전을 CDN으로 사용 |
| WebGL | 브라우저가 GPU에 직접 그림을 그리게 하는 저수준 그래픽 API. Three.js의 토대 |
| Gerstner waves | 물 입자를 원 운동시켜 사실적 파도(뾰족한 꼭대기·평평한 골)를 만드는 파동 모델. birds.cafe가 GLSL로 직접 구현 |
| GLSL vertex shader | GPU에서 정점 위치를 계산하는 셰이더 언어/단계. 수만 정점의 출렁임을 병렬 처리 |
| PMREM 환경맵 | 거칠기별로 미리 흐림 처리한 환경맵. 수면·금속의 사실적 반사를 싸게 계산(PMREMGenerator.fromScene) |
| UnrealBloom | 밝은 부분을 추출·흐림·합성해 빛이 번지게 하는 후처리 효과. 영화적 발광의 정체 |
| ACES 톤매핑 | HDR을 화면(LDR)에 자연스럽게 압축하는 색 변환. 헐리우드 영화가 쓰는 방식 |
| atmospheric scattering | 빛의 산란을 물리로 계산해 하늘 색을 그리는 기법. Three.js Sky 애드온이 사용 |
| quaternion slerp | 3D 회전을 4숫자로 표현(쿼터니언)하고, 두 회전을 최단 경로로 부드럽게 보간(slerp). 짐벌 락 회피 |
| V-formation / flocking | V자 편대 비행 / 무리 짓기. birds.cafe는 리더 추종형 V자(V_POSITIONS 11슬롯)를 사용 |
| instanced rendering | 같은 모양을 위치만 바꿔 드로 콜 한 번으로 대량 렌더(InstancedMesh). 구름 최적화에 사용 |
| importmap | 브라우저가 'three' 같은 모듈 이름을 CDN URL로 해석하게 하는 지도. 번들러를 없애는 열쇠 |
| procedural geometry | 모델 파일 없이 코드로 도형(지형·새)을 생성. Islands.js·LowPolyBird.js |
| GLTFLoader / .glb | 표준 3D 모델 포맷(.glb)을 불러오는 Three.js 로더. 갈매기·종이비행기 로드 |
| tone mapping / HDR | 넓은 밝기 범위(HDR)를 화면이 표현 가능한 범위로 변환하는 과정. ACES가 그 한 방식 |
| draw call | GPU에 "이걸 그려라"라고 보내는 명령 한 번. 많을수록 느려져, 인스턴싱으로 줄인다 |
| EffectComposer / pass | 여러 후처리 단계(pass)를 순서대로 엮는 Three.js 파이프라인. 렌더→블룸→출력 |
| 분산 관계 (c=√(g/k)) | 파동의 위상 속도와 파수의 관계. "긴 파도가 빠르다"는 실제 해양 물리를 코드화 |
| HTMLAudioElement | <audio> 태그 기반 단순 오디오 재생. Web Audio API 대신 채택(모바일 호환·단순) |
| 번들러리스 ESM | webpack·Vite 없이 브라우저 표준 ES 모듈만으로 코드를 쪼개고 로드하는 방식 |
| LICENSE 미지정 | birds.cafe엔 라이선스 파일이 없음 → 법적으로 모든 권리 저자 보유. 학습은 가능하나 코드 재사용·재배포는 저자 확인 필요 |