fighting-konoha/guide/sprite-qa.md

160 lines
9.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Sprite QA — supaya tidak ada frame yang besar sendiri
Mata manusia baru sadar frame "berdenyut" setelah dimainkan. Jadi sprite **diukur dulu** sebelum masuk game.
Semua pemeriksaan dilakukan pada **atlas final** (`sprite-sheet-alpha.png` + `manifest.json`), bukan `frames/`.
Untuk proyek ini baca [character-workflow.md](character-workflow.md) terlebih dahulu. Angka toleransi di bawah adalah target untuk landmark yang valid. Hasil template yang menangkap bahu/sepatu atau gagal karena rotasi harus dicatat sebagai tidak terukur; jangan dilaporkan lolos hanya berdasarkan median lain.
Tool ada di [tools/](tools/). Jalankan dengan Python dari venv sprite-gen (butuh Pillow, NumPy, `sprite_gen`):
```bash
PY=<SPRITE_GEN_ROOT>/.venv/bin/python # Windows: .venv\Scripts\python.exe, set PYTHONUTF8=1
```
## Checklist (wajib lolos semua)
**Skala antar gerakan — SEMUA gerakan, termasuk serangan, lompat, skill**
- [ ] `measure_head_scale.py` dijalankan untuk **setiap** gerakan terhadap idle frame 0:
median tiap gerakan **0.97–1.03**
- [ ] `head-check.png` dilihat: semua kepala tampak sama besar dengan TEMPLATE; frame yang kotaknya bukan
kepala dicatat sebagai "tidak terukur" dan dicek dengan perbandingan badan penuh (lihat langkah 2)
- [ ] Setiap menambah gerakan baru, cek ini diulang untuk gerakan itu (jangan anggap aman karena
gerakan lain sudah lolos)
**Frame sambungan — frame yang bertemu idle (awal & akhir serangan/kombo, akhir hurt)**
- [ ] Ukur badan penuh dan crown→sabuk→kaki, bukan kepala/rambut saja. Pada ARCO, kepala seragam pernah menyembunyikan recovery 170–176 px dibanding idle 180 px.
- [ ] Frame **pertama dan terakhir** tiap gerakan aksi diukur sendiri terhadap idle frame 0 di atlas
(bukan cuma median strip): kepala 0.97–1.03, dan untuk pose tegak tinggi helm & helm→sabuk selisih ≤ 3 px
- [ ] Posisi kepala frame terakhir sama dengan idle (± 2 px) — kalau tidak, geser bulat (`write_dx.py`)
- [ ] Dilihat berdampingan: frame terakhir | idle f0 dengan garis tinggi yang sama (known-issues #17)
**Per gerakan tegak (idle, walk, run)**
- [ ] Tinggi frame idle selisih **≤ 2 px** satu sama lain
- [ ] Posisi badan (`torso_x`) tidak goyang lebih dari **±1.5 px** dalam satu gerakan
- [ ] Naik-turun badan saat jalan (bob) wajar, **bukan** hasil skala yang berbeda
- [ ] Walk/run: **kaki bergantian** — `measure_atlas.py` baris "jarak kaki" lebar→rapat→lebar→rapat,
rasio terkecil/terbesar ≤ 0.45 (kalau tidak: generate ulang, lihat known-issues #15)
- [ ] Walk/run di game: frame maju berdasarkan jarak tempuh (`stride`), bukan fps tetap
**Makhluk terbang / tanpa kaki**
- [ ] Bagian yang seharusnya diam (kepala saat hover, badan saat menyerang) tidak bergeser > 2 px antar frame
- [ ] Tidak ada piksel di tepi sel (sayap/ekor terpotong)
**Semua gerakan**
- [ ] Jump hanya memakai pose atletis yang dipilih; tidak replay crouch di udara. Tuck doublejump tidak memiliki kaki menendang.
- [ ] Normal jump → tuck → spin → kembali jump dilihat di runtime dengan pivot terukur dan skala konstan.
- [ ] Source, request, jumlah frame, manifest, wrapper JS, metrics, dan titik efek sama-sama versi terbaru.
- [ ] Gerakan baru dibuat dengan base dari kartu karakter yang sama (bukan base baru / deskripsi teks saja)
- [ ] Kaki semua frame di baris yang sama (`bottom` sama) — untuk karakter yang berdiri
- [ ] Tidak ada frame kosong / terpotong / pose menempel ke tetangga
- [ ] Tidak ada sisa warna chroma (pinggiran magenta/hijau) — lihat di atas latar gelap **dan** terang
- [ ] Ketajaman sama di semua frame (tidak ada frame yang sedikit buram) — artinya tidak ada `scale ≠ 1`
atau `dx` pecahan di `curation.json`
- [ ] Identitas konsisten: warna, senjata di tangan yang sama, aksesori tidak hilang
- [ ] GIF preview tiap gerakan dilihat sekali dengan kecepatan asli dan 0.5×
**Terakhir**
- [ ] `qa-notes.md` diisi: faktor skala, koreksi per pose, dx, hasil `measure_atlas.py`
- [ ] Pisahkan bukti angka, contact sheet/screenshot, dan loop yang benar-benar ditonton. Jangan menyebut full motion sudah mulus bila baru keyframe yang diperiksa.
- [ ] Jika menggunakan tool landmark putih ARCO untuk karakter berambut gelap/bertanduk, ganti landmark; jangan membiarkan alat memilih armor sebagai rambut.
## Langkah pemeriksaan
### 1. Ukur atlas
```bash
$PY guide/tools/measure_atlas.py run
```
Contoh keluaran yang **baik**:
```
idle 0: h=141 bottom=199 head_w=38 torso_x=112.0 ok
idle 1: h=140 bottom=199 head_w=40 torso_x=111.6 ok
...
rentang tinggi 140–141 rentang torso_x 111.6–112.3
SEMUA LOLOS
```
Contoh yang **buruk** (kasus nyata sebelum diperbaiki): idle frame 1 `h=149` sementara lainnya `142`
→ karakter terlihat membesar sekali tiap siklus napas.
### 2. Cek skala SEMUA gerakan lewat kepala (di gambar asli)
Aturan: **samanya ukuran pixel palsu (pitch) TIDAK berarti samanya ukuran karakter.** Model bisa menggambar
strip serangan dengan karakter lebih kecil (supaya pedang terangkat muat di slot), dengan pitch yang sama.
Jadi setiap gerakan diukur terhadap idle memakai bagian yang bentuknya tetap: **kepala/helm**.
Simpan `--rel` yang sedang dipakai di `rel.json` (satu sumber kebenaran, juga dicatat di kartu karakter):
```json
{ "idle": [1, 0.95, 1, 0.987], "walk": [0.990], "jump": [1.032],
"attack1": [1.059], "attack2": [1.075], "attack3": [0.974] }
```
```bash
$PY guide/tools/measure_head_scale.py <char_dir> --poses idle:4,walk:6,jump:4,attack1:4,attack2:4,attack3:4 \
--rel-json rel.json --landmark white [--head-box x0,y0,x1,y1]
```
```
attack1 0: kepala di atlas = 0.878 × idle (skor 0.45)
...
median 0.944 → PERLU KOREKSI; --rel baru 1.059
```
- Pengukuran di **gambar asli** (kepala ±120 px) dengan pencocokan **tepi** — di atlas kepala cuma ±20 px dan
model memberi pencahayaan berbeda per strip, jadi pencocokan warna/siluet di atlas tidak bisa dipercaya.
- `--head-box`: kotak kepala di pose acuan (lihat tile TEMPLATE di `head-check.png`; harus berisi kepala saja).
- `--landmark white`: untuk karakter dengan bulu helm/rambut putih, mencegah kepala tertukar dengan bahu.
- **Wajib buka `head-check.png`.** Tiap kepala dikembalikan ke ukuran template memakai skala terbaca;
kalau benar, semuanya sama besar. Kotak yang bukan kepala = "TIDAK VALID" → frame itu dinilai dengan
median gerakannya, lalu dicek manual dengan perbandingan badan penuh berjajar (kaki di garis yang sama).
- Koreksi **per gerakan** (satu angka untuk satu strip), bukan per frame, kecuali frame idle yang jelas beda.
Jangan pakai registrasi siluet (`register_frames.py` tanpa `--dx-only`) untuk menilai skala pose aksi:
lutut ditekuk menurunkan kepala dan alat "membesarkan" karakter untuk mengejarnya; lengan/senjata juga
mengacaukan kecocokan. `register_frames.py` hanya untuk mencari **geser bulat** pada pose tegak.
## Perbaiki skala di gambar asli
```bash
# satu angka --rel = strip diperkecil utuh (aman walau pose di gambar asli saling menempel)
$PY guide/tools/rebuild_raw.py raw-original/attack1.png run/raw/attack1.png --poses 4 --rel 1.059
# angka per pose (khusus frame yang beda sendiri, mis. idle)
$PY guide/tools/rebuild_raw.py raw-original/idle.png run/raw/idle.png --poses 4 --rel 1,0.95,1,0.987
# pose saling menempel juga bisa: dipotong di titik sentuh, hanya pose dengan rel berbeda yang ditempel ulang
$PY guide/tools/rebuild_raw.py raw-original/combo3.png combo-run/raw/combo3.png --poses 6 --rel 1.143,1.143,1.143,1.143,1.143,1.223
sprite-gen extract --run-dir run --states idle,attack1
```
- `--rel` di `rel.json` selalu relatif terhadap **gambar asli** (bukan terhadap raw sebelumnya).
`measure_head_scale.py` sudah memperhitungkan rel yang sedang dipakai dan mencetak `--rel baru`.
- Setelah extract ulang → ukur lagi sampai semua median 0.97–1.03 → **ukur ulang hitbox & titik efek** di game
(lihat [sprite-runtime.md](sprite-runtime.md#hitbox-dari-data-bukan-tebakan)).
## Perbaiki goyang dengan geser bulat
```bash
$PY guide/tools/register_frames.py run walk --dx-only # → dx bulat: 1,-5,-4,0,0,-4
$PY guide/tools/write_dx.py run walk 1,-5,-4,0,0,-4
sprite-gen compose-atlas --run-dir run
$PY guide/tools/measure_atlas.py run # harus SEMUA LOLOS
```
Geser bulat = piksel dipindah utuh, tanpa blur (sudah diverifikasi: selisih piksel ≤ 2/255).
## Yang TIDAK boleh dilakukan
| Jangan | Kenapa |
|---|---|
| Koreksi ukuran dengan `scale` di `curation.json` | Di-resample bicubic → frame itu sedikit buram → berkedip tajam/buram saat animasi |
| `dx`/`dy` pecahan (0.5, 1.5) | Sama: resample → buram sebelah |
| Mengecilkan tiap frame agar muat sel | Tiap frame dapat skala berbeda → karakter berdenyut. Perbesar sel |
| Memeriksa di `frames/` | Belum termasuk curation; yang dipakai game adalah atlas |
| Menilai hanya dengan mata di gambar diam | Selisih 2 px tidak terlihat di atlas, tapi terasa saat animasi |
| Menyembunyikan pose jelek dengan mengubah fps | Masalah tetap ada; generate ulang strip itu |
| Menganggap strip "sudah sama skala" karena pitch-nya sama | Pitch sama ≠ karakter sama besar; serangan bisa 6–15 % lebih kecil |
| Hanya mengecek idle & walk | Justru serangan/skill yang paling sering meleset (pose senjata terangkat) |
| Percaya angka alat tanpa membuka `head-check.png` | Alat bisa menangkap bahu/lambang sebagai "kepala" dan tetap mencetak angka |