로우폴리 PS1 단편 영상
모든 것은 코드로 이루어집니다: Claude가 단편당 하나의 Blender 스크립트를 작성하여 사용자의 머신에서 렌더링하고, 프레임을 확인한 후 수정한 다음, 사운드와 자막을 구축합니다. 비디오 생성기는 사용하지 않습니다.
examples/lift-rocket/ ("만약 엘리베이터가 로켓만큼 빠르다면?", 34초)에서 검증되었습니다. 새 단편을 작성하기 전에 해당 예제를 읽어보세요: 그것이 템플릿입니다.
요구 사항 (먼저 확인하고 누락된 것을 설치하세요)
- Blender 4.2+ (5.3에서 테스트 완료). 찾기: PATH에
blender, 또는C:\Program Files\Blender Foundation\<ver>\blender.exe//Applications/Blender.app/Contents/MacOS/Blender. - Python 3.10+ with
numpy pillow soundfile faster-whisper; PATH에ffmpeg. - 캐릭터: Quaternius "Ultimate Modular Men" (CC0, 무료, quaternius.com).
assets/quaternius-modular-men/에 압축 풀기 또는LOWPOLY_PACK을Individual Characters/Blends폴더로 설정. - TTS 음성 (ElevenLabs 또는 사용자가 보유한 모든 도구). 선택 사항: 사운드를 확인할
earshotMCP.
파이프라인 (단편당 하나의 폴더, examples/lift-rocket/을 시작 템플릿으로 복사)
| # | 단계 | 파일 / 명령어 | 완료 조건 |
|---|---|---|---|
| 1 | 스크립트 | script.md | 후크 질문 + 결과의 연쇄(각각 더 나쁘고 재미있음) + 실제 숫자 + 건조 펀치라인. 30 |
| 2 | 음성 | vo/takeA.mp3 | 2테이크 중 깨끗한 단어 타이밍이 있는 것을 선택. 활기찬 크리에이터 목소리(차분함 = "지루함" 피드백). |
| 3 | 단어 타이밍 | python ../../scripts/words.py vo/takeA.mp3 | takeA_words.json에 길이가 0인 단어 없음. |
| 4 | 샷 목록 | script.md의 표 | VO 구문당 하나의 샷, 1~3초마다 새로운 그림, 각 샷은 단어 시간에 연결. |
| 5 | 씬 | build.py (Blender) | blender -b -P build.py -- frames --still 30,120,...으로 스틸 렌더링; 전체 렌더 전에 모든 스틸 확인. |
| 6 | 미리보기 + 노트 | python ../../scripts/preview.py . render 후 serve | 사용자가 열린 노트 없음 (미리보기 루프 참조). |
| 7 | 렌더 | blender -b -P build.py -- frames | 360x640 EEVEE PNG, f_0001.png... |
| 8 | 사운드 | python build_mix.py | SFX를 그림 프레임에 배치, VO 위에, 로파이 배경음 아래, 덕킹. mix_raw.wav + cues.json 생성. |
| 9 | 포스트 | python post.py | PS1 디더, 효과, 단어 강조 자막, 통계 콜아웃 -> 1080x1920 out/ 폴더. |
| 10 | 먹싱 | 아래 참조 | short.mp4, 측정 및 동기화 확인. |
모든 곳의 프레임 규칙: 24fps, 프레임 1 = t 0, F = lambda t: int(round(t * 24)) + 1.
먹싱:
ffmpeg -y -framerate 24 -i out/f_%04d.png -i mix_raw.wav -c:v libx264 -crf 16 -pix_fmt yuv420p \
-af "loudnorm=I=-15:TP=-1.5:LRA=11,aresample=48000" -c:a aac -b:a 192k -shortest short.mp4
그림을 재절단하지 않고 나중에 음성 교체:
python ../../scripts/words.py vo/takeB.mp3 후 python ../../scripts/align.py vo/takeA_words.json vo/takeB.mp3 --end 34 (--end = VO 배경음이 멈추는 지점; 출력은 end + 2초 길이. 없으면: 마지막 단어 + 1.5초, 그림보다 짧은 파일 생성)
-> takeB_aligned.wav + takeB_aligned_words.json. build_mix.py (VO 환경 변수)와 post.py가 이를 가리키도록 설정.
미리보기 루프 (전체 렌더 전)
브라우저에서 빠른 저해상도 미리보기, 사용자가 타임라인에 노트를 남기면 Claude가 수정 후 내용이 변경된 샷만 재렌더링. 샷 = build.py의 카메라 마커.
| 수행 | 명령어 (단편 폴더에서) |
|---|---|
| 미리보기 렌더 | python ../../scripts/preview.py . render (첫 실행: 모든 샷; 이후: 변경된 샷만) |
| 노트 페이지 열기 | python ../../scripts/preview.py . serve -> http://127.0.0.1:8765 |
| 노트 읽기 | python ../../scripts/preview.py . notes (샷별로 그룹화, 프레임 번호 포함) |
| 수정 후 | build.py 편집, render 재실행, python ../../scripts/preview.py . done 3 5 --reply "변경 사항" |
| 샷 강제 렌더 | render --shots c_press,c_slam 또는 render --all |
build.py는--blend-only를 허용해야 함 (씬 빌드 후 저장, 렌더하지 않음). 예제 참조.- 변경 감지: 모든 미리보기 프레임은 지문(활성 카메라, 모든 보이는 오브젝트의 변환, 아마추어 포즈, 얼굴 교체, 재질, 조명, 메시 데이터)을 가집니다. 샷은 프레임 중 하나가 변경된 경우에만 재렌더링됩니다. 변경되지 않은 샷은 이전 프레임을 유지합니다.
preview.json의 설정 (선택 사항):scale50 (%),step2 (매 2번째 프레임),samples1,audio(기본mix_raw.wav, 또는 VO),final= 전체 파이프라인 명령어. 페이지의 "승인 및 최종 렌더" 버튼이 이를 실행합니다;notes는 사용자가 클릭하면 APPROVED를 출력합니다.- 노트를 하나씩 수정하고 각각에 답변한 후 재렌더링하고 사용자에게 새로고침할 필요가 없다고 알림: 페이지가 자동으로 비디오를 다시 로드합니다. 동일한 종류의 반복 노트는 이 파일에 규칙이 됩니다.
- lift-rocket 기준 측정 (RTX 3070 Ti 노트북, 820프레임): 첫 미리보기 76초, 샷 하나 변경 13초, 변경 없음 12초. 대부분의 프레임 렌더 시간은 고정 오버헤드이므로 해상도 낮추는 것보다 프레임 수를 줄이는 것이 더 효과적입니다.
Kit API (scripts/lowpoly_kit.py, Blender 내부에서 임포트)
load_character(scn, "Suit")->(armature, meshes, actions). 캐릭터: Adventurer, Beach, Casual_2, Casual_Hoodie, Farmer, King, Punk, Spacesuit, Suit, Swat, Worker.- 액션 (제자리, 24개): Death, HitRecieve, HitRecieve_2, Idle, Idle_Neutral, Interact, Kick_Left/Right, Punch_Left/Right, Roll, Run, Run_Back/Left/Right, Walk, Wave, Gun_Shoot, Idle_Gun*, Idle_Sword, Sword_Slash, Run_Shoot.
add_faces(scn, arm, head, tag, kinds, spec, drop)(아마추어 이동 전에 호출). 종류: neutral, blink, side, shock, grimace, squint, dazed, xeyes.drop은 팩의 3D 눈/눈썹을 삭제합니다.show_faces(faces, [(frame, kind), ...]): 하드 컷, 각 얼굴은 다음까지 유지.nla(arm, [(action, start, end, blend[, influence]), ...]): 이후 항목이 더 높은 트랙에 배치.drive_root(arm, [(frame, x, y, yaw, ease), ...], last): yaw 0은 -Y 방향 (기본 카메라).new_cam,aim,yaw_to,ps1_render_settings(scn, outdir).- 세트:
scripts/set_lift_lobby.py(BUILD_ONLY=True로 실행하면box,uv_world,mat반환). 새 세트는 동일한 패턴을 따름: 픽셀 텍스처가 있는 상자 (interpolation = "Closest").
룩 규칙
- 렌더 360x640,
filter_size 0, AgX Punchy, 플랫 쉐이딩; 포스트: 15비트 컬러 + Bayer 디더, x3 nearest. - 얼굴은 평평한 32x28 픽셀 데칼로 하드 컷으로 교체; 얼굴을 부드럽게 애니메이션하지 마십시오.
- 힘을 받으면 아마추어 스케일 키프레임으로 찌그러짐 (z축 감소, xy축 증가). 코미디 비트: 빠르게 진입 (2~3프레임), 유지.
- 자막: 1~3단어, 두꺼운 산세리프, 흰색에 검은색 테두리, 현재 단어는 노란색, 하단 1/3 (y 1300 of 1920).
- 통계 콜아웃: 빨간색 둥근 상자, 상단 1/3, 4프레임에 걸쳐 팝인, 실제 숫자당 하나.
사운드 규칙
- 모든 SFX는 피크가 그림 프레임에 오도록 배치 (
at(name, frame, db, peak=...)). - 음성이 반드시 이겨야 함: VO는 0 dBFS로 정규화, SFX 버스 -11 dB, 음악 -7 dB, 음성이 말하는 동안 사이드체인 덕킹 (SFX -5 dB, 음악 -9 dB). lift 단편 v1에서는 SFX가 -3, 음악이 0이어서 "음성이 안 들려요" 피드백 발생.
- 큰 멈춤 비트에서 음악을 완전히 자르고, 이후 다시 가져옴. 침묵이 개그를 판매합니다.
- 마스터: loudnorm -15 LUFS, 트루 피크 -1.5 dBTP.
- 확인: earshot
timeline을 mp4와cues.json에 대해 실행 (목표: 모든 큐가 100ms 이내),measure로 LUFS 확인.
이미 발생한 버그 (반복 금지)
- 팩 메시는 렌더 숨김 상태로 제공됨:
hide_render = False(kit에서 처리). - 문 메시는 이미 닫힌 위치에 있음: 오프셋만 애니메이션, 그렇지 않으면 0.7m 간격으로 닫힘.
- 약 2.75m보다 낮은 천장은 엘리베이터 크기 차량 내부에서 카메라가 벽 디스플레이를 가림.
- 아직 보여져서는 안 되는 캐릭터: 자신의 샷까지
location.z = -50키프레임. - NLA 원샷에서 홀드로 전환: 클립의
blend_out = 0설정, 그렇지 않으면 포즈가 한 프레임 동안 튐. - 역방향 샷이 검게 나옴 (조명 없는 천장): 전면 조명과 약간의 천장 방출 추가. 역방향 앵글은 일찍 렌더.
- faster-whisper가 mp3에서 PyAV 불일치로 깨질 수 있음:
words.py는 먼저 ffmpeg로 디코딩. - -0.04초에서 시작하는 구문은 정렬에서 빈 조각 생성:
max(0.0, ...)으로 클램프 (처리 완료).
비용 (lift 단편, RTX 3070 Ti 노트북)
렌더 820프레임 92초, 포스트 17초, $0. 캐릭터, SFX, 음악: $0 (CC0 + 코드로 합성). 음성: ElevenLabs 테이크당 약 $0.10, 단편당 2테이크.
전달 전 확인사항
- 최종 프레임의 컨택트 시트를 확인 (약 2초 간격), 스틸만 확인하지 말 것.
- 음량 측정, 동기화 확인, 자막과 VO 텍스트 대조 (철자 수정은 post.py
FIX에서 처리). - 무엇을 측정했는지, 무엇을 사람이 듣지 않았는지 사용자에게 알릴 것.