블록-툰 스토리 비디오 ("Busted by the Shoeshine Man" 스타일)
사용자가 세로(720x1280 또는 1080x1920, 30fps) 1인칭 스토리 비디오를 원할 때 사용합니다. 영상 길이는 30-130초이며, 음성 해설 위에 덧입혀지고, 통통한 둥근 블록 3D 캐릭터, 부드럽고 연속적인 움직임, 강렬한 컬러 캡션이 특징입니다. 모든 것은 코드로 처리됩니다: 헤드리스 Chromium 기반 three.js 씬, 하나의 결정론적 frame(t) 함수, ffmpeg로 파이프된 스크린샷.
사용자의 최우선 순위는 캐릭터 디자인의 충실도와 애니메이션의 부드러움입니다. 5단계의 캐릭터 확인을 건너뛰지 마십시오.
1. 참조 비디오의 모습 (측정 기준)
- 720x1280, 30fps, ~122초, AAC 스테레오 약 -14 LUFS, 음성 아래 조용하고 연속적인 배경 사운드.
- 약 47개의 하드 컷(단일 프레임 컷, 크로스페이드 없음), 평균 샷 길이 ~2.6초(범위 1.2-5초). 샷 내부에서 카메라는 항상 움직입니다: 이징된 푸시인, 풀아웃, 크레인/하이앵글 드리프트, 로우앵글 푸시, 천장이나 창문으로의 휩틸트. 정적 프레임은 1% 미만입니다.
- 캡션은 하단 1/3(상단 약 64-68% 높이)에 위치하며, 그룹당 2-3개의 소문자 단어, 굵은 둥근 기하학적 산세리프체(Poppins ExtraBold), 흰색 바탕에 키워드는 컬러(금색 #f5c32c, 청록색 #48c9f0, 산호색 #ff5b4d, 분홍색 #ff7ac0, 녹색 #2ecc8f, 주황색 #ffa21f)로 표시, 두꺼운 검은색 외곽선, 각 그룹에 작은 스케일 팝 효과.
- 환경: 따뜻한 크림/탠 인테리어(벽 #dcbf96, 바닥 #c7aa7d, 돌 #d9c39b, 판벽 #b99a6f, 하늘 #c7dcec)에 악센트 색상 자주색 #570d0e, 남색 #1c2a49, 금색 #e0990d, 청록색 #1f8f8c, 빨간색 #a3160f.
- 조명: 부드럽고 밝으며 약간 따뜻함; 부드러운 그라데이션 음영과 부드러운 투사 그림자. 하드 셀 밴드 없음.
2. 캐릭터 디자인 바이블 (정확히 따를 것)
실루엣 및 비율
- 모든 신체 부위는 둥근 상자입니다. 베벨 반경은 부위의 가장 작은 치수의 약 15-18%입니다. 신체 부위에 구체나 캡슐을 사용하지 마십시오.
- 머리: 가로가 세로보다 약간 더 넓은 직육면체(0.44 x 0.41 x 0.39 단위), 클로즈업 샷에서 인물 높이의 약 1/4-1/3이므로 와이드 샷에서 인물은 약 4등신입니다. 짧은 목 블록. 몸통 0.46 x 0.56 x 0.26. 팔 0.27 + 0.26(위팔 + 아래팔). 다리 0.30 + 0.27. 총 높이 약 1.7 단위.
- 손은 통통한 벙어리 장갑 블록입니다: 손바닥 블록에 네 개의 짧은 두 마디 손가락과 별도의 엄지손가락. 손은 펴서 주먹을 쥐거나 벌릴 수 있습니다. 손이 거미줄처럼 가늘어 보이지 않아야 합니다.
- 각 손목에 흰색 커프스 밴드. 검은색 신발, 평평한 블록에 둥근 발가락.
- 보이는 관절 끊김 없음. 두 개의 둥근 상자가 구부러진 관절(엉덩이, 무릎, 어깨, 팔꿈치, 손목, 목/몸통, 엉덩이/몸통)에서 만날 때 베벨이 벌어져 틈이나 노치가 생겨 끊김으로 읽힙니다. 모든 관절을 옷과 같은 색상의 천 레이어 아래에 숨깁니다: 관절 피벗을 중심으로 한 작은 구(
ball())로, 팔다리 절반 너비 + ~0.002 크기로 하여 팔다리를 따라 늘리고(sc[1,1.2-1.4,1]), 두 엉덩이를 연결하는 골반 블록, 목/몸통 이음새 위의 칼라 블록, 커프스 아래의 피부색 손목 조각. 공을 너무 크게 하지 마십시오(팔다리 절반 너비 + 0.015로 하면 무릎과 팔꿈치가 덩어리짐). 구부린 포즈(팔 올리기, 걷기 중간 다리 들기)로 스트레스 테스트를 하고 확대하여 확인하십시오.
얼굴 (머리 앞면에 평면으로 칠해지며, 매 프레임 캔버스 텍스처에 다시 그려짐)
- 크고 둥근 눈: 흰색 공막, 녹색 또는 갈색 홍채, 검은색 동공, 흰색 하이라이트. 두꺼운 검은색 위쪽 눈꺼풀 라인. 눈은 움직일 수 있고(lookX/lookY), 깜빡이고 가늘게 뜰 수 있습니다.
- 두껍고 가늘어지는 눈썹으로 기울어지고(화남/걱정) 올라갑니다. 검은색 원형 안경은 눈보다 훨씬 큽니다(링 반경은 눈 반경의 약 2배). 안경은 하나의 연결된 프레임이어야 합니다: 두 개의 링은 코 위로 곡선 브릿지 아크로 연결되고 각 링에는 얼굴 가장자리까지 이어지는 템플 라인이 있습니다. 두 개의 떠 있는 링은 부서져 보입니다(첫 번째 캐스트 시트에서 사용자가 플래그를 지정했습니다).
- 코 없음. 입은 아주 작은 선/곡선이거나, 말할 때 분홍색 혀가 있는 채워진 열린 입입니다. 미소 값 -1..1.
- 선택적 부드러운 블러셔.
머리카락, 모자, 액세서리 (쌓인 통통한 블록)
- 주인공: 뾰족한 부분이 있는 갈색 퀴프, 사이드 피스, 뒷 블록. 기타: 짧거나 긴(사이드 커튼 + 뒷 블록). 검은색 프린지는 얇은 각진 블록.
- 경찰 모자: 남색 크라운 블록 + 검은색 바이저 + 금색 배지. 납작 모자: 갈색 블록 + 짧은 챙 + 밝은 버튼.
- 노인: 흰색 블록 콧수염 + 흰색 관자놀이 패치.
- 복장은 색상 블록으로 단순함: 정장(자주색 재킷, 흰색 셔츠, 어두운 넥타이, 옷깃, 금색 코드 V + 파란색 줄무늬 흰색 ID 배지), 제복(남색, 금색 배지, 검은색 벨트 + 금색 버클), 앞치마(흰색 셔츠, 단추 점이 있는 갈색 앞치마), 엑스트라용 회색/녹색/탠/남색/청록색 변형.
- 피부 톤: 피치 #efaa71, 황갈색 #d89a62, 갈색 #8a5230, 어두운색 #5a3320, 밝은색 #f4c49a. 머리카락 갈색 #59321a, 검은색 #14110f.
렌더링
- 모든 부위에 역-외피 쉐이더를 통한 얇고 어두운 잉크 외곽선(#1c0f06), 화면 일정 너비. 너비는 카메라 거리의 분수입니다:
w = px / ((H/2) / tan(fov/2))여기서px는 원하는 선 두께(화면 픽셀 단위, 720x1280 비디오 샷의 경우 약 0.003, 약 1.5-2px). 먼 거리의 다중 캐릭터 시트(1600x900)의 경우K.setOutlineWidth(.0009)를 사용하고, 흉상 타일의 경우 약.0016; 기본값은 먼 카메라에서 너무 두껍습니다. 얼굴과 작은 트림 조각은 외곽선을 건너뜁니다. - Lambert 재질, 반구 조명(약 2.9) + 하나의 부드러운 그림자 태양(약 1.5). 부드러운 PCF 그림자.
- 배경 군중은
lod: 1사용(손가락 없음, 머리카락 블록 적음). 엑스트라마다 고유한 디자인을 만들지 마십시오. 피부, 머리카락, 복장 색상 및 헤어스타일을 변경합니다.
다양성 규칙: 모든 지명 캐릭터는 실루엣(모자, 머리 블록 모양, 안경, 콧수염, 피부 톤, 복장 색상)만으로 식별할 수 있어야 합니다. 모두 동일한 비율을 사용합니다.
참조 시트 (사용자가 캐릭터 디자인이나 환경을 요청할 때, 비디오 전에 반드시 수행)
scene.html 옆에 designsheet.html과 envsheet.html을 하나씩 만듭니다(동일한 킷, 동일한 render.cjs, node render.cjs designsheet.html out 0,1,2 --w 1600 --h 900 실행). frame(t)는 Math.round(t)로 모드를 선택합니다; 레이블은 3D 포인트를 new THREE.Vector3(x,y,z).project(camera)로 투영하여 위치시킨 DOM 오버레이(Poppins)입니다; 페이지 제목과 견본도 DOM입니다.
- 캐스트 파일
cast.js:DEF테이블(이름, 역할, 4개 견본[피부, 머리카락, 복장, 악센트]), 캐릭터당 하나의 빌더가 있는BUILD테이블(makeChar+ 몇 개의 추가box()액세서리),makeOne(name),makeCast(), 그리고 매 프레임마다 얼굴과 포즈를 리셋하고 깔끔한 모델 시트 포즈를 적용하는pose(name, C). 각 캐릭터를 실루엣(다른scale/headScale, 헤어스타일, 모자, 안경, 콧수염 블록, 피부 톤 및 하나의 악센트 색상)으로 고유하게 만듭니다. - 시트 0 라인업: 모든 캐릭터를 한 줄로(카메라 약 [0,1.02,15], fov 16.5), 각각 아래에 이름 + 역할 + 견본 사각형. 시트 1 주인공 턴어라운드(yaw 0, .75, PI/2, PI: 정면, 3/4, 측면, 후면). 시트 2 표정: 6개의 흉상(중립, 행복, 화남, 놀람, 걱정, 잘난 척)을 가위 타일로 그리는데, 6개의 캐릭터를 하나의 3D 씬에 쌓으면 행이 바닥 아래로 가고 겹치기 때문입니다. 타일 패턴:
renderer.autoClear=false; renderer.setScissorTest(true);그런 다음 타일당setViewport(x, H-y-h, w, h); setScissor(...); renderer.clear(); camera.aspect = w/h; camera.updateProjectionMatrix(); renderer.render(scene, camera);를 수행하고frame끝에서 뷰포트, 가위 테스트 및 autoClear를 재설정합니다. - 눈썹 부호 규칙:
face.brow = [height, tilt, arch], tilt > 0 = 안쪽 끝이 아래로 (화남), tilt < 0 = 안쪽 끝이 위로 (걱정).drawFace에서ang = -s * f.brow[1] * .55입니다. (첫 번째 버전에서는 반전되어 화난 경찰관이 슬퍼 보였습니다.) - 환경 시트 0: 760x318 가위 타일 4개(입구에서 본 주인공 와이드, 아치형 창문이 있는 끝 벽, 주요 소품 코너,
hall.ceiling.visible = false인 하이앵글 크레인), 각각 캡션; 세트에 캐스트를 배치하여 스케일을 확인하고 카메라나 서로를 향하게 합니다(yaw 확인: yaw 0은 +z 방향의 카메라를 향함; 카메라에 등을 대고 있는 캐릭터는 스테이징 버그입니다). 시트 1: 각 소품을 먼 소품 무대(x = 400 + i*20)에 단독 배치하여 홀 샷에 절대 나타나지 않도록 하고, 타일 486x300. 시트 2:up = [-1,0,0](긴 축이 수평으로 실행됨)인 탑뷰 평면도, 카메라 약 [-1,120,-13] 직하방, 좁은 fov(약 9.3) 및 월드 포인트에서 투영된 핀 레이블. 항상 헤더에 10개 견본의 팔레트 스트립을 포함합니다. - 홀의 천장과 보는 하나의 그룹으로 존재하며
H.ceiling으로 노출됩니다(그림자 투사 끄기, 태양은 여전히 홀을 비춤); 크레인 및 탑뷰 샷을 위해 숨깁니다. 표지판 텍스트는 평면에 맞게 크기를 조정해야 합니다: 1.9 x 0.5 표지판은 800 56px Poppins를 사용하며, 78px(잘림)가 아닙니다. - Three.js 함정:
Object3D.position및.rotation은 읽기 전용 속성입니다. 구성 요소를 설정하고(m.rotation.x = ...,m.position.set(...))Object.assign(mesh, { rotation: ... })를 절대 사용하지 마십시오. 이는 오류를 발생시키고__ready를 중단시킵니다. - PNG와
.js파일을 SendUserFile로 전달하고, 킷 변경 후 모든 시트를 다시 렌더링하여 사용자가 오래된 시트를 보유하지 않도록 합니다.
3. 애니메이션 부드러움 규칙
- 애니메이션은 시간의 순수 함수입니다:
frame(t)는t에서 모든 것을 설정합니다. 프레임 간에 상태가 전달되지 않으므로 모든 프레임은 순서에 관계없이 또는 병렬로 렌더링될 수 있습니다. - 모든 움직임에
kf([[t0,v0],[t1,v1]], t, easeFn)을 사용합니다. 푸시인 및 도착에는easeOut을, 크레인/휩 움직임에는easeIO를, 작은 것에는ease를 사용합니다. 카메라나 팔다리에 선형을 절대 사용하지 마십시오. - 카메라는 절대 정적이지 않습니다. 각 샷에는 이징된 위치 + 시선 + fov + 몇 도의 롤, 그리고 항상 켜져 있는 핸드헬드 드리프트
[sin(.5t)*.03, sin(.37t)*.02]가 있습니다. - 단어나 비트에서 샷 경계에서만 하드 컷. 크로스페이드 없음. 평균 샷 2-3초.
- 모든 캐릭터에 항상 유휴 레이어: 호흡 보빙, 스웨이, 머리 롤, 약 3-4초마다 깜박임(0.13초 이징), 작은 눈 움직임.
K.idle()이 이를 수행합니다. - 입 열림 양은 실제 음성 해설의 음량에서 비롯됩니다(
lipsync.py가 30fps로 env.json을 작성). 절대 루핑 플랩 안 됨. - 제스처: 준비(포인트/강타 전 작은 뒤로 당기기), 오버슈트(
easeBack), 유지, 안정. 머리가 이끌고, 몸통이 따릅니다(twist전에headYaw사용). - 걷기 사이클: 다리는 반대, 각 팔은 같은 쪽 다리와 반대로 흔들림, 무릎은 뒤로만 굽힘(
Math.max(0, knee)), 사이클당 2회 보빙, 작은 몸통 비틀기와 기울임. 위상은 사이클 단위: 정상 걷기의 경우 약 1 사이클/s 사용. - 캐릭터는 가구와 겹치면 안 됩니다: 명시적인 x/z로 배치하고 정지 이미지를 확인합니다.
4. 파이프라인
- 스크립트: 1인칭, 처음 3초 내에 후크, 짧고 강렬한 문장, 상승, 반전, 마지막 라인 페이오프. 약 2.5단어/초.
- 음성 해설: 사용자는 일반적으로 ElevenLabs 파일을 제공합니다. 그렇지 않으면 요청하거나 자리 표시자 트랙을 합성합니다.
voice.wav로 저장합니다. - 단어 타이밍: 단어 수준 타임스탬프를 얻습니다(스크립트 텍스트에 대한 강제 정렬, 예: pocketsphinx 또는 whisper 단어 타임스탬프).
words.json을[{"w":"my","s":0.20,"e":0.41}, ...]형식으로 저장하고, 스크립트의 철자와 구두점을 사용합니다(캡션은 후행 구두점을 사용하여 그룹을 나눕니다). - 립싱크 엔벨로프:
python3 lipsync.py voice.wav env.json. - 샷 리스트: 샷당 한 줄: 시작/종료 시간, 뷰어가 보는 것, 카메라 이동, 행동하는 사람. 약 2.6초마다 컷을 목표로 합니다. 프레이밍 다양화: 클로즈업 얼굴, 미디엄 투샷, 로우앵글 푸시인, 군중 위 하이앵글 크레인, 휩틸트, 어깨 너머, 소품(전화기, 배지)에 대한 인서트. 샷을
scene.html의SHOTS에 씁니다. - 캐스트 + 세트 빌드
blockkit.js/props.js(코드 참조)에서.room(),chair(),bench(),crowd()재사용; 위 팔레트에서box()로 새 소품 빌드. - 먼저 정지 이미지 확인(저렴):
node render.cjs scene.html outstills 1,4,7,...; 각 지명 캐릭터의 클로즈업, 걷기 하나, 제스처 하나, 와이드 하나를 확인합니다. 비디오 렌더링 전에 수정합니다. - 비디오 렌더링 (720x1280에서 2코어 기준 약 1-1.7초/프레임, 따라서 40초는 약 15-25분; 청크를 병렬로 백그라운드에서 실행):
export NODE_PATH=$(npm root -g) # playwright는 샌드박스에 전역으로 설치되어 있음; 그렇지 않으면: npm i playwright && npx playwright install chromium
S=30; for i in 0 1; do (node render.cjs scene.html part$i.mp4 --from $((i*S)) --to $(((i+1)*S)) > r$i.log 2>&1 &); done # 30초씩 2개 청크; 둘 다 기다림
printf "file 'part0.mp4'\nfile 'part1.mp4'\n" > list.txt && ffmpeg -y -f concat -safe 0 -i list.txt -c copy silent.mp4
- 오디오 + 먹싱 (음성 아래 연속적인 조용한 배경 사운드, 약 -14 LUFS):
D=130 # >= 비디오 길이
ffmpeg -y -f lavfi -i "sine=f=55:d=$D" -f lavfi -i "sine=f=82.4:d=$D" -f lavfi -i "sine=f=110:d=$D" -filter_complex "[0][1][2]amix=inputs=3:normalize=0,lowpass=f=500,tremolo=f=0.12:d=0.35,aformat=channel_layouts=stereo,afade=t=in:d=1[bed]" -map "[bed]" -ar 44100 bed.wav
ffmpeg -y -i silent.mp4 -i voice.wav -i bed.wav -filter_complex "[2:a]volume=0.12[b];[1:a][b]amix=inputs=2:duration=first:normalize=0,loudnorm=I=-14:TP=-1.5:LRA=7,aformat=channel_layouts=stereo[a]" -map 0:v -map "[a]" -c:v copy -c:a aac -b:a 192k final.mp4
- 전달 전 QC: (a) 컨택 시트, 약 2.6초마다 한 프레임, 캐릭터/캡션 확인; (b) 모션 확인, 축소된 그레이 스트림에서 평균 프레임 차이, 정적 프레임 0개 예상:
python3 - <<'E'
import subprocess, numpy as np
W,H=90,160
a=np.frombuffer(subprocess.run(['ffmpeg','-v','error','-i','final.mp4','-vf',f'scale={W}:{H},format=gray','-f','rawvideo','-'],capture_output=True).stdout,dtype=np.uint8).reshape(-1,H,W).astype(np.float32)
d=np.abs(np.diff(a,axis=0)).mean(axis=(1,2)); print(len(a),'frames; static:',(d<.02).sum(),'; spikes(cuts):',(d>14).sum())
E
(c) 음량 ffmpeg -i final.mp4 -af ebur128 -f null - (약 -14 LUFS), (d) 캡션이 말과 일치하는지 확인.
SendUserFile로 전달(mp4 + 커버 스틸).
5. 설정
mkdir -p work && cd work && npm init -y >/dev/null && npm i [email protected] @fontsource/poppins
# 아래 파일을 이 폴더에 만듭니다: blockkit.js props.js captions.js render.cjs lipsync.py scene.html
# (또는 이 레포지토리의 kit/ 폴더에서 준비된 파일을 복사합니다. 여기에는 st...
[번역을 위해 잘렸습니다]