\[ \newcommand{\sbr}[1]{\left[#1\right]} \newcommand{\br}[1]{\left(#1\right)} \newcommand{\cbr}[1]{\left\{#1\right\}} \newcommand{\abr}[1]{{\left|#1\right|}} \newcommand{\exbr}[1]{{\left\langle #1 \right\rangle}} \newcommand{\nbr}[1]{{\left\lVert #1 \right\rVert}} \newcommand{\fNorm}{\mathcal{N}} \newcommand{\sC}{\mathbb{C}} \newcommand{\sN}{\mathbb{N}} \newcommand{\sZ}{\mathbb{Z}} \newcommand{\sR}{\mathbb{R}} \newcommand{\sL}{\mathit{\Lambda}} \newcommand{\sSS}{\mathit{\Omega}} \newcommand{\sFW}{\mathfrak{W}} \newcommand{\sFB}{\mathfrak{B}} \newcommand{\sFO}{\mathfrak{O}} \newcommand{\sFF}{\mathfrak{F}} \newcommand{\sFP}{\mathfrak{P}} \newcommand{\Open}[1]{\mathfrak{O}(#1)} \newcommand{\Close}[1]{\mathfrak{A}(#1)} \newcommand{\sComp}[1]{{#1}^{c}} \newcommand{\sIn}[1]{{#1}^{i}} \newcommand{\sAd}[1]{{#1}^{a}} \newcommand{\sOp}[2]{{#1}^{#2}} \newcommand{\rt}{\mathbf{t}} \newcommand{\ReLU}{\mathrm{ReLU}} \newcommand{\rx}{\mathbf{x}} \newcommand{\rf}{\mathbf{f}} \newcommand{\cf}{\mathrm{f}} \newcommand{\rv}{\mathbf{v}} \newcommand{\re}{\mathbf{e}} \newcommand{\ry}{\mathbf{y}} \newcommand{\rz}{\mathbf{z}} \newcommand{\rh}{\mathbf{h}} \newcommand{\rr}{\mathbf{r}} \newcommand{\rW}{\mathbf{W}} \newcommand{\rF}{\mathbf{F}} \newcommand{\card}{\mathrm{card}} \newcommand{\herf}{\frac{1}{2}} \newcommand{\Od}{\mathrm{d}} \newcommand{\Ot}{\mathrm{t}} \newcommand{\Oh}{\mathrm{h}} \newcommand{\Op}{\mathrm{p}} \newcommand{\Ov}{\mathrm{v}} \newcommand{\Ohr}{\hat{\mathrm{r}}} \newcommand{\AHot}{\mathbb{1}} \newcommand{\Transe}{\mathrm{T}} \newcommand{\deriv}[2]{\frac{\partial #1}{\partial #2}} \newcommand{\eFh}{{F^\mathrm{h}}} \newcommand{\eFm}{F^\mathrm{h}} \newcommand{\efm}{f_\mathrm{m}} \newcommand{\efh}{f_\mathrm{h}} \newcommand{\efv}{f_\mathrm{v}} \newcommand{\de}{d_\mathrm{e}} \newcommand{\ah}{\mathrm{h}} \newcommand{\bphi}{\bar{\phi}} \newcommand{\hr}{\hat{r}} \newcommand{\hrr}{\hat{\rr}} \renewcommand{\Oh}{\mathrm{h}} \newcommand{\hO}{\hat{O}} \newcommand{\hZ}{\hat{Z}} \newcommand{\hE}{\hat{E}} \newcommand{\hT}{\hat{T}} \newcommand{\hTs}{\hat{T}_\mathrm{s}} \newcommand{\hTe}{\hat{T}_\mathrm{e}} \newcommand{\fNN}{f_\mathrm{NN}} \newcommand{\ifNN}{f^{*}_\mathrm{NN}} \newcommand{\Deltat}{\Delta_{\mathrm{t}}} \]
少しC++と関係ないが,プログラミング言語と周辺環境の使い方を学ぶためには,課題があったほうがよいので,複数のSVGをC++で作成し,inkscapeでSVGをPNG (Portable Network Graphics)に変換し,ffmpegでPNGをパラパラ漫画のように連続で表示する動画を作成する方法を紹介する.手順は以下のとおりである.この各手順をこのページでは詳細に説明する.
この課題では,今のインターネットで使われている基本となる技術をまんべんなく実践により学ぶことができるので,是非取り組まれると良い.ここでは,のような動画を生成する方法を例として説明する.動画というのは大変わかりやすい.君たちみたいな若者から老人までショート動画で時間を浪費してしまうほどにわかりやすいので,作れるようになっておくと良い.
まずは,SVGを複数出力するコードの例を示す.例えば,は,円軌道の軌跡をsin, cosの微分方程式を解くことにより得て,小さな円でこの軌道を描画する例である.以降このコードをmovie.cppと呼ぶことにする.
中でincludeの指定がされているmytools.hppは以前の解説で作成したSVGによるグラフ描画クラスや,ベクトルクラスR2をまとめたヘッダファイルである.内容はに定義されるとおりであるが,長く見づらいためこちらのmytools.hppのリンクから手元に取得して見ると良い.ヘッダは利用時に他のヘッダやcppのファイル中に記載の変数と名前が競合することがあったり,使いたい名前空間が競合することがため,名前空間を用意して,そのなかに各要素を定義している.名前空間は簡単にいえば姓と名前により人を区別するような機能と同じであるが,不慣れなものは名前空間の解説を参考にして理解してもらいたい.mytools.hppは
mytoolsの中にはTMUという名前空間が定義されていて,その中にR2やSVGが定義されているので,上記のコードのようにTMU::R2と指定して使う.円軌道を描くにはleap frog法を用いている(詳しく知りたいものは調べよ).出力ファイルが多量となるため,17行目でdir の変数にある文字列の階層を,filesystem::create_directoriesの関数により作成し,24行目でそこにファイルを出力するようにファイル名を決定し,ファイルを出力するコードを作成している.この状態でコードを実行すると dir の文字列"res"の階層に,連番が付与されたファイルが多量に出力されるはずである.実行して,階層 res を確認してみよう.
#include <iostream>
#include <fstream>
#include <string>
#include <filesystem>
namespace TMU{
using namespace std;
string str(double x){
return to_string(x);
};
string str(int x){
return to_string(x);
};
struct R2{
double x;
double y;
R2(double x_, double y_){
x = x_; y = y_;
}
R2(){x = 0; y = 0;}
};
R2 operator+(R2 a, R2 b){
R2 r;
r.x = a.x + b.x;
r.y = a.y + b.y;
return r;
}
struct SVGGraph{
double vx0, vy0, H, W;
double scale;
double r;
string s;
SVGGraph(){
scale = 210; // firefox が小数点以下の座標をうまく表示しないため座標を大きく引き延ばすための値
vx0 = -1.2; vy0 = -1.2; W= 2.4; H= 2.4; r=0.1;
s = "";
}
void setPointSize(double r_){
r = r_;
}
void circle(R2 p){
s += "<circle r='" + str(r*scale) + "' cx='" +str(p.x*scale) + "' cy='"+str(p.y*scale) + "' fill='black' />\n";
}
void drawVAxis(string title){
s += "<text style='font-size:" + str(0.05*scale) + ";' transform='translate(" + str(-1.1*scale) + ", 0) rotate(-90)'>"+title + "</text>";
s += "<line x1='" + str(-1*scale) + "' y1='"+str(1*scale) + "' x2='" + str(-1*scale)+"' y2='" + str(-1*scale)+ "' style='stroke:rgb(0,0,0);stroke-width:"+ str(0.01*scale) + "' />";
}
void drawHAxis(string title){
s += "<text style='font-size:" + str(0.05*scale) + ";' transform='translate( 0, " + str(1.1*scale) + ") rotate(0)'>"+title + "</text>";
s += "<line x1='" + str(-1*scale) + "' y1='"+str(1*scale) + "' x2='" + str(1*scale)+"' y2='" + str(1*scale)+ "' style='stroke:rgb(0,0,0);stroke-width:"+ str(0.01*scale) + "' />";
}
string serialize(){
string r = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='"+to_string(vx0*scale)+" " + to_string(vy0*scale) + " " + to_string(W*scale) + " " + to_string(H*scale) + "'>\n";
r = r + s + "</svg>\n";
return r;
}
};
}
階層resの内容の確認は, ls res と打つか,cd resと実行してresへ移動した後,ls と実行すると良い.例えば著者の環境だと以下のように表示される.途中省略している.
hiroki@hiroki2:~/git/cpp$ ls res/
graph_0.svg graph_115.svg graph_14.svg graph_2.svg graph_45.svg graph_60.svg graph_76.svg graph_91.svg
...
...
graph_8.svg
graph_114.svg graph_13.svg graph_29.svg graph_44.svg graph_5.svg graph_75.svg graph_90.svg
ここでは,前節までのmovie.cppにより生成した連番のsvgファイルをpngファイルへ一旦変換し,pngファイルをパラパラ漫画のように組み上げて動画を作る方法を説明する.linux(wsl上のlinuxでも可能)上か,mac osのterminal上で実行することで,これを行う一連のコマンドを説明する.Windowsであれば,WSL等を使ってlinuxの環境を用意してもらいたい(著者はWindowsを消してlinuxを入れてしまうことをおすすめするが.).
ここでのコマンドを実行するためには,ffmpegとinkscapeが必要である.ubuntuであれば,apt installで,macであればbrew installで入れることができるだろう.例えば,以下の用に実行する.
sudo apt install ffmpeg
brew install ffmpeg
上記環境の構築を済ませたら,SVGファイルを出力した階層へ,cd により移動する.lsを実行することで以下のように目的のSVGが存在することを確かめる.
hiroki@hiroki2:~/git/cpp/res$ ls
graph_0.svg graph_115.svg graph_14.svg graph_2.svg graph_45.svg graph_60.svg graph_76.svg graph_91.svg
...
...
graph_8.svg
graph_114.svg graph_13.svg graph_29.svg graph_44.svg graph_5.svg graph_75.svg graph_90.svg
上記SVGをすべてPNGに変換する.一つのSVGを変換するコマンドは次のとおりである.$iはファイル名で置き換える.1024は解像度でありこの数値は読者のPCのディスク容量と読者が満足できる解像度のトレードオフで適宜変更する.
inkscape "$i" --export-filename="$i.png" --export-width=1024
すべてのファイルを上記コマンドで一つずつ変換していては,大変であるため,以下のコマンドで一気に変換すると良い.
for i in *.svg; do inkscape $i --export-filename=$i.png --export-width=1024;echo converting $i; done
簡単に解説すると,for i in *svg; は,最後がsvgで終わる文字列のファイル名に関して処理を繰り返しで実行することを意味するコマンドである.文字列は,変数 i に格納されるので,このiを用いて,ファイルを順番に処理する.doは繰り返しで実行されるコマンドの始まりを意味する.doneは繰り返すコマンドが定義される範囲の終わりを意味する.
inkscape $i --export-filename=$i.png --export-width=1024; は実際にファイルを変換するコマンドである.i に入った文字列を取り出すには$をつけて,$iのように記載する.単にiとだけかけば,それは変数ではなく文字として解釈されて変数としてのiに入った文字列を使うことができないので注意する.
echo converting $i; は記載された文字列をそのまま画面に出力するためのコマンドで,変数の中身を画面に表示したりするために使われる.ファイルを順番に処理して行くため,すべての繰り返しが終わるまで時間がかかるから,その間不安にならないために,このコマンドは今どのファイルを処理しているのかわかるようにしている.
実行が終わったら,pngが生成されていることをコマンド lsや,ファイルブラウザから直接ファイルをいくつか開くなどのことにより確認しよう.
そしていよいよ動画を生成する.ここまでの状態であれば,以下のコマンドにより,動画を生成できるはずである.このコマンドの意味は,%dに数値がはいるようなgraph_%d.svg.pngというファイル名のファイルから動画を生成するというものである.毎秒30枚の速度でファイルの画像を切り替え,動画の形式はx264を用いたmp4であることを指定している.より詳しくは,インターネットで適当に調べてもらいたい.
ffmpeg -framerate 30 -i graph_%d.svg.png -c:v libx264 -pix_fmt yuv420p output.mp4
動画は,上記コマンドではoutput.mp4として生成されるはずである.これをmv output.mp4 ../output.mp4 などのコマンドで適宜わかりやすい上位階層等に移して,ファイルブラウザ等で探し,ブラウザなどmp4を再生できるソフトウェアで開いて中身を確認すると良い.
模範解答を以下に示す.
自然数\(n\)にたいして頂点の列\(p_1,p_2,...,p_n\in\sR^2\)があるとする.頂点\(p_i, p_{i+1}, i=1,2,...,n-1\)をつなぐ線分上をたどりながら\(p_i\)を\(i=1,...,n\)まで順番に巡回する点の軌道を動画で可視化する.要するにのような動画を作成せよということである.
模範解答をmovie_line.cppに示す.コンパイルにはmytools2.hppが必要である.
ヒントとして数式でこの方法を説明及び定義する.移動速度を\(V\in \sR\)とする.また,移動開始してからの移動距離を\(x\)とする.線分間の長さ\(L_i\)を \[ L_i = \nbr{p_i - p_{i+1}} \] と定義する.ただし\(\nbr{\cdot}\)はL2ノルムである.この時,距離\(x\)における線分を構成する始点を\(p_k\)とすれば,いずれかの\(\lambda\in [0, L_{k})\)が存在して,\(q\)を\(x\)における座標とすれば, \[ q_x = \frac{(1-\lambda) p_k + \lambda p_{k+1}}{L_k}, \] および, \[ x = \lambda + \sum_{i=1}^{k-1}L_i \] となる.次に,\(x\)の地点から次の\(D\)だけ進んだ場合に,\(k\)が変わるか,つまり次の線分に移るかどうかを考える.これは簡単に, \[ Q(D): \lambda + D \geq L_k \] の\(Q(D)\)が真となれば,次の\(k+1\)へうつり,そうでなければ同じ\(k\)で定義される線分上の点を取れば良いということになる.\(x\)は実際のところ,軌跡を描くだけであれば記録する必要はない.あとは\(\lambda + D\)が\(L_k\)からはみ出た分だけ次の線分に持ち越す処理をしたり,頂点上には必ず点を配置しわかりやすくする,などのことが考えられるが,その方法の導出は読者の課題とする.
こちらclass_access.htmlの基礎力課題の星の描画を,のように動画により行ってみよう.ただし,段々と色が変わるようにする.また,SVGの出力,PNGへの変換から,ffmpegを使う方法など,動画の作成はここで紹介したコマンドによる方法とする.エラーや環境依存性の問題がある場合,できるかぎり解決を試みよ.
https://standardtlak.org/numeric/plane-collision2D.htmlの課題の軌跡を,ここで紹介したように動画として可視化してみよう.重力や空気抵抗,反発係数を取り入れても面白いだろう.