立っているだけで滑るキャラクター — 白い足袋を目印に位置の揺れを消す

からくり双星の待機モーションで、立ち止まったキャラクターが左右へ滑って見えました。原因は、一コマずつ別々に描かれた原画の位置のずれです。全体の重心は扇の揺れを拾い、足元の画素だけを見ると、裾と木槌も拾ってしまいます。最後に効いたのは「白い足袋」という目印でした。測り方と、あえて直さなかった場所まで書きます。

立っているのに、滑る

からくり双星のアオイが宵桜の宿場の足場に立つ画面
位置の揺れは、足場や影と重なる実際の画面でいちばん目立ちます。

からくり双星の主人公、アオイとトワの絵は、生成した原画から起こしています。240×200pxのセルに一コマずつ描き、横に16コマ並べたものが待機の動きです。

組み込んで動かすと、「アオイとトワの待機モーションが微妙に揺れている」という指摘が入りました。見ると、立ち止まっているのに体が左右へ少しずつ滑っています。

原因は原画の作り方にありました。一コマずつ別々に描かれるので、セルの中での立ち位置が数pxずつ違うのです。歩きや攻撃なら体が動くので紛れますが、待機は同じ姿勢が続くぶん、そのずれだけが動きとして目に入ります。

直すには、各コマで「キャラクターがどこに立っているか」を一つの数字にする必要があります。問題は、どこを測るかでした。

最初の二つの物差しは、どちらも外れた

最初に思いつくのは、絵全体の重心です。これは使えません。アオイは木槌、トワは扇を持っていて、待機中もそれが揺れています。全体の重心で測ると、扇が振れるだけで立ち位置まで動いたように測定されます。直したい揺れと、残したい演技を区別できません。

次に、不透明な画素のいちばん下、つまり足元の周辺だけを見る方法を試しました。今度は暗い色の裾や木槌の先が足の画素に混ざり、基準そのものがコマごとに動きました。

待機の1コマで立ち位置を測る三つの方法。全体の重心は扇の揺れで動き、足元の不透明な画素には暗い裾が混ざる。最下10行の明るい点だけを見ると足袋だけが残る

「白っぽい二つの点が足です」

行き詰まったところで、「下側にある白っぽい二つの点が足です。その位置を基準に」という指摘をもらいました。二人とも白い足袋を履いていて、服や裾は暗い色です。明るさで絞れば、足だけが残ります。

今の測定手順は次のとおりです。

いちばん左を選ぶのには理由があります。アオイのもう一方の足袋のあたりでは、木槌の光が1pxだけ明るい点として拾われていました。幅2px未満を捨て、左の足袋に決め打ちすると、全コマで同じ場所を測れます。

16コマを測った結果です。通常の画面ではセル幅240pxを150px、つまり0.625倍で描くので、画面上のずれも併記します。

軸(横)の幅足元(縦)の幅
アオイ2.0px(画面1.3px)2px(画面1.3px)
トワ6.0px(画面3.8px)0px

トワは足元の高さこそ揃っていましたが、横に6px、画面で4px近く振れていました。これが「滑って見えた」正体です。

セルの中心へ寄せてはいけない

ずれが測れたら、揃える先を決めます。セルの中心へ寄せたくなりますが、これはやりません。キャラクターの軸は、もともとセルの中心と一致していないからです。中心へ寄せるとキャラクター全体が動き、絵と当たり判定、絵と影の位置関係まで変わってしまいます。

揃える基準は、待機モーションの全コマで測った位置の平均です。コマ間のずれだけを減らし、キャラクター全体の位置は変えません。

// anchors[i] は i 番目のコマの足袋の位置(見つからなければ null)
const found = anchors.filter(Boolean);
if (found.length === 0) throw new Error("目印を検出できません");
const meanX = found.reduce((s, p) => s + p.x, 0) / found.length;
const meanY = found.reduce((s, p) => s + p.y, 0) / found.length;
const round1 = v => Math.round(v * 10) / 10;   // 0.1px 単位
const offsets = anchors.map(p =>
  p ? [round1(meanX - p.x), round1(meanY - p.y)] : null
);

補正量は0.1px単位に丸めて、ゲーム本体の表へ書き込みます。補正後のずれは、この丸めの0.1px以内です。

補正は、描く座標系の内側で足す

測った補正は原画上の画素数です。画面へ描くときは表示倍率を掛けます。トワの6pxのずれなら、画面では3.75pxです。

左右の向きにも注意が要ります。本作は、キャラクターを描く座標を反転させた内側で補正を足しています。そのため向きが変われば、補正も一緒に反転します。反転の外側から画面の座標のまま足す作りなら、向きに応じて符号を変えなければ、左向きのときだけ逆へずれます。

あえて直さなかったところ

補正したのは待機の行だけです。歩き・攻撃・技の行は触っていません。足の移動は、歩きや攻撃の動きの一部だからです。揃えてしまうと、今度は「歩いているのに足の位置が動かない」という別の不自然さが生まれます。

扇や木槌の揺れも残しています。測る場所を足袋に限ったので、体の位置を直しても、持ち物の動きは消えません。ずれて見えるものが、全部まちがいとは限りません。

ほかの素材へ使うときは

この手順の数値(10行、明るさ140、幅2px)は、白い足袋という本作の絵に合わせたものです。暗い靴や長い裾の絵にそのまま使うと、別のものを足と見誤ったり、何も見つけられなかったりします。

別の素材に使うなら、まず検出した位置へ印を重ねて、全コマを目で確かめます。本作では16コマすべてで検出できましたが、見つからなかったコマを補正ゼロのまま通すと、そのコマだけが滑ります。検出できなかったコマは、自動補正せずに目視で確認します。

振り返ると、近道は「動かしたくないもの」と「動いてよいもの」を先に分けることでした。そして今回それを分けたのは、画素の統計ではなく、絵を見た人の一言でした。

コマの切り出しと描画先の指定はMDNのdrawImageの解説が詳しく、生成した絵を背景から切り抜く話は白物戦記の画像差し替えの記事に書きました。