個人でWebサイトやアプリを作っています(屋号は sesebox です)。
いま、キャラクターに服やアクセサリーを着せて育てるスマホアプリを作っています。キャラが歩いたり走ったりするアニメーションを、Pythonの画像ライブラリ Pillow でGIFにして確認しているのですが、「歩き」のはずのGIFが、どう見ても走っているように見えるという問題が起きました。
脚の動きや絵の枚数を疑っていましたが、原因は絵ではなくGIFの仕様でした。同じところで詰まる人がいそうなので書いておきます。
何が起きたか
正面から見た歩きのアニメを、**1秒に40コマ(1コマ25ミリ秒)**のつもりで作っていました。
frames[0].save(
"walk_front.gif",
save_all=True,
append_images=frames[1:],
duration=25, # 1コマ25ms = 40fps のつもり
loop=0,
)
ところが、できたGIFを再生すると明らかに速い。歩きというより小走りです。
横から見た歩きは自然に見えていたので、最初は「正面の脚の振り方が大きすぎるのかな」と絵のほうを直そうとしていました。
原因:GIFのコマの長さは「10ミリ秒単位」
GIFという画像形式は、各コマの表示時間を 1/100秒(=10ミリ秒)単位の整数で持っています(GIF89aの仕様で決まっています)。
つまり、25ミリ秒というコマの長さは、GIFの中に書き込めません。 書けるのは 10ms・20ms・30ms…のような10の倍数だけです。
実際に Pillow(12.3.0)で、いろいろな値を渡して保存し、読み直してみました。
from PIL import Image
frames = [Image.new("RGB", (8, 8), (i * 20, 0, 0)) for i in range(8)]
for d in (15, 25, 33, 45, 50):
frames[0].save("t.gif", save_all=True, append_images=frames[1:],
duration=d, loop=0)
print(d, "->", Image.open("t.gif").info["duration"])
15 -> 10
25 -> 20
33 -> 30
45 -> 40
50 -> 50
10の倍数に切り捨てられています。 45ミリ秒も、50ではなく40になりました。
25ミリ秒のつもりが20ミリ秒になったので、再生速度は 25 ÷ 20 = 1.25倍。1秒に40コマのつもりが、実際は50コマで再生されていたわけです。歩きが小走りに見えるのも当然でした。
調べてみると、ほかのアニメも同じ状態でした。
| アニメ | 作ったつもり | 実際の再生 |
|---|---|---|
| 正面の歩き | 40fps(25ms) | 50fps(20ms)=1.25倍速 |
| 背面の歩き | 30fps(33.3ms) | 33.3fps(30ms) |
| 横の歩き・走り | 48fps(約20.8ms) | 50fps(20ms) |
横の歩きが自然に見えていたのは、48fps → 50fps とずれがたまたま小さかったからでした。正面だけおかしく見えた理由もこれで説明がつきます。
どう直したか
やったことは2つです。
1. コマの長さを最初から10の倍数にする
「何fpsで作るか」から考えるのをやめて、「1コマ何ミリ秒か」を10の倍数(20ms・30ms・40ms…)から選ぶようにしました。
| 1コマの長さ | 1秒あたりのコマ数 |
|---|---|
| 20ms | 50コマ |
| 30ms | 約33.3コマ |
| 40ms | 25コマ |
そのうえで、1つのポーズを何コマ見せるかを整数にそろえるようにしました。たとえば20msで1ポーズ3コマなら、1ポーズ60ms=1秒に約16.7ポーズです。歩く速さは「1秒に何ポーズ進むか」で決まるので、これを基準に調整しました。
2. 作ったGIFの再生時間を、ファイルから読み直して確かめる
意図した値ではなく、実際にファイルに書かれた値で確認するようにしました。
from PIL import Image
im = Image.open("walk_front.gif")
n = im.n_frames
d = im.info["duration"] # 1コマ目の長さ(ms)
print(f"{n}コマ × {d}ms = 1ループ {n * d / 1000:.2f}秒")
コマごとに長さを変えている場合は、im.seek(i) で1コマずつ移動して im.info["duration"] を足し合わせると、1ループの正確な秒数が出せます。
わかったこと
- GIFのコマの長さは10ミリ秒単位。 それ以外の値は保存の時点で丸められる(Pillowでは切り捨てだった)
- 「40fpsで作った」と思っていても、GIFになった時点で別の速さに変わっていることがある
- 動きが「速い」「遅い」と感じたら、絵を直す前に実際の再生速度を数字で見る
絵の動き(脚の角度や振り幅)ばかり疑っていましたが、実は書き出しの段階で速さが変わっていました。アニメの見た目の違和感は、まず数字で確かめるのが近道だと思いました。