18のWakattaaです。よろしくお願いします。
皆さんBlenderは使ってますか?Blenderは3Dでいろいろできるすごいソフトです(雑)。しかも無料です。みんなも使ってみようね。
今回はBlenderでいろいろ面白いことができるよ!という紹介をするために、こんなのを作ってみようと思います。
これはもともとは普通の一枚の板ポリゴンです。ノード構成を頑張るとこんなのができます。というわけで、今回はこれを作ってみましょう。(注:Blenderの操作が大体わかってる人向けになります。この記事はだいたい、「Blenderの基本操作のチュートリアルを一通りやり終えたけど...で?」みたいな人へのネタ提供みたいなつもりで書いています)
scriptノードを使う
ところで、マテリアルの中のノードにこんなのがありますよね。
多分Blenderを使っていてこれのお世話になることはあんまないんじゃないかと思います。日本語の資料も少ないからね。仕方ないね。
簡単に言うと、これを使えばプログラミングっぽくノードを構成できます。公式referenceはこ↑こ↓。https://github.com/imageworks/OpenShadingLanguage/blob/master/src/doc/osl-languagespec.pdf
これの出番さんがなかなかないので、今回はこれも(無理やり)使ってみようと思います。
まず最初に、Render Engine をcycleにして、Open Shading language(これの略がOSL)にチェックを入れます。残念ながら今のところ(2.80)EeveeはScriptをサポートしていません。悲しいねえ。でまず正方形ポリゴン出してマテリアルを作って、scriptさんを出します。そしてscripy->Text Editorの画面も出します。そしてNewを押すといろいろ打ち込めそうな画面が出てきます。
これはBlender内蔵のText Editorです。(外部からコードを読み込むこともできるが今回は省略)ここにOSL呼ばれる言語でいろいろ書けます(C言語に似た文法です)。プログラミングわからんという人も、とりあえず脳死でこう書いてみましょう。
shader blog(float input = 0, output float out = 1)
{
out = 1.0 - input;
}
今回は1から入力値を引いた値を出力します。そして名前をわかりやすいのに変えます(Textだけだと味気ない)。そして**拡張子「.osl」**を必ずテキスト名の末尾に書いてください。マテリアルのscriptの方でも今書いたテキストを選んでおきます。そしてRun script を押すとコンパイルしてくれます。うまくいけばこんな感じになります。
(ViewerノードについてはNode Wranglerで検索、多分今回の制作で必須ツール)0またはそれ以下が黒、1またはそれ以上が白と表示されてるので、うまく動いていることがわかります。
とりあえずshader blogを一つの関数と考えれば大丈夫です。C言語における引数がfloat input = 0, output float out = 1の部分です。出力の部分にoutputをつければ、ノードの左側の部分に、なければ右側の部分に点が追加されます。また、この引数は必ず(outputも)初期値を設定しなければならないため、= 0とつけています。
作っていこう
製作に入ります。今回の制作物のノード構成はこんな感じです。
まず最初のアイディアとして、この正方形をxy座標系とみなします。Geometryのposistionがxy座標を出力できるとことがわかると思います(ベクトルをNode Wranglerで見る際には、Rがx,Gがy,Bがzに対応する)。
ここから「穴の部分を描写するところ(つまり内側)なら白、そうでないなら黒」を出力するようにしたいです。そうすれば「外側」と「内側」を分けて処理すればいいことになるからです。ノード構成を頑張ってもいいですが、if文使った方が速そうですよね。scriptを使ってみましょう。
求める条件は「xまたはyの絶対値が一定値以下なら白(つまり1)、それ以外は黒」なので、このようにコードがかけます。(その四角の大きさを外のValueノードから入力しているのは、script内でその一定値を定義するといちいちscriptの中をのぞかなくてはいけないのでめんどいから)
ちなみにscriptを使わないとこんな感じです。
Eevee大好きマン(自分もそう)はscript使えないのが残念ですが、MathのGreater/Less thanノードを使えばif文と同じ事がほぼノードだけで実現できると思います。
次に、「今見てる点は、無限に深い穴を掘った穴の場合の時、どの点に対応するのか」を考えます。今回の制作の山場だと思います。重要なので、自分はそれを出力するノード群をグループ化しました。
線がぐちゃぐちゃになってしまいますが、頑張りましょう。
(こういう風にノード間の線に点を入れて整理したい場合はShift+右クリックしながら線を切ると、点で区切れます)
(これ全部scriptで記述してもいいんじゃ?という方もいるかと思います。今回の制作はscriptなくても多分再現できると思うので正直どこをscriptにして処理させてももいいです。こんかいscriptを使っているのは単に「普段使わない機能を使ってみよう!」というお気持ちでやっているだけで、実際の製作ではEeveeへの互換性がないのであんまり使ってもいいことはないです。ただ、今回のように「if文」「for文」が効果を発揮する場合もあるので、役立たずではないと思います。今回、読者はあんまりscriptの使用経験がないことを想定してるので、たくさんは使わないつもりです。「ここ絶対script使った方が見やすい!」と思ったら使っています。)
まずは4つの各面について、「今見ている点が穴の側面だった時の座標」を計算します。
どうやってこれを算出するかを考えます(ベクトルの知識がある程度必要です。センター数Bが解けるなら理解できると思います。)。まず、x軸に垂直で、生の領域にある面についての算出を考えます。ここで視線ベクトルを(これは3次元ベクトルであることに注意)、先ほどvalueノードで設定した原点から淵までの距離を(これは定数)、描写している点の座標をとします(Pのx成分を,y成分をとします、も同様)。
よく見れば、「からをいくらか伸ばしたら、求める面に必ず接触する」ことがわかると思います。どのぐらい伸ばすかというと、その伸ばすスカラー量をとして、
が成立することが、の条件だといえます。したがって、
となります。これをノードで実現すればいいです。よって、まずこの平面での、求める仮想上の座標は、
です。ほかの4面についても同様です。その処理は、また新しくグループ化しておきました。
(inputの後ろで-1倍していますが、これがないとなぜかエラーになりました。原因はわからずです、ごめんなさい・・・)
ちなみに、この2つ上の画像で外積を使っていますが、これは単なる筆者の趣味です。上から順に、(1,0,0)、(0,-1,0)、(-1,0,0)、(0,1,1)を出力しています。外積を使う必然性は全くありません。(作っている途中で必要になるかもと作っておいたのを残してるだけです。)
さて、4つの座標を計算したら、つぎに「どこの面を表示するべきなのか」を計算します。なんだかif文の出番っぽいですね。scriptを使ってみましょう。
shader select(vector vec1=(0,0,0),vector vec2=(0,0,0),vector vec3=(0,0,0),vector vec4=(0,0,0),vector norm1=(0,0,0),vector norm2=(0,0,0),
vector norm3=(0,0,0),vector norm4=(0,0,0), output vector resultpos=(0,0,0),output vector resultnorm=(0,0,0),output int resultnum=0)
{
vector vecs[4] = {vec1,vec2,vec3,vec4};
vector norms[4] = {norm1,norm2,norm3,norm4};
point C = point("camera",0,0,0);
C = transform("common","object",C);
vector P2 = transform("common","object",P -point("object",0,0,0));
vector tmpvec;
int number;
float prelength=99999;
float tlength;
for(int i=0;i<4;i++){
if(dot(norms[i],P2-C)<0){
tlength=length(C-vecs[i]);
if(prelength>tlength){
number=i;
prelength=tlength;
}
}
}
resultpos = vecs[number];
resultnorm = norms[number];
resultnum = number;
}
今見ている点が「描写する面」にある条件とは、
- 視線ベクトルと、候補の仮想上の面の法線ベクトルが逆向き
- 上の条件を満たす面のうち、よりカメラに近いもの
です。前者の条件は、「2つのベクトルの内積が負の値」であることと同値です(script中のdotの部分)。
後者の条件を判定するために、まずカメラの座標を取得します。script中のpoint C = point("camera",0,0,0) -point("object",0,0,0);です。transformのところは今は無視してください。
入力した4つの候補の点と、そのカメラの座標の距離を比較して、一番近いものを出すようにしました(細かいことはコード参照、疑問点があったらコメントか筆者のTwitterに来てください・・・)。
ちなみに、scriptを使わないとこんな感じになりました。
ややこしいですね。scriptの強さがわかると思います。
さて、最終的な出力はこうなると思います。
テクスチャ用の座標を出力する
これと、先ほどの「外側」の座標を組み合わせると、こうなります。
これにNoiseテクスチャをつないでみましょう。
いい感じですね。しかし、普通の画像テクスチャ(https://3dtextures.me/tag/wood/ からいただきました。)を張ろうとすると・・・
は?
みたいなことになるわけです。なんでこうなるのかというと、NoiseテクスチャはBlender側が勝手に面情報からうまくテクスチャを張ってくれるのですが、普通の画像テクスチャだと「どういう風に貼るの?」という情報が必要になってくるわけです(そのためにUV展開するわけですね)。このままだと、「xy平面上に貼る」というデフォルト仕様上のまま処理してしまうので、z軸に従って伸びてしまうわけです。
というわけで、仮想上の穴にテクスチャを張るために、新しく「テクスチャ用」の画像情報を出力しましょう。そのために、方針として「穴の中にxy平面を折り曲げた際の座標」を計算します(大体画像みたいなイメージ)。
この画像の場合では、z座標の絶対値を、y軸の淵の部分の値から引けばいいことがわかります。scriptを使って、こんな感じにかけました
これを画像テクスチャにつないでみると、
いい感じですね!
オブジェクトの回転、平行移動、拡大縮小に対応させる
Objectモードにして、出来たものをちょっと動かしてみましょう。
はい、こんな感じになると思います、ダメです(わかりやすくするために穴ではないところは明るくしました。)。
今までは、すべてこのポリゴンが回転縮小平行移動なしに、原点に存在することを前提としてノードを組んできました。だから原点から動かしたときのことは考えられていないわけです。
じゃあどうすれば治るかというと、「このポリゴンから見た座標系に、取得した情報を書き換える」という処理が必要です。先ほどの「これは今はいったん無視」は全部これ関係です。プログラミング的にこれをやろうとすると4次の変換行列を構成して処理しなければならないですが、(OSLには行列演算がちゃんと定義されています)実はBlenderにWorld座標とObject座標の変換を自動でやってくれる機能があります。ありがたいですね。(例えば法線や視線ベクトルを変換する場合には、平行移動は考えないことに注意してください。)
こんな感じです。ちなみに、こんな風にすればscript無しでも行けます。
おしまい
今回はこのほかにも仮想上の光源を設定してみたりしています。今回はそこまで解説しませんが(ここまでできた人ならできると思います。)、うまいことやればいろいろできそうですよね。皆さんもBlenderを使っていきましょう。