민들레 홀씨
민들레를 불면 홀씨가 하나씩 떨어져 갓털을 빙글 돌리며 바람을 타고 흩어진다.
프롬프트를 복사해 claude.ai 에 붙여 넣으면 바로 실행됩니다 · Opus 5.5 · Effort High 기준
- 권장 모델
- Claude Opus 5.5 · Effort High
- 화면비
- 9:16 (세로)
- 조작
- 화면을 쓸어 불기(마이크 입김도 된다) · 끌어서 시점 돌리기
- 결과물
- HTML 파일 하나 · three.js 를 인터넷에서 불러옵니다
프롬프트
Claude 가 읽는 제작 지시문입니다. 맨 위 [바꿀 곳]의 값을 바꿔 붙여 넣어도 됩니다.
해 질 녘 들판에 선 민들레 한 송이를 누르거나 쓸어서 불면, 홀씨가 하나씩 떨어져 갓털을 빙글 돌리며 바람을 타고 천천히 흩어지는 세로(9:16) 3D 장면을 HTML 한 파일로 만들어 줘.
핵심 아이디어: 화면 가운데 위쪽에 역광을 받은 민들레 홀씨 공(지름 약 0.5 S)이 지평선에 걸려 있다. 바람은 꽃 뒤에서 보는 사람 쪽으로 분다. 첫 바람이 t=1.2초에 저절로 불어 홀씨 40개 안팎이 하나씩 떨어지고, 그중 한 개(주인공 홀씨)가 t=4.0초에 카메라 앞 3.25 거리까지 날아와 화면 오른쪽 아래(가로 0.68, 세로 0.62)에서 갓털 지름 약 0.38 S 로 크게 보이며 초당 0.5바퀴 빙글 돌면서 천천히 내려앉는다. 이것이 이 작품의 시그니처 순간이다. 그 뒤로는 보는 사람이 불 때마다 홀씨가 하나씩 떠나고, 거의 다 떠나면 꽃이 다시 홀씨를 채운다.
[바꿀 곳]
CONFIG 맨 위에 아래 이름과 기본값으로 둔다.
- SEED_COUNT: 260 — 한 송이에 붙은 홀씨 수
- POOL_SIZE: 640 — 홀씨 인스턴스 전체 수(붙은 것 + 나는 것 + 땅에 앉은 것)
- BREEZE: [0.12, 0.04, 0.30] — 기본 바람(세계 단위/초, 꽃 뒤 → 카메라 쪽)
- FALL_SPEED: [0.18, 0.26] — 홀씨마다 고르는 낙하 속도 범위
- SPIN_REV: [0.35, 0.8] — 갓털 회전(바퀴/초) 범위, 방향은 홀씨마다 무작위
- FIRST_GUST_AT: 1.2 — 저절로 부는 첫 바람 시각(초)
- HERO: true — 주인공 홀씨 연출 켜기
- REGROW_SEC: 5 — 다시 채우는 데 걸리는 시간
- SEED: 20261009 — 난수 시드
- SKY_TOP: #A9B7BC, SKY_HORIZON: #E9DDC6, SUN_GLOW: #F6E3BE
- PAPPUS_COLOR: #F4F0E6, ACCENT_YELLOW: #E9B92F
[크기]
S = 화면 짧은 변(세로 화면에서는 가로 폭). 세계 단위로 짜되 첫 화면에서 아래 비율이 나오게 맞춘다.
- 홀씨 공: 꽃턱 반지름 0.18, 홀씨 하나의 길이 0.70(씨 0.12 + 가는 부리 0.50 + 갓털 우산 0.08), 갓털 우산 반지름 0.32. 공 바깥 지름 약 1.9. 카메라 거리 7.0, 세로 시야각 50°에서 9:16 화면의 보이는 폭은 7.0 × 2 × tan25° × 0.5625 ≈ 3.67 이므로 공 지름은 1.9 / 3.67 ≈ 0.52 S. 화면 위에서 0.30~0.59 높이, 가로 0.24~0.76 을 차지한다.
- 주인공 홀씨: 카메라에서 3.25 떨어진 곳의 보이는 폭은 3.25 × 0.5246 ≈ 1.70, 갓털 지름 0.64 / 1.70 ≈ 0.38 S. 화면 자리는 가로 0.68, 세로 0.62(t=4.0).
- 꽃대: 땅(y=0)에서 꽃턱 중심 y=3.0 까지. 꽃대 밑동은 화면 세로 약 0.9 자리.
- 지평선은 화면 위에서 약 0.43, 즉 홀씨 공이 지평선에 걸려 하늘을 등진다. 아래 57% 는 풀밭, 위 43% 는 하늘과 날아가는 홀씨.
[장면]
- 하늘: 반지름 80 구, 안쪽 면, ShaderMaterial 로 세로 그라데이션(위 SKY_TOP → 지평선 SKY_HORIZON) + 해 방향 둘레의 부드러운 SUN_GLOW(지수 감쇠, 해 원반 경계 없음). 해는 꽃 뒤 왼쪽, 지평선 위 8° 높이. 안개(fog) 영향 받지 않음.
- 땅: 반지름 60 원판, 색 #737D52, roughness 1. 풀잎 7000개를 InstancedMesh 로(잎 하나 = 꼭짓점 5개 가늘어지는 삼각형 3장, 높이 0.25~0.6, 꽃에서 반지름 1.2~30 사이), setColorAt 으로 #6E7A4E ~ #A4A86A 사이 색. 풀은 움직이지 않는다.
- 민들레 본체: 꽃대 = 반지름 0.035 원기둥 #7C8A55, 밑동을 축으로 바람 세기만큼 최대 0.06 rad 기운다(홀씨 공도 함께). 꽃턱 = 반지름 0.18 구 #B9A27A, 홀씨가 빠진 자리처럼 작은 어두운 점이 박힌 CanvasTexture. 밑동에 톱니 가장자리 잎 6장(THREE.Shape, #5F6E42)이 로제트로 눕는다.
- 홀씨 하나(로컬 +Y 축): 씨 = y 0→0.12, 반지름 0.012 삼각기둥 #8A6B4A / 부리 = y 0.12→0.62, 반지름 0.005 삼각기둥 / 갓털 = 꼭짓점 y 0.62, 중간 고리(r 0.16, y 0.665), 바깥 고리(r 0.32, y 0.70)를 잇는 20분할 얕은 우산. 갓털은 직접 만든 BufferGeometry 로, UV 를 위에서 내려다본 평면 투영(u = 0.5 + 0.5·x/0.32, v = 0.5 + 0.5·z/0.32)으로 준다.
- 갓털 텍스처: 256×256 투명 캔버스에 중심에서 뻗는 털 44가닥(길이 0.82~1.0 무작위, 선 굵기 1.3px, 흰색 알파 0.8), 털 끝 40% 구간에 4~6개의 짧은 잔털(4~8px, ±25°). 털 길이와 잔털 수가 고르지 않아서 돌 때 회전이 눈에 읽혀야 한다. 밉맵 켬.
- 홀씨 두 InstancedMesh(몸통 = 씨+부리, 갓털)가 같은 행렬을 쓴다. 쓰지 않는 칸은 크기 0 행렬.
- 붙은 홀씨 자리: 피보나치 구 260방향(작은 흔들림 포함). 붙은 홀씨는 꽃턱 표면(중심 + 방향×0.18)에서 +Y 를 그 방향으로 둔다. 홀씨마다 버티는 힘 h ∈ [0.2, 1.0].
- 배경: 거리 6~25 에 작은 홀씨 공 30개(반지름 0.25~0.4 구에 같은 털 텍스처, 투명, 가는 꽃대), 노란 민들레꽃 80송이(반지름 0.08~0.12 납작한 원판, 꽃잎 CanvasTexture, ACCENT_YELLOW). 첫 화면부터 공중에 홀씨 12개가 거리 2~12 에 흩어져 돌며 떠 있다 — 빈 하늘로 시작하지 않는다.
바람과 불기
- 바람장 w(p,t) = BREEZE·(1 + 0.35·sin(0.21t)) + (0.20·sin(0.55p.z + 0.43t + 1.3), 0.18·sin(0.61p.x + 0.37t) + 0.04, 0.20·sin(0.47p.x + 0.52t + 2.1)) + 숨 B(t)·숨방향·1.8.
- 숨 B(t)(0~1.2)는 모든 입력을 더한 값, 숨방향은 가장 센 입력의 방향(단위 벡터, 위로 0.25 섞음).
- 떨어지는 규칙: 초당 떨어지는 수 = 붙은 수 × 0.5 × max(0, B − 0.08)². 소수는 누적해 1이 넘을 때마다 점수가 가장 높은 홀씨 하나를 놓는다. 점수 = 0.7·(0.5 − 0.5·dot(홀씨방향, 숨방향)) + 0.3·(1 − h) + 0.05 난수. 그래서 바람 맞는 쪽부터 하나씩 떠난다.
- 떠날 때 첫 속도 = 숨방향×(0.6 + B) + 홀씨방향×0.3 + (0, 0.35, 0). 0.8초 동안 몸 방향이 바깥쪽에서 위쪽(갓털 위, 씨 아래)으로 쿼터니언 slerp.
- 나는 동안: v += (w + (0, −낙하속도, 0) − v)·(1 − exp(−1.6·dt)). 축은 위에서 0.12~0.25 rad 기울고 그 기울기 방향이 회전의 0.3배로 세차 운동하며, 수평 상대 속도 반대쪽으로 최대 0.3 rad 더 기운다. 갓털은 자기 축으로 SPIN_REV 만큼 계속 돈다.
- 땅(y ≤ 0.02)에 닿으면 멈추고, 축을 0.6 rad 무작위로 눕히고, 회전은 0.5초에 걸쳐 멎는다. 10초 뒤 1.5초 동안 줄어들고 칸을 비운다. |x| 또는 |z| > 40, y > 25 이면 바로 비운다.
- 저절로 부는 바람: FIRST_GUST_AT 에 B 펄스 1.05(감쇠 시간 0.8초, 방향 = BREEZE 쪽) → 4초까지 홀씨 35~50개가 떠난다. 그 뒤 6~10초마다(시드 난수) B 0.35 펄스(감쇠 1.0초)로 서너 개씩.
- 주인공 홀씨(HERO): 꽃턱 앞쪽 오른쪽 위의 한 홀씨를 t=1.4초에 놓고, 4.0초까지 2차 베지어(P0 = 붙은 자리, P1 = P0 + (0.6, 0.6, 1.2), P2 = 꽃 중심 + (0.3, −0.4, 3.8) 을 t=4.0 의 자동 yaw(≈0.147)만큼 꽃 중심의 세로축 둘레로 돌린 점)를 easeInOutSine 으로 따른다. 이때 카메라(자동 pitch ≈0.092)와의 거리는 약 3.25. 2.5~7초 동안 축을 카메라 쪽으로 0.7 rad 기울여 우산 안쪽의 털이 비스듬히 보이게 한다(옆에서 납작한 선으로 보이면 실패). 4초 뒤에는 낙하 0.12, 바람 따름 계수 0.4 로 약 3초 동안 화면에 머물며 0.5바퀴/초로 돈다. 그다음은 보통 홀씨와 같다.
- 다시 채우기: 붙은 수가 SEED_COUNT 의 10% 아래이고 2초 동안 숨이 0.08 아래면, 빈 자리를 무작위 순서로 REGROW_SEC 동안 채운다(하나마다 0.5초에 크기 0→1). 빈 칸이 없으면 가장 오래 땅에 앉은 것을, 그것도 없으면 카메라에서 가장 먼 나는 홀씨를 거둔다.
입력(불기)
- 짧게 누르기(250ms 안, 10px 안 움직임) = 훅 한 번: B 펄스 0.9, 감쇠 0.35초, 방향 = 카메라에서 누른 점을 지나는 광선(약 13개가 떠난다).
- 꾹 누르기(250ms 넘게, 10px 안) = 입김: B 가 1.2초에 걸쳐 0→1.0, 떼면 0.3초에 0. 방향 = 카메라 → 꽃.
- 쓸기(10px 넘게 끌기) = 시점 회전 + 돌풍: B = min(1, 끄는 속도 / (1.5 S/초)), 방향 = 끄는 방향을 카메라 오른쪽·위 벡터로 옮긴 것.
- 마이크: 화면 아래 가운데(바닥에서 4%) 작은 반투명 알약 단추 「마이크로 불기」(13px). 누르면 getUserMedia 로 소리 크기(RMS)를 재어 B = clamp((rms − 0.02) / 0.15, 0, 1), 방향 = 카메라 → 꽃. 거절·미지원·막힘이면 단추 글자를 「마이크를 쓸 수 없어요 · 화면을 눌러 불어 보세요」로 바꾸고 3초 뒤 흐리게. 모든 Promise 에 catch 를 단다. 단추의 pointer 이벤트는 캔버스로 넘기지 않는다.
- 단추 위에 안내 한 줄 「눌러서 · 꾹 눌러서 · 쓸어서 불기」를 5초 뒤 1초에 걸쳐 지운다. 그 밖의 글자는 없다.
[카메라]
- PerspectiveCamera near 0.05, far 200. 바라보는 점 T = (0, 2.6, 0), 거리 D = 7.0.
- 시야각은 짧은 변 S 기준 크기를 지키게 창 비율 a(가로/세로)로 정한다: a ≤ 0.5625 이면 세로 50°, 0.5625 < a ≤ 1 이면 tan(세로 반각) = 0.2623 / a, a > 1 이면 tan(세로 반각) = 0.2623(세로 약 29.4°). 어느 창에서나 D=7 에서 짧은 변이 약 3.67 이상 넓어지지 않는다. 창 크기가 바뀔 때마다 다시 계산한다.
- 위치 = T + D·(cos pitch·sin yaw, sin pitch, cos pitch·cos yaw). 첫 화면 yaw 0, pitch 0.06 → 카메라는 (0, 3.02, 6.99), 꽃 앞에서 거의 같은 높이로 본다.
- 입력이 없을 때: yaw = 0.25·sin(2π·t/40), pitch = 0.06 + 0.04·sin(2π·t/27). t=0 에서 첫 화면 값과 같다.
- 끌기: yaw −= dx/S × 2.2, pitch += dy/S × 1.6. 범위 yaw ±1.4, pitch −0.04 ~ 0.55. 손을 떼고 3초 뒤 2초에 걸쳐(지수 보간, 계수 1.5/초) 자동 움직임 값으로 돌아간다.
- 흔들림·줌은 없다. 카메라가 지면 아래로 가지 않는다.
[빛]
- renderer.toneMapping = ACESFilmicToneMapping, toneMappingExposure 1.0, outputEncoding = sRGBEncoding, antialias true, 배경색 SKY_HORIZON.
- HemisphereLight: 하늘 #DCE3E4, 땅 #6F7350, 세기 0.55.
- DirectionalLight(해): 색 #FFE2B8, 세기 1.2, 위치 (−6, 3, −14) → 원점. 꽃 뒤 왼쪽에서 오는 역광. 그림자는 쓰지 않는다.
- Fog: 색 SKY_HORIZON, near 12, far 50.
- 재질: 땅·풀·잎·꽃대 MeshStandardMaterial roughness 0.9~1.0, metalness 0. 씨 몸통 MeshStandardMaterial #8A6B4A roughness 0.8. 갓털 MeshBasicMaterial(PAPPUS_COLOR, 털 텍스처, transparent, opacity 0.9, depthWrite false, DoubleSide, fog 켬) — 빛과 무관하게 늘 밝은 흰 털.
[구현]
- HTML 파일 하나로 만든다. 라이브러리는 three.js 한 줄만 쓴다: <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> — 이 r128 판에 없는 것(모듈 import, OrbitControls 같은 확장 파일, CapsuleGeometry, outputColorSpace)은 쓰지 않는다. 그 밖의 바깥 파일·글꼴·네트워크 요청은 쓰지 않는다.
- three.js 다음 <script> 맨 앞에 CONFIG 객체를 두고 [바꿀 곳]의 값과 이 글에 나온 숫자(크기·색·빛·속도)를 이름 붙여 모은다.
- 장면은 requestAnimationFrame 의 타임스탬프로 구한 시간 t(초)로 진행한다. 한 프레임에 밀린 시간은 0.1초까지만 반영한다. setTimeout·setInterval·CSS 애니메이션으로 움직이지 않는다.
- 난수는 시드 고정 난수(mulberry32 등)만 쓴다. Math.random 은 쓰지 않는다.
- 텍스처는 코드로 만든다(CanvasTexture·DataTexture). 바깥 그림 파일을 쓰지 않는다.
- 캔버스는 늘 창 전체 크기다. 창 크기가 바뀌면 카메라 비율과 렌더러 크기를 다시 맞춘다. 스크롤이 생기지 않게 한다. renderer.setPixelRatio 는 2 까지만.
- 입력이 없으면 카메라가 스스로 천천히 움직인다. 마우스로 끌거나 손가락으로 끌면 시점이 돈다(가로 = 좌우 회전, 세로 = 위아래 기울기, 범위 제한). 손을 떼고 3초 뒤 자동 움직임으로 부드럽게 돌아간다. 터치는 페이지 스크롤·확대가 일어나지 않게 막는다(touch-action: none).
- 시점 각도를 window.__view = { yaw, pitch } (라디안)에 두고 매 프레임 갱신한다. 카메라는 이 두 각도로 놓는다 — 자동 움직임도 끌기도 이 각도를 바꾼다.
- 브라우저 저장소(localStorage·sessionStorage·IndexedDB·쿠키)를 쓰지 않는다 — 미리보기 칸에서 막혀 오류가 난다.
- 탭이 숨으면 멈추고 돌아오면 이어서 한다.
- WebGL 을 못 쓰는 기기에서는 오류 대신 화면에 짧은 안내 문구를 띄운다.
- 글자를 쓰면 시스템 글꼴만: 'Pretendard', 'Apple SD Gothic Neo', 'Malgun Gothic', 'Noto Sans KR', sans-serif.
- 모든 함수·상수를 먼저 정의하고, 시작은 스크립트 맨 끝에서 한 번만 부른다.
- 콘솔에 오류가 나지 않게 한다. 스스로 대조하는 표는 console.table, 어긋남은 console.warn 으로만 낸다.
이 작품만의 구현
- 홀씨 상태는 칸마다 하나의 Float32Array 묶음(위치·속도·축·회전각·회전속도·낙하속도·나이·상태: 비었음/붙음/남/앉음/자람)으로 두고, 매 프레임 Object3D 하나를 재사용해 두 InstancedMesh 의 행렬을 갱신한 뒤 instanceMatrix.needsUpdate. 프레임마다 새 객체를 만들지 않는다.
- 회전: 최종 방향 = (축 기울기 쿼터니언) × (축 둘레 회전각 쿼터니언). 회전각 += 2π·SPIN_REV·dt.
- 포인터는 pointerdown/move/up/cancel 하나로 마우스·터치를 함께 다루고 setPointerCapture 를 쓴다. 누름·꾹 누름·쓸기 구분은 [장면]의 입력 규칙대로.
- 마이크는 단추를 누른 뒤에만 AudioContext 를 만든다. 실패해도 나머지 조작은 그대로 돈다.
- 합격 기준(기본 설정): 시작 때 console.table 로 ① 붙은 홀씨 수 = 260 ② 첫 화면 홀씨 공 지름 / S = 0.45~0.58(공의 바깥 구를 투영해 잰다) ③ 풀 크기 ≤ 640 ④ 홀씨 100개를 기본 바람으로 y=3.0 에서 dt 1/30 로 모의 비행시킨 착지 시간 중앙값 8~22초. t=6초에 두 번째 표로 ⑤ 4초까지 꽃에서 떠난 홀씨 35~50개(처음부터 떠 있던 12개는 빼고 주인공은 넣는다) ⑥ 주인공 홀씨의 카메라 최단 거리 3.0~3.5, 그때 갓털 지름 / S 0.33~0.43 ⑦ renderer.info.render.triangles ≤ 300,000. 어긋난 줄마다 console.warn 을 한 번 낸다.
- 성능: 그림자·후처리 없음, 전체 삼각형 약 10만 이하(풀 21,000 + 홀씨 640 × 약 75 + 나머지). 결과 파일은 100KB 안쪽.
[하지 말 것]
- 꽃가루·먼지·반짝이 같은 장식 입자를 더하지 않는다 — 화면에 떠 있는 것은 홀씨뿐이어야 홀씨가 읽힌다.
- 블룸·렌즈 플레어·빛줄기·발광 테두리를 넣지 않는다 — 역광의 흰 털 자체가 밝음을 맡고, 후처리는 폰에서 느려진다.
- 홀씨에 꼬리·궤적선을 그리지 않는다 — 천천히 도는 갓털이 주인공이다.
- 새·나비·벌레·사람·캐릭터를 넣지 않는다 — 장면의 생명은 바람과 홀씨다.
- 하늘을 짙은 남색이나 고채도 노을로 칠하지 않는다 — 바탕은 채도 낮은 색이고 강조색은 노란 꽃과 흰 털 둘뿐이다.
- 화면 모서리에 제목·점수·조작 설명판을 두지 않는다 — 글자는 단추와 5초짜리 안내 한 줄뿐이다.
- 카메라 흔들림·자동 줌을 넣지 않는다 — 고요하게 내려앉는 느낌이 깨진다.
- 마이크 권한을 페이지가 열리자마자 묻지 않는다 — 미리보기 창에서 막히고 오류가 난다. 단추를 눌렀을 때만 묻는다.
- 갓털을 낱개 Mesh 수백 개로 만들지 않는다 — 반드시 InstancedMesh 두 개로 그린다.
- 홀씨가 한꺼번에 우르르 떨어지게 하지 않는다 — 언제나 점수 순으로 하나씩 놓는다.
사용 방법
필요한 것: claude.ai 계정 — 무료 계정에서도 Claude Opus 5.5 를 고를 수 있습니다
- claude.ai 에서 새 대화를 엽니다.
- 모델 메뉴에서 Claude Opus 5.5 를 고르고, 같은 메뉴의 Effort(답변 깊이)를 High 로 둡니다.
- 복사한 프롬프트를 붙여 넣고 전송합니다. 결과로 나온 HTML 파일을 브라우저로 열면 바로 해 볼 수 있습니다.
추가 요청 예시
결과가 나온 뒤 같은 대화에서 이어서 요청하면 해당 부분만 수정됩니다.
- 홀씨가 다 날아가면 새 민들레가 피어나게 해 줘
- 들판을 해 질 녘 노을빛으로 바꿔 줘
- 바람 세기를 고르는 단추를 하나 달아 줘
3D