曲げスクリプトの実装の話
ここではこのスクリプトの実装の話を書きます。
何をするスクリプトか
画像の指定したアンカー2点の間(下の画像の緑の部分)を円弧やベジェ曲線のような曲線に沿って曲げるアニメーション効果です。
アンカーの外側の部分(画像の赤と青の部分)は変形しません(緑の部分と自然につながるように平行移動と回転のみ)。
この時、アンカーは画像の外側を指定することは出来ないものとします。
また、アンカー間を結ぶ直線の長さと曲線の長さ(画像の黒い線)が変わらないようにします。
https://scrapbox.io/files/61b39e7180b4880020a5766f.png
処理の流れ
上記の内容を実現するにあたって以下の流れでスクリプトを書きます。
1. 画像を複数の四角形に分割する
2. 分割した四角形の頂点を目的の形になるように移動させる
3. 1と2の四角形を元に obj.drawpoly() で描画する
変形系のスクリプトなら大体こんな感じになるかと思います。
今回苦労したのは1の画像の分割です。
https://scrapbox.io/files/61ac6914ee8557001f7afb8b.png
画像を複数の四角形に分割する
今回のスクリプトでは赤い領域と青い領域は変形しない部分なのでまずここを切り分けます。
そして緑の領域は滑らかに変形させるためにさらにN分割します(上の画像だと3分割)。
さて、画像を$ 1+1+N個の領域に分割するこの処理ですが、上の画像の例だと全部綺麗な長方形になるので簡単そうですね。
しかし今回はアンカーの位置を自由に設定できるので、必ずしもこのように綺麗になるとは限りません。
下の画像の様に領域の形が長方形でない四角形になったり、三角形や五角形、六角形にもなり得ます。
https://scrapbox.io/files/61b354714aa22c001d315b64.png
また、五角形と六角形についてはさらに分割して四角形(と三角形)のみで構成されるようにする必要があります。
これは最後に使う obj.drawpoly() が四角形しか扱えないためです。
「分割しなくても、五角形や六角形を内包する大きな四角形として扱えば良いのでは?」
という天才的なアイデアをひらめく方もいるかもしれませんが、残念ながらそれは出来ません。
下の画像は$ 6 \times 6マスのカラフルな画像のピンクで囲んだ領域を右側の黄色で囲んだ三角形の領域にobj.drawpoly()した結果です。
uv座標に画像の範囲外の座標を指定しても画像の範囲内にクリッピングされてしまうようで、期待した結果は得られません。
https://scrapbox.io/files/61aca9267c50a6001e3783f7.png
code:lua
local w, h = obj.getpixel()
obj.drawpoly(
-w/2, -h/2-h/3, 0, -- xy 左上
w/2+w/3, h/2, 0, -- xy 右下
-w/2, h/2, 0, -- xy 左下
-w/2, h/2, 0, -- xy 左下
0, -h/3, -- uv 左上
w+w/3, h, -- uv 右下
0, h, -- uv 左下
0, h) -- uv 左下
改めて問題を整理してみましょう。
分かっているのは画像のサイズと2つのアンカーの座標、そしてアンカーの間を分割する数Nです。
この情報をもとに画像を$ 1+1+N (+ \alpha)個の四角形(と三角形)に分割しなければいけません。
この問題、直線の交点の計算などを駆使すれば別に解けない問題ではありませんが、場合分けが大量に発生してかなり面倒です。
一度デバッグが嫌になってほったらかしました。
なんとか完成させた自分のスクリプトだと KaroterraBend.lua の中にある createSrc() がこの処理を担当しています。
実装のイメージとしては次のような感じです。
まず下の画像のように、画像に対して十分に大きな黒い長方形を考える (easy)
黒い長方形と画像の交差領域を1個か2個の四角形(と三角形)として算出する (HARD)
https://scrapbox.io/files/61acb5c77071e8001fe7ceb7.png
そして2つ目の交差領域を求めるというのを fitCurv() が担当しています。
まず黒い長方形の中に画像の頂点が何個あるか調べて、そこからさらに外積の値を使って三角形か五角形か、台形か平行四辺形か、みたいな感じで条件分岐をかけてます。
ちなみに、こういうポリゴンの交差領域を求める問題をポリゴンクリッピングと言うらしいです。
この記事を書いている途中でこのアルゴリズムの存在に気付いたので試しに実装してみたのですが、自分で書いた方が処理速度が速かったです。
おそらく今回のスクリプトにおける制約条件を色々織り込んだうえで書いたからだと思いますが、流石に可読性は Sutherland-Hodgman algorithm で書いた方が圧倒的に高いですね。
分割した四角形の頂点を目的の形になるように移動させる
これはただの座標変換なのでそんなに難しくないです。
まず画像を分割してできた四角形の頂点座標を、アンカーを基準とした座標系に変換します。
これは平行移動と回転だけでできます。
そして目的の曲線にそって座標軸を曲げてあげれば完成です。
https://scrapbox.io/files/61b37e9662bd4f001db910ba.png
ここで注意したいのが、曲線の長さです。
はじめにこのスクリプトの条件として二つのアンカーを結ぶ直線と曲げた後の曲線の長さが変わらないようにする、という条件を設定しました。
これを守るためには、曲線上の任意の2点間の長さを計算できなければいけません。
円弧については $ l=r \theta で簡単に計算できます。
しかし(3次)ベジェ曲線の長さを一発で計算することはできません。
そこで積分(台形公式)を使って近似値を計算します。
3次ベジェ曲線のある点の座標は媒介変数 $ t を使って以下のように表されます。
$ x(t) = (1-t)^3x_0 + 3(1-t)^2tx_1 + 3(1-t)t^2x_2 + t^3x_3
$ y(t) = (1-t)^3y_0 + 3(1-t)^2ty_1 + 3(1-t)t^2y_2 + t^3y_3
ただし、$ (x_i,y_i) \;\; (i=0,1,2,3) は制御点の座標です。
この時、曲線の$ A \le t \le B の部分の長さは
$ \int_A^B \sqrt{x'(t)^2 + y'(t)^2} dt
この値を式変形だけで厳密に求めるのは難しいですが、台形公式で数値的に計算すれば十分実用的な精度で求められます。
「ベジェ曲線@曲げKR」の台形積分とコメントしてある箇所がこれの実装なので、積分する関数を変えれば他の曲線に沿って曲げることもできます。
obj.drawpoly() で描画する
さて、ここまでの処理で「元の画像を分割した四角形」と「それを変形させた四角形」が用意出来ました。
あとはこれらをもとに obj.drawpoly() を使って描画すればスクリプトの完成です!
とは言え、ここでも気を付けることが全くないわけではありません。
まず、そのまま描画してしまうと、四角形と四角形の境界に筋が出来てしまいます。
このスクリプトでは描画の前に obj.setoption("blend", "alpha_add2") としてあげることで筋ができるのをごまかしています。
https://scrapbox.io/files/61b38ba9ea382d001d2971c6.png
また、obj.drawpoly() の挙動がそもそも怪しいのではという疑惑があります。
下の画像は中央のカラフルな四角形の周りに、ある一辺を obj.drawpoly() で閉じたものを並べたものです。
上下の辺を閉じたものに比べて、左右の辺を閉じたものは何かおかしいのが分かるでしょうか。
今回参加している Advent Calendar 主催の /ePi5131 さん曰くこんなものらしいので、どうしようもなさそうです。 https://scrapbox.io/files/61b38e6baad61a001fdf88bd.png
code:lua
-- 右側を閉じる例
local w, h = obj.getpixel()
obj.drawpoly(
-w/2,-h/2, 0,
w/2, 0, 0,
w/2, 0, 0,
-w/2, h/2, 0,
0,0, w,0, w,h, 0,h)
これは分割数をわざと低くするときに影響してきます。
下の画像は分割数4で「円弧@曲げKR」を使っている例ですが、所々歪んでいるのが分かるかと思います。
https://scrapbox.io/files/61b392ecf6642b001e162d4d.png
この問題に対応するには、1つの四角形を更に細分化して obj.drawpoly() すればよいです。
下の画像は画像を $ 2 \times 2 に細分化している例です。
標準スクリプトの簡易変形にある分割数の設定がちょうどこれにあたります。
https://scrapbox.io/files/61b752546f138f001d577024.png
下の画像は右に行くほど細分化の度合を上げている様子です。
分割数があがるほどそれらしい変形になっているのが分かるかと思います。
https://scrapbox.io/files/61b756c70e0e95001f9347e1.png
分割数低めの時に画像が歪むのに気付いたのはこの記事の執筆中だったので、 @曲げKR.anm の紹介動画では触れていませんが最新版では細分化数を設定できるようにしてあります。
おわりに
今回のスクリプト @曲げKR.anm がどんな実装になっているかざっくり書いてみました。
これがもしスクリプトを書いている人、これから書こうかなと思っている人のヒントになれば幸いです。