GitHub 트렌딩 딥다이브 · 2026-06-23

birds.cafe 딥다이브
— 번들러 없는 순수 Three.js로 만든 '바다 위 갈매기 비행' WebGL 힐링 시뮬레이터

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)

목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석 (한 프레임이 그려지기까지)
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

"게임은 '점수로 이기는 것'을 안다. birds.cafe는 '브라우저에서 바다 위를 나는 기분'을 만든다 — 그 뒤엔 묵직한 그래픽 수학이 숨어 있다."

한 줄로

birds.cafe는 번들러도 서버도 없이, 브라우저 탭 하나에서 '바다 위 갈매기 편대 비행'을 실시간으로 그려내는 WebGL 힐링 시뮬레이터다.

도메인 이름 그대로 birds.cafe(새들의 카페)입니다. 키보드(또는 모바일 터치)로 갈매기 한 마리를 조종하면, 뒤따르는 갈매기들이 V자 편대를 이뤄 함께 납니다. 발 밑에는 실시간으로 출렁이는 바다, 머리 위에는 대기 산란으로 그려진 하늘, 그리고 시간이 흐르면 새벽→낮→황혼→밤으로, 때로는 폭풍·비·번개로 풍경이 통째로 바뀝니다. 클리어 조건도, HUD 게이지도 없습니다. 그저 "나는 기분" 그 자체가 목적입니다. 그런데 이 단순한 경험을 만들기 위해, 코드 안에서는 바다 물리(Gerstner 파동)·항공기 자세 제어(쿼터니언)·실시간 반사(환경맵)·영화적 후보정(블룸·톤매핑)이 매 프레임 60번씩 돌아갑니다.

용어
WebGL · Three.js
WebGL은 브라우저가 그래픽카드(GPU)에 직접 그림을 그리게 해 주는 저수준 API입니다. 빠르지만 날것이라 삼각형 하나 그리는 데도 코드가 한참 필요합니다. Three.js는 그 위에 씌운 라이브러리로, "씬(Scene)에 카메라·조명·물체(Mesh)를 올리고 렌더러로 그린다"는 직관적인 모형을 제공합니다. birds.cafe는 Three.js r160 버전을 쓰는데, 게임 엔진(유니티·언리얼) 없이 웹 표준만으로 이 정도 비주얼이 가능하다는 걸 보여주는 좋은 예입니다.
용어
importmap · 번들러리스(bundler-less) ESM
보통 요즘 웹 프로젝트는 webpack·Vite 같은 번들러로 수십 개의 JS 파일을 하나로 묶어 배포합니다. birds.cafe는 그 단계가 아예 없습니다. 대신 index.htmlimportmap(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·셰이더·게임 수학을 처음 공부하는 사람에게 작지만 알찬 교본입니다.

2왜 주목받는가

"화려한 데모는 많다. 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) 굽는다"를 전 과정 눈앞에서 보여줍니다. 배우려는 사람에겐 종합 주방보다 오픈 키친이 훨씬 낫습니다.

3기술 스택 전체 지도

"코드의 91.9%가 JavaScript지만, 진짜 무게중심은 'Three.js 애드온 조합'과 'GLSL 셰이더 한 줄'에 있다. 백엔드는 — 없다."

먼저 정직하게 못 박겠습니다. birds.cafe에는 백엔드·데이터베이스·테스트·CI가 전혀 없습니다. 서버 측 언어(Node·Python·Go 등)도 없고, API 엔드포인트도 없습니다. 이건 순수 클라이언트(브라우저) 전용 프로젝트입니다. 따라서 "전체 지도"는 사실상 프런트엔드 그래픽 스택 + 인프라(배포)로 나뉩니다.

① 렌더링 코어 — Three.js r160 + 애드온

이 프로젝트의 심장은 Three.js와, 그 위에 얹은 공식 애드온(addons/examples)들의 조합입니다. importmap이 이들을 전부 jsDelivr CDN에서 직접 끌어옵니다.

모듈 (Three.js addon)역할한 줄 설명
three (r160)렌더링 엔진씬·카메라·메시·조명·WebGLRenderer 전부. 모든 것의 토대.
Sky대기 산란 하늘atmospheric scattering 공식으로 하늘 색·태양 위치를 물리 기반으로 그린다. turbidity 등 파라미터로 낮/황혼/폭풍 분위기를 바꾼다.
Water반사 수면실시간 반사·굴절을 흉내 내는 물 셰이더. 여기에 자체 Gerstner 파동을 주입해 출렁임을 더한다.
GLTFLoader3D 모델 로더.glb(갈매기·종이비행기) 모델을 불러온다.
EffectComposer · RenderPass
UnrealBloomPass · OutputPass
후처리(post-processing)"기본 렌더 → 블룸(빛 번짐) → 최종 출력(톤매핑/감마)"의 포스트프로세싱 파이프라인. 영화 같은 발광 효과의 정체.
Line2 · LineGeometry · LineMaterial두꺼운 선새의 비행 궤적(트레일)을 화면 픽셀 단위 두께로 그린다(기본 WebGL 선은 두께 조절이 안 됨).
용어
PMREM 환경맵 베이킹 (PMREMGenerator.fromScene)
물체에 주변 환경이 반사돼 비치게 하려면, 그 주변(하늘·바다)을 한 장의 특수한 텍스처로 "구워(bake)" 두어야 합니다. PMREM(Prefiltered Mipmapped Radiance Environment Map)은 그 환경맵을 거칠기(roughness)별로 미리 흐림 처리해 둔 형태로, 금속·물 표면의 사실적인 반사를 싸게 계산하게 해 줍니다. birds.cafe는 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 파일이 없어 법적으로 모든 권리 저자 보유 — 재사용 주의.

4아키텍처 심화 분석 (한 프레임이 그려지기까지)

"핵심은 단 하나의 루프 — 매초 60번, '입력→물리→날씨 보간→렌더(+블룸)'가 돈다. main.js가 지휘자, 나머지는 단원이다."

전체 구조 한 장 — 오케스트레이터 + 도메인 모듈

index.html (importmap + HUD + 시작화면 + 모바일 버튼) │ <script type="module" src="js/main.js"> ▼ ┌──────────────────────────────────────────────────────────┐ │ main.js (1,141줄) ── 오케스트레이터 / 지휘자 │ │ · Scene · WebGLRenderer(ACES 톤매핑, PCF 섀도) │ │ · EffectComposer ▶ RenderPass ▶ UnrealBloomPass ▶ Output │ │ · 입력(키보드/터치) · 오디오(<audio>) · 비/번개 · rAF 루프 │ └───┬──────────┬──────────┬──────────┬──────────┬──────────┘ │ 조립 │ │ │ │ ▼ ▼ ▼ ▼ ▼ ┌────────┐ ┌────────┐ ┌─────────┐ ┌────────┐ ┌──────────┐ │weather │ │ ocean/ │ │ flock.js│ │clouds/ │ │ islands/ │ │ .js │ │Ocean │ │ 편대비행 │ │Instanced│ │ 절차지형 │ │5 프리셋 │ │ + │ │물리(yaw/ │ │Clouds │ │ │ │~25파라 │ │Gerstner│ │pitch/뱅킹│ │단일 │ │ birds/ │ │lerp보간 │ │Waves │ │slerp) │ │드로콜 │ │LowPolyBird│ └────────┘ └────────┘ └─────────┘ └────────┘ └──────────┘ 매 프레임 animate(): rAF → dt 계산 → flock.update(조향) → ocean/clouds/날씨 lerp 보간 → 번개 트리거(확률) → composer.render() ← 여기서 블룸까지 적용

한 프레임은 이렇게 그려집니다. index.html이 importmap으로 Three.js를 잡고 main.js를 모듈로 로드합니다. main.js가 씬·렌더러·후보정 파이프라인을 세우고, 도메인 모듈들(weather·ocean·flock·clouds·islands·birds)을 조립합니다. 그 뒤 requestAnimationFrame 루프(animate())가 시작돼, 매 프레임 — 시간 간격(dt)을 재고 → flock.update()로 새들을 조향하고 → 바다·구름·날씨를 보간하고 → 확률적으로 번개를 트리거하고 → composer.render()로 블룸까지 입혀 화면에 출력합니다. 이 한 줄이 초당 60번 돕니다.

설계 패턴 ① — 데이터 주도 날씨 전환 (lerpWeather)

가장 우아한 발상입니다. 날씨를 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로 섞기만 하면 새벽→낮이 매끄럽게 전환됩니다. 새 날씨("노을")를 추가하려면? 파라미터 묶음 하나만 더 적으면 끝입니다. 데이터 주도 설계의 교과서적 사례입니다.

설계 패턴 ② — 물리 기반 편대 비행 (월드업 구속 뱅킹 + slerp)

새가 회전할 때 비행기처럼 기울어지는(뱅킹, 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
쿼터니언은 3D 회전을 표현하는 4개 숫자(x,y,z,w)입니다. 오일러 각(yaw/pitch/roll)으로 회전을 다루면 짐벌 락(gimbal lock)이라는 고질병(특정 자세에서 축 하나가 잠김)이 생기는데, 쿼터니언은 이를 피하고 회전 합성·보간이 안정적입니다. slerp(Spherical Linear intERPolation, 구면 선형 보간)는 두 회전 사이를 최단 경로로 일정한 각속도로 부드럽게 잇는 보간법입니다. 게임에서 캐릭터가 갑자기 휙 도는 대신 자연스럽게 방향을 트는 데 필수입니다. birds.cafe의 갈매기가 부드럽게 선회하는 비결이 바로 quaternion.slerp(target, t)입니다.

여기에 안정성을 위한 디테일이 더 붙습니다 — V자 편대를 위한 V_POSITIONS 11개 슬롯(리더 뒤로 좌우 번갈아 배치), 자연스러운 흔들림을 위한 난류 오프셋(turbulence), 그리고 보간 중 값이 깨질 때를 대비한 NaN 가드까지. "단순해 보이는 비행"이 사실은 여러 안전장치의 합작입니다.

설계 패턴 ③ — Gerstner 파동을 직접 GLSL로 (분산 관계 c=√(g/k))

이 프로젝트에서 가장 "그래픽 프로그래밍다운" 부분입니다. 진짜 바다처럼 출렁이는 파도를 만들기 위해, 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)라는 한 줄은 "긴 파도가 짧은 파도보다 빠르게 간다"는 실제 해양 물리(분산 관계)를 그대로 코드에 옮긴 것입니다.

함정
왜 셰이더(GLSL)에서 계산할까 — CPU로 하면 안 되나?

수면은 보통 수만~수십만 개의 정점으로 이뤄집니다. 이 모든 정점의 출렁임을 매 프레임 JavaScript(CPU)로 계산하면 60fps는커녕 버벅임의 지옥입니다. 그래서 birds.cafe는 파동 공식을 GPU에서 도는 정점 셰이더(GLSL)에 넣어 수만 정점을 병렬로 한 번에 처리합니다. 다만 트레이드오프가 있습니다 — GPU에서 움직인 정점의 실제 높이를 CPU(JS)가 정확히 알기 어려워, "새가 수면에 정확히 닿게" 하려면 별도의 근사 계산이 필요합니다. 실시간 그래픽에서 "어디까지 GPU로 미루나"는 늘 따라오는 고민입니다.

설계 패턴 ④ — 인스턴싱으로 구름을 '드로 콜 한 번'에

구름 한 덩이를 그릴 때마다 GPU에 "그려라" 명령(드로 콜, draw call)을 보내면, 구름이 수백 개면 명령도 수백 번 → 느려집니다. clouds/InstancedClouds.jsInstancedMesh를 써서 같은 구름 모양을 위치·크기만 바꿔 단 한 번의 드로 콜로 전부 그립니다. GPU에게 "이 모양을, 이 위치 목록대로, 한꺼번에 그려"라고 말하는 것 — 대량의 동일 객체를 그릴 때의 정석 최적화입니다.

설계 패턴 ⑤ — 모바일 적응형 품질 저하

모바일 GPU는 데스크탑보다 약합니다. birds.cafe는 시작 시 IS_MOBILE을 감지해, 모바일이면 픽셀 비율(해상도)·블룸 강도·그림자 해상도·강수 입자 수를 동적으로 낮춥니다. "같은 코드, 다른 품질"로 한 코드베이스가 데스크탑의 화려함과 모바일의 부드러움을 모두 잡습니다. 반응형 디자인(@media)이 레이아웃을 바꾸듯, 여기선 렌더 품질 자체를 기기에 맞춥니다.

5디렉토리 구조 해부

"폴더 이름이 곧 책임이다. js/ 아래 한 폴더 = 한 가지 일. main.js가 이들을 엮는다."

최상위 한눈에

birds.cafe/ ├── index.html ← importmap · HUD · 모바일 버튼 · 시작화면 ├── style.css ← 반응형 스타일(@media 900px 모바일 분기) ├── README.md ← 충실한 설명 문서(TrendShift엔 설명 없었지만 여긴 알참) ├── models/ ← 3D 에셋 │ ├── seagull.glb · 갈매기 (55KB, Poly Pizza 출처) │ └── paperplane.glb · 종이비행기 (3KB) └── js/ ← 전부 순수 ES 모듈 (빌드 없음) ├── main.js · 1,141줄 — 오케스트레이터(씬/렌더러/후보정/입력/오디오/루프) ├── flock.js · 522줄 — 편대 비행 물리(yaw/pitch/뱅킹/slerp/난류/NaN가드) ├── weather.js · 173줄 — 5 프리셋 + lerpWeather(~25 파라미터 보간) ├── ocean/ │ ├── Ocean.js · 151줄 — 카메라 추적 3×3 타일 수면 │ └── GerstnerWaves.js · 98줄 — 8파 GLSL 동적 생성(분산관계 c=√(g/k)) ├── clouds/ │ └── InstancedClouds.js · 226줄 — 인스턴싱(단일 드로 콜) ├── islands/ │ └── Islands.js · 67줄 — 절차적(procedural) 지형 생성 └── birds/ └── LowPolyBird.js· 145줄 — 절차적 저폴리 갈매기 + 날갯짓 애니메이션
용어
오케스트레이터(orchestrator) 패턴 · 도메인 모듈
한 파일(main.js)이 지휘자가 되어 전체 흐름(초기화·루프·이벤트)을 총괄하고, 실제 일은 각자 한 가지만 담당하는 도메인 모듈(flock·weather·ocean…)에 위임하는 구조입니다. 오케스트라에서 지휘자는 직접 악기를 연주하지 않고 단원들의 타이밍을 맞추죠. 이렇게 나누면 "바다가 이상하다 → ocean/만 본다", "새가 뒤집힌다 → flock.js만 본다"처럼 문제를 격리하기 쉽고, 모듈 하나를 통째로 교체하기도 쉽습니다. 작은 프로젝트지만 구조는 어른스럽습니다.

파일 크기로 본 무게중심

파일줄 수(약)역할
js/main.js1,141오케스트레이터 — 씬·렌더러·후보정·날씨 전환·입력·오디오·비/번개·애니 루프
js/flock.js522편대 비행 물리 — yaw/pitch 평활화·월드업 구속 뱅킹·쿼터니언 slerp·V_POSITIONS·난류·NaN 가드
js/clouds/InstancedClouds.js226인스턴싱 구름 — 단일 드로 콜로 다수 구름
js/weather.js1735 날씨 프리셋(dawn/day/dusk/night/storm) + lerpWeather 보간
js/ocean/Ocean.js151카메라 추적 3×3 타일 수면(Water + Gerstner)
js/birds/LowPolyBird.js145절차적 저폴리 갈매기 + 날갯짓
js/ocean/GerstnerWaves.js988파 Gerstner GLSL 동적 생성
js/islands/Islands.js67절차적 섬 지형

무게중심이 main.js(조립·루프)와 flock.js(비행 물리)에 쏠려 있습니다. 즉 이 프로젝트가 가장 공들인 건 "전체를 어떻게 매 프레임 돌리나"와 "새를 어떻게 자연스럽게 날리나"입니다. 그래픽 기법(Gerstner·인스턴싱)은 의외로 짧은 코드에 압축돼 있어, 오히려 학습용으로 읽기 좋습니다.

비유

이 폴더 구조는 영화 촬영장과 같습니다. main.js는 감독(전체 진행·컷 사인), weather.js는 조명·날씨 팀, ocean/은 수조 특수효과 팀, flock.js는 와이어 액션(새의 움직임) 팀, clouds/·islands/는 배경 미술 팀입니다. 각 팀은 자기 일만 하고, 감독이 "액션!"(animate())을 외치면 모두가 동시에 한 프레임을 만듭니다. 팀이 잘 나뉘어 있어, 한 팀의 작업물만 갈아끼우기 쉽습니다.

6학습 포인트 (기술별 — 배울 것 + 실습 아이디어)

"birds.cafe 한 레포 안에 셰이더·게임 수학·후처리·최적화·번들러리스 ESM이 다 들어 있다. 작아서 오히려 다 읽힌다."

① Gerstner 파동 — 해양 물리를 코드로

배울 것: 파동의 중첩(여러 파를 더함)·분산 관계(c=√(g/k))·가파름(steepness)이 파형에 미치는 영향. 정점을 수직만이 아니라 수평으로도 굴려 사실적 파도를 만드는 원리. 실시간 해양 렌더링의 기본기입니다.

실습: GerstnerWaves.js에서 파동 개수(8)를 2개로 줄여 보고, choppiness(뾰족함)와 steep 값을 키워 보며 잔잔한 호수 ↔ 거친 폭풍 바다가 어떻게 갈리는지 눈으로 확인.

② 쿼터니언 · slerp — 게임 수학의 핵심

배울 것: 오일러 각의 짐벌 락 문제와 쿼터니언이 그걸 어떻게 피하는지. slerp로 회전을 부드럽게 보간하는 법. "월드 업으로 구속"해 캐릭터가 뒤집히지 않게 하는 실전 트릭. 3D 게임/시뮬레이션이라면 어디서나 쓰입니다.

실습: flock.jsslerp 비율(8*dt)을 2*dt20*dt로 바꿔, 새의 선회가 둔해지거나 너무 휙휙 도는 차이를 체감. finalUp.y < 0.2 가드를 지우면 새가 어떻게 뒤집히는지도 관찰.

③ PMREM 환경맵 · 실시간 반사

배울 것: 왜 반사를 위해 환경을 "텍스처로 굽나"(PMREM). PMREMGenerator.fromScene(sky)로 동적 하늘을 환경맵으로 만들어 수면이 그 하늘을 반사하게 하는 법. 거칠기(roughness)별 프리필터링의 개념.

실습: 날씨를 바꿨을 때 수면 반사가 따라 바뀌는지 확인하고, 환경맵 재생성(re-bake) 주기를 늘리거나 줄여 보며 "반사 품질 vs 성능"의 트레이드오프를 느껴 보기.

④ 포스트프로세싱 — EffectComposer · UnrealBloom · ACES

배울 것: "기본 렌더 → 효과 패스 → 출력"으로 이어지는 후처리 파이프라인 구성법. UnrealBloom으로 빛을 번지게 하는 원리(밝은 부분 추출 → 흐림 → 합성). ACES 톤매핑이 화면을 "필름처럼" 만드는 이유.

실습: main.js에서 UnrealBloomPassstrength·threshold·radius를 조절해 발광이 과해지거나 사라지는 걸 관찰. toneMappingNoToneMapping으로 바꿔 ACES가 있고 없고의 색감 차이 비교.

⑤ atmospheric scattering — Sky로 하늘 그리기

배울 것: 하늘 색이 그림이 아니라 물리(빛의 산란)로 계산된다는 사실. turbidity(혼탁도)·rayleigh·태양 위치 파라미터가 새벽·정오·황혼·폭풍 분위기를 어떻게 만드는지.

실습: weather.js의 프리셋에서 turbidity와 태양 고도(sunPosition)를 바꿔 보며, 같은 Sky 셰이더로 전혀 다른 하늘이 나오는 걸 확인.

⑥ 인스턴싱 · 절차적 지오메트리 · 모바일 최적화

배울 것: InstancedMesh로 드로 콜을 줄이는 법(구름). 코드로 지형·새를 만드는 절차적 생성(Islands.js·LowPolyBird.js). IS_MOBILE 감지로 품질을 동적 조절하는 적응형 렌더링.

실습: 구름 수를 10배로 늘려 보고, 인스턴싱일 때와 (일부러) 개별 메시로 바꿨을 때 프레임 차이를 측정. 모바일 분기에서 낮추는 항목(픽셀비/블룸/섀도)을 하나씩 꺼 보며 영향 비교.

⑦ 번들러리스 ESM · importmap

배울 것: 빌드 도구 없이 import로 모듈을 쪼개고, importmap으로 'three' 같은 별칭을 CDN URL에 매핑하는 법. "정적 서버 하나로 끝나는" 배포의 단순함과, CDN 의존이라는 트레이드오프.

실습: index.html의 importmap에서 Three.js 버전(r160)을 다른 버전으로 바꿔 보고, 호환성 문제가 어디서 터지는지(애드온 경로 변경 등) 추적. 의존성을 로컬 파일로 내려받아 importmap을 로컬 경로로 바꿔 보기.

7하드웨어 / 시스템 요구사항

"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·텍스처를 받느라 잠깐 검은 화면이 보일 수 있는데, 이는 정상입니다. 사운드는 브라우저 정책상 사용자가 한 번 클릭/터치한 뒤에야 재생됩니다.

8직접 해볼 수 있는 실습 과제

"띄워서 날려 보기 → 날씨/편대 손대기 → 파동 만지기 → 입력 추가 → 비행 로직 통째로 교체, 난이도별로."

과제 1난이도 ★☆☆☆☆ · 입문

로컬에서 띄우고 날려 보기

git clone → 프로젝트 폴더에서 python -m http.server 실행 → 브라우저로 http://localhost:8000 접속. 갈매기를 조종해 바다 위를 날아 보고, 날씨가 바뀌는 걸 관찰한다. (검은 화면이면 7장의 file:// 함정을 의심.)

목표: "번들러 없이 정적 서버만으로 3D가 돈다"를 체감. 개발자 도구(F12) Network 탭에서 Three.js·에셋이 CDN에서 로드되는 걸 확인하는 게 포인트.

과제 2난이도 ★★☆☆☆ · 초급

편대 모양 바꾸기 + "노을" 날씨 추가

(a) flock.jsV_POSITIONS 좌표를 수정해 V자 편대를 W자나 원형으로 바꿔 본다. (b) weather.js에 기존 프리셋을 본떠 "노을(sunset)" 프리셋을 하나 추가하고, 전환 목록에 끼워 넣는다.

목표: "데이터(좌표·파라미터)만 바꾸면 동작이 바뀐다"는 데이터 주도 설계를 손으로 확인. 분기 코드를 건드리지 않고도 새 날씨가 매끄럽게 보간되는 걸 보는 게 핵심.

과제 3난이도 ★★★☆☆ · 중급

Gerstner 파동으로 '잔잔한 호수 ↔ 폭풍' 만들기

GerstnerWaves.js의 파동 개수·steepness·choppiness·파장을 조절해, 거울처럼 잔잔한 수면과 거칠게 부서지는 폭풍 바다를 각각 만들어 본다. 값을 날씨 프리셋과 연동해 "폭풍일 때 자동으로 파도가 거세지게" 묶어 본다.

목표: 파동 파라미터가 파형에 미치는 영향을 직관적으로 이해. 분산 관계(c=√(g/k))를 바꾸면 파도 속도가 어떻게 달라지는지도 실험.

과제 4난이도 ★★★★☆ · 중상

모바일 자이로(기울기) 입력 추가하기

모바일에서 DeviceOrientationEvent(폰 기울기)를 받아, 폰을 기울이면 갈매기가 그 방향으로 선회하도록 입력 계층을 확장한다. 기존 터치 컨트롤과 충돌하지 않게 토글로 전환.

목표: 기존 입력 파이프라인(키보드/터치)에 새 입력원을 붙이는 법, 그리고 자이로 값을 yaw/pitch 목표치로 매핑하는 감각을 익히기. 권한 요청(iOS)도 다뤄 보기.

과제 5난이도 ★★★★★ · 고급

진짜 boids로 비행 로직 교체 + 새 모델 갈아끼우기

(a) 현재의 "리더 추종 V자 편대"를 고전 boids 알고리즘(분리·정렬·응집)으로 교체해, 새들이 자율적으로 무리를 짓게 한다. (b) seagull.glb를 다른 GLB 모델로 바꾸고, 모델의 기본 방향이 달라 생기는 회전 어긋남을 modelCorrectionQ(보정 쿼터니언)로 맞춘다.

목표: 편대 비행을 떠받치는 물리/수학을 통째로 갈아끼워 보며 진짜 군집 시뮬레이션을 구현. 모델 로딩 후 좌표계·축 보정이라는, 3D에서 늘 만나는 실전 함정을 직접 해결하기.

9관련 기술 심화 학습 로드맵 (6주 플랜)

"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를 만들 수 있게 됩니다.

10핵심 키워드 사전

"이 문서와 저장소에서 반복되는 그래픽·게임 수학 용어를 한곳에 모았다."

용어의미
Three.jsWebGL을 쉽게 다루게 해 주는 JS 3D 라이브러리. birds.cafe는 r160 버전을 CDN으로 사용
WebGL브라우저가 GPU에 직접 그림을 그리게 하는 저수준 그래픽 API. Three.js의 토대
Gerstner waves물 입자를 원 운동시켜 사실적 파도(뾰족한 꼭대기·평평한 골)를 만드는 파동 모델. birds.cafe가 GLSL로 직접 구현
GLSL vertex shaderGPU에서 정점 위치를 계산하는 셰이더 언어/단계. 수만 정점의 출렁임을 병렬 처리
PMREM 환경맵거칠기별로 미리 흐림 처리한 환경맵. 수면·금속의 사실적 반사를 싸게 계산(PMREMGenerator.fromScene)
UnrealBloom밝은 부분을 추출·흐림·합성해 빛이 번지게 하는 후처리 효과. 영화적 발광의 정체
ACES 톤매핑HDR을 화면(LDR)에 자연스럽게 압축하는 색 변환. 헐리우드 영화가 쓰는 방식
atmospheric scattering빛의 산란을 물리로 계산해 하늘 색을 그리는 기법. Three.js Sky 애드온이 사용
quaternion slerp3D 회전을 4숫자로 표현(쿼터니언)하고, 두 회전을 최단 경로로 부드럽게 보간(slerp). 짐벌 락 회피
V-formation / flockingV자 편대 비행 / 무리 짓기. 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 callGPU에 "이걸 그려라"라고 보내는 명령 한 번. 많을수록 느려져, 인스턴싱으로 줄인다
EffectComposer / pass여러 후처리 단계(pass)를 순서대로 엮는 Three.js 파이프라인. 렌더→블룸→출력
분산 관계 (c=√(g/k))파동의 위상 속도와 파수의 관계. "긴 파도가 빠르다"는 실제 해양 물리를 코드화
HTMLAudioElement<audio> 태그 기반 단순 오디오 재생. Web Audio API 대신 채택(모바일 호환·단순)
번들러리스 ESMwebpack·Vite 없이 브라우저 표준 ES 모듈만으로 코드를 쪼개고 로드하는 방식
LICENSE 미지정birds.cafe엔 라이선스 파일이 없음 → 법적으로 모든 권리 저자 보유. 학습은 가능하나 코드 재사용·재배포는 저자 확인 필요

11참고 링크