はじめに
オセロAdvent Calendar向けの3つ目の記事です。私のは長い記事ばかりでいい加減読むのが辛くなっていると思いますが、今回は軽めの記事になっていますので気軽に読んでいただければと思います(と思ったけど、思ったより長くなりました・・・)。ただ、12月22日公開予定の4つ目の記事は大変長くなっているので、それまでに英気を養っておいていただければと思っています。
前置きはこれくらいにして今回のテーマですが、オセロの石を返すアニメーション画像を作ってみることです。オセロアプリで石を返した時に使うアニメーションを想定しています。
何だ、オセロ Advent Calendarだから題材をオセロの石にしただけで、オセロに関係ないただのPOV-Rayの記事じゃないか、と思われた方も多いと思います。しかし本記事はオセロであることが極めて本質的に関わってくるのです。その理由は追って本文で書きます。
昔話
時をさかのぼること約30年、筆者はレイ・トレーシングなる技術を知りました。光沢のある球体をレンダリングした画像が雑誌に載っているのを見て(当時はインターネットなど普及していなかったのですよ)、ワクワクしたものでした。レイ・トレーシングは文字通り光線を追跡シミュレーションすることで物体の画像をレンダリングする技術ですが、当時球体1つを描くだけでも何時間・何日とかかるようなもので、レイト・レーシングなどと言われることもあったようです。今のオセラーの方なら、そんな計算資源があればBookをどれだけ増やせることかという謎の換算で、その計算量を実感するかもしれません。
もちろん当時の筆者にはそれを実行するような環境はなかったのですが、頭の片隅にこの技術のことが残りました。
そして約30年の月日が流れ、ひょんな事からオセロの石を返すアニメーションを作る必要性に迫られた筆者は、この技術のことを思い出しました。あの技術は現代でもまだあるのだろうか?
当時はなかったGoogleという文明の利器を使って検索してみたところ、POV-Rayというレイ・トレーシングのソフトがあることが分かったので、これを使ってみることにしました。
というのが、この記事の背景です。この経緯からおわかりいただけるかと思いますが、筆者はPOV-Rayに詳しいわけでも何でもないので、「やってみた」系の記事と捉えていただけると幸いです。
本題
POV-Rayでオセロの石を描く
ではPOV-Rayを使ってオセロの石を描くことから始めます。まず作る画像ですが、オセロのマス1マス分の正方形の画像にしたいと思います。この1マスに石が置いてある状態から、少しずつ角度を変えて裏返す画像にしましょう。角度は30度ずつ変えることにします。開始のコマを0コマ目とすると6コマ目で裏返ることになります。そのまま続けて12コマ目で元に戻るところまで、計13コマの画像を作りたいと思います。
作り方を大まかに言うと、カメラ(視点)の設定、光源の設定、背景面の定義、石の定義を行なってレンダリングを実行することで石を描くことができます。
#declare discRadius = 1.75; // 石の半径
#declare discThickness = 0.7; // 石の厚み
#declare boxSize = 4.25; // マスの1辺のサイズ
#declare cameraDistance = 25; // カメラから原点までの距離
#declare deg = 60; // 石の回転角度
// カメラの設定
camera{
location <0, 0, cameraDistance> // カメラ位置
look_at <0, 0, 0> // カメラは原点方向を向いている
angle degrees(atan2(boxSize / 2, cameraDistance)) * 2 // 視野角
right <1, 0, 0> // 座標系は左手系、アスペクト比は1:1
up <0, 1, 0>
}
// 光源の設定
light_source {
<-5, 5, 50> // 光源の位置
color rgb <1.4, 1.4, 1.4> // 光源の色
}
// 背景面の定義
object {
plane {<0, 0, 1>, -discThickness / 2} // 平面の法線ベクトルと原点からの距離
texture { pigment { rgb <0, 1, 0> } } // 平面の色
}
// 石の定義(白い面)
object {
cylinder {<0, 0, 0>, <0, 0, discThickness / 2>, discRadius} // 円筒形の天面と底面の中心
texture { pigment { rgb <1, 1, 1> } } // 色
rotate y*deg // y軸を中心にdeg度回転
rotate z*15 // z軸を中心に15度回転(固定)
translate <0, 0, discRadius * abs(sin(radians(deg)))> // z軸方向に平行移動
}
// 石の定義(黒い面) 説明は白い面と同様なので省略
object {
cylinder {<0, 0, 0>, <0, 0, -discThickness / 2>, discRadius}
texture { pigment { rgb <0, 0, 0> } }
rotate y*deg
rotate z*15
translate <0, 0, discRadius * abs(sin(radians(deg)))>
}
コメントを付けておいたので、大体の意味はわかるかと思いますが、意味がわかりにくい部分について補足しておきます。
最初にあるdeclare
は定数の定義です。各種長さを定義していますが、今回はPOV-Rayの空間上の1単位を1cm相当としました。オセラーならみんな知っているオセロの石の直径=3.5cm、半径=1.75cmは牛乳瓶のフタのサイズですね。
カメラの定義のangle
は視野の角度で、画像を1マスの大きさにしようとすると$\tan \theta/2 = (4.25/2) / 25$となるはずなので、逆算して求めました。また、right
とup
は画像のアスペクト比と座標系を定義するもののようで、今回はアスペクト比は1:1(正方形)で、座標は左手系になっています(多分)。
光源の色のrgbは本来は0.0〜1.0で指定するもののようなのですが、出来上がった画像が暗めだったので、試しに1以上の値を指定してみたら明るくなったのでこうしてあります(詳細はよくわかりません・・・)
背景面は後で述べる諸事情により、原色の緑を使っています。また、石の中心をXY平面上に置きたかったので、石の厚みの半分だけZ方向マイナスに移動した面を背景面としました。
石はまずは60度回転したものを描画しています。回転軸がy軸そのものだと真っ直ぐすぎる感じがしたので、回転軸を後で15度傾けています。そのまま回転だけすると背景面にめり込んでしまうので、z軸の正の方向に半径の$\sin 60^\circ$倍だけ移動しています。(若干盤面から浮くことになります)
細かい文法等は大分大学の方が日本語リファレンスを作成されておりますので、そちらをご参照ください。
さて、これで定義ができました。あとはPOV-Rayのオプションのところを288x288, AA 0.3
(画像サイズとアンチエイリアスの設定)とし、コマンドラインのところを+FN
(PNGファイル出力)としてレンダリングしてみましょう。
ちゃんとそれっぽく描けました。
全コマの画像を作成する
あとは#declare deg = 60;
の部分をちまちま変えて実行すれば良いですが(実際、当初作った時はそうしていました)、実はもっといいやり方がありました。
アニメーションに使うような連続画像を作る場合、先ほど+FN
と指定した場所にさらに+KFF13
を加えると、13枚の画像を連続でレンダリングしてくれます。そしてclock
という変数に0.0〜1.0の範囲を13等分した値(1枚目が0.0で、13枚目が1.0)を設定してくれます。つまり、角度の変数を定義していたところを
#declare deg = clock * 360;
とすれば、0度〜360度まで30度刻みで13枚の画像ファイルを一気に作成してくれるのです(もちろん、0度と360度の画像は同じになります)
便利ですね!
背景色を透過させる
さて、今回のテーマはオセロアプリで使う用に石のアニメーション画像を作成することでした。オセロアプリで使う場合、盤面自体をいろいろな理由で着色することもあるので、石の画像の背景は透過色にしておきたいところです。PNGファイルにはアルファチャンネル(透過度)を付けることができます。先ほどの画像で緑色が見えている部分が透過すべき部分なので、これを利用してアルファの値を求めて設定したいと思います。この計算をしやすくするために、背景を原色緑にしたのでした。クロマキー合成みたいな方法ですね。今回は影の部分もあるので、いい具合に透過度を設定したいところです。
出来上がるべき画像は、オセロの石とその影の画像なので完全にグレースケールで各ピクセルのRGBは全て同じ値となり(この値をVとします)、何らかのアルファの値$\alpha$が設定されることになります。この画像の背景に原色緑があると、先ほどの画像になるはずなので、元の画像のRGBの値をそれぞれR,G,Bとすると
$$B = V * \alpha / 255\\
G = V * \alpha / 255 + 255 * (255 - \alpha) / 255$$
が成り立つはずです。これを解いて、$\alpha$とVの値を求めます。Pythonで書いてみました。
import cv2
import matplotlib.pyplot as plt
import numpy as np
for i in range(1, 14):
# 元のファイルの読み込み
file = 'disc{0}.png'.format(format(i, '02'))
img = cv2.imread(file)
# 変換後の画像用
convert_img = np.zeros((img.shape[0], img.shape[1], 4), dtype='uint8')
# 元の画像のBとGの値(OpenCVで読み込んだのでBGRの順になっている)
blue = img[:, :, 0]
green = img[:, :, 1]
alpha = 255 - (green - blue)
alpha[green <= blue] = 255
# 元の画像で緑色が完全に見えている部分が必ずしもG=255ではないので、ほぼ緑だった部分はG=255とみなす。
alpha[alpha < 20] = 0
# 変換後画像のBGRAをそれぞれ設定
# Bの後半の長い部分はゼロ除算防止(参考 https://www.it-swarm.net/ja/python/ゼロ除算で0を返す方法/1049445598/)
convert_img[:, :, 0] = blue * np.divide(255 * np.ones(alpha.shape, dtype='float'),
alpha.astype(np.float),
out=np.zeros_like(alpha, dtype='float'),
where=alpha!=0)
convert_img[:, :, 1] = convert_img[:, :, 0]
convert_img[:, :, 2] = convert_img[:, :, 0]
convert_img[:, :, 3] = alpha
# ファイル出力
cv2.imwrite('cvt' + file, convert_img)
この手法が使えたのも、対象物がオセロの石で白黒2色で最終的にR=G=Bの画像を作れば良かったから簡単にできたわけです。将棋の駒を返す画像だったら、いろいろな意味でこんな簡単にはいかないはずです。
ほら、本質的にオセロであることが関わっているオセロ Advent Calendar向けの記事ですね!(無理やり)
変換後の画像をMacのKeynoteに貼り付けて、背景に赤い四角を描いてみました。ちょっと見づらいですが、影の部分もきちんと透過していることがわかります。
これもPythonのfor文で回したので、13枚分の画像ができています。
アニメーションファイルを作る
ここまで出来れば、あとはアプリ側でこれらの画像を連続して表示するようにすればパラパラ漫画的にアニメーションすることができます。
ただせっかくなのでブラウザ上でも表示できればと思って、いろいろ調べてみたところアニメーションPNGなるファイル形式があることがわかりました。アニメーションGIFのPNG版ですね。ただし、IE, Edgeや古いブラウザではうまく表示できないかもしれません。
作成方法ですが、Macではアニメ画像に変換する君というストレートな名前のアプリで作れるようです。Windowsの場合も「アニメーションPNG」でググればいろいろと作成用アプリが出てくると思います。
「アニメ画像に変換する君」はLINEスタンプ用のアニメーションファイルと、Webページ用のアニメーションファイルを作ることができますが、今回は後者を選びます。先ほど作成した13枚のファイルを貼り付けて、フレームレートやループ回数を指定すると簡単にアニメーションPNGファイルが作れます。
で、アニメーションPNGを作って貼り付けて一度記事を公開したのですが、どういうわけかQiitaの公開記事ではアニメーションPNGがうまくアニメーションしなくなるようです(下書きの時は見えていたのですが・・・)。
仕方がないので急きょアニメーションGIFに作り直しました。
出来上がったものがこちらです。
あ、貼り付けるファイルを間違えました。本当はこっちです。
ちゃんとアニメーションして見えていますでしょうか?
おわりに
これでこの記事は終わりなのですが、多分いろいろとモヤモヤされている方もいらっしゃるような気がします。気になる方はGitHubにソースコードを置いておきましたので、ご参照いただければと思います。
ではまた12月22日の記事でお会いしましょう!