Shader Graphを触ってみる
この記事は、ボイゲ Advent Calendar 2019 - Adventar 5日目の記事です。
ひとつ前(4日目): www.nicovideo.jp ひとつ後(6日目): hassakulab.com
まとめ
- 背景
- Shaderとか難しい、触ったことない、でも触れたらなんかすごそう
- 手法
- UnityでShader Graphというのが実装されたから使ってみる
- 結果
- Shader Graph怖くない
Shaderとは
普通のプログラムが使うCPUではなく、GPUで描画させるために必要な処理を記述したもの
Shader Graphって何がいいの
今までのShaderは、専用の(Cライクな)言語を覚える必要があった。 しかもShaderを記述するためには、行列演算を主とした幾何・代数への知識が必要だった。 そのため、実際に映像表現を作る人にとってハードルが高かった。
Shader Graphでは、Shaderのすべてをグラフ記述することが可能になり、学習コストが下がった。
// Shader Graphのスクリーンショット貼る
なので今まで足踏みしてた人も、使ってみようよ!という記事です。
Shader Graphを習う
あくまでも自分の場合だけど、
Youtubeにサンプル解説動画があるので、たくさん見よう ほとんど英語だけど、動画にUnity Editorの操作画面が出てるから、とりあえずトレースする
実は、Shader Graphを触る前に、WebGLを使ったレンダリングをフルスクラッチで書いてて、 その時の知見がいくつか有用だったので紹介。
VertexとFragmentシェーダー
WebGL(OpenGLやDirectXでも)では、GPUでの描画プロセスで、Vertexシェーダーを実行したあとに、Fragmentシェーダーを実行される。 Vertexシェーダーでは、描画される座標が計算されて、Fragmentシェーダーでは、その座標における色が計算される。
下の図では、Scene上にビームサーベルが配置されている。プレビュー画面では、カメラに映ったビームサーベルが表示される。 何気なく受け入れてしまうが、ビームサーベル(のメッシュ)の座標(x,y,z)を画面座標(X,Y)に変換しなければならない。 これがVertexシェーダーの役割だ。
普通に、Unityを触ってる限り、Vertexシェーダーはテンプレからいじる必要はない。 画面上にうまく表示されるようにカメラやオブジェクトの位置を調整するからで、 Vertexシェーダーを変にいじると、表示するオブジェクトが歪んだり、ぶっ飛んで行ったり、形が崩れたりする。
Fragmentシェーダーについてはいったん置いといて、Shader Graphでは、Vertex, Fragmentシェーダーを同一のグラフ上で記述する。 Unlit Shader Graphを作成すると、グラフ上にUnlit Masterというノードが追加される。 このMasterノードが最終的な描画内容を受けるアウトプットに相当する。
Vertexシェーダーの役割は、このMasterのPositionに相当する。 普通はいじる必要がないから、デフォルト値である「Object Space」を接続しておけばいい。
逆に、Vertexシェーダーを使う場合を考えてみる。
左のPositionノードは各頂点の座標(Transformで指定するやつ)、これが実描画の時に通常は、Object→Viewへと座標変換される。 ここでTransformノードで、View→Objectと逆変換を先に入れておくと、座標変換がキャンセルされ、常に同じ座標に表示されることになる。1
更に、timeノードを使うと、時間経過に応じた動きを追加することもできる。 試しに回転行列を作り、先ほどのPositionに掛けてみる。
ゲーム画面上でも、レンダリングされるものが回転しだす。
シェーダーでできることは、基本的にスクリプトでも実現可能だ。 回転するキューブであれば、FixedUpdate内でTransformにAddRotationし続ければいい。 シェーダーで実行することができれば、その独立度合が高くなる。 各スクリプトのタイミング問題や、処理速度の問題に頭を悩ませなくていい。
// 閉会式動画が作られるらしいので、やっつけでもうちょっと見た目のインパクトがあるものを作ってみる
上の図はのこぎり波をフーリエ変換したものを8次までシェーダー内で再現した。
のこぎり波のように、びょーん、びょーん、びょーんと拡大するシェーダーになる。ほんとはビートを刻むようなエフェクトっぽくしたかったのだが諦めた。2
Fragmentシェーダーは、Shader GraphではColorとAlphaでMasterに接続される。 簡単な例として、先ほどのキューブを、アルファグラデーションをかけてみる。
UVノードは、メッシュ内での(x,y)座標位置、Splitノードによりメッシュ状のX座標だけを抽出している。3 ゲーム画面上では、X座標に応じて透明になるレンダリングがされる。
先ほどのTimeノード、それにランダム性を加えるNoiseノードを組み合わせれば、ちょっとした登場エフェクトなども作れる。
ここでは、単色ばかりの簡単な例を出したが、特に2Dゲームであればテクスチャの扱い方がわかれば、応用はさせやすい。
冒頭でもいったように実用的なものは、Youtubeなどを参照してほしい。 Shader Graphの場合、ある程度のノードの塊でどんな処理か意識できるようになるのがポイントだと思う。
ゲームジャムでのシェーダーづくり
ゲームジャムをするときに、シェーダーまで手を出せないよ!という人も多いと思う。 ただ、ちょっとした座標の動き、表現の動きをシェーダーで作れると「やり方」に幅がうまれる。
例えば、最初の企画が「ここで、うわー!ってやるとババーンとなって!ドかーってなるかんじ!」って時に 当たり判定をいれて、ダメージ計算をして、破壊判定をさせるといった処理系は手を出しやすい。
それを実装する人をしり目に「じゃあ、おれそのババーンがドかーんとなるシェーダー作ります。」とか言えるとかっこいい。かっこよくない?
スクリプト+パーティクルシステムで実装すると、インターフェースを相談したり、パラメータ例を伝えたり、 結合時にタイミングを見ながら、そのパラメータを調整したりとなるが、シェーダーであれば、マテリアルまで用意しておいて入れ替えるだけで済む。
システム処理系と描画処理系の独立性が高い分、結合が簡単になるのである。
// なんか面白いオチ
スマホを立体ディスプレイにする
https://lookingglassfactory.com/:Looking Glassの出来が思った以上によかったので、昨今のディスプレイとレンチキュラーでどこまでのものができるか作ってみました。
動画では立体感があまり伝わりませんが、裸眼でもそこそこ見えます。 レンチキュラーレンズの特性上、特定の模様に弱いのと、視野角と解像度の最適化がまだできていないようです。
コードの追記数とTake数の記載
こんなことを書いたもんで、自分の投稿についてのメモを随時更新
Qiitaにコードを書く人、その難易度の指標として、追記回数とTake数を書いてくれないだろうか
— いくしーど (@ixsiid) 2018年5月13日
ちなみにこんな定義 追記数: 保存した回数 Take数: 根本的にうまくいかなくて、やり方・切り口から変えた回数 実際の回数はかなりざっくりです
追記が多いものはリファレンスが意味不明だったり、使い方が応用的だったり、 処理速度やタイミングのチューニング、抽象化を何度も行ったもの Take数が多いものは、自分が何をやりたいのかがプログラミング上で具体化できてなくて試行錯誤を繰り返しているもの
RFLP的に言えば、追記はPでTakeはL
追記 ~100, Take 5 qiita.com
追記 ~300, Take 2 qiita.com
RtMidiを使って仮想Midiデバイスのルーティング
仮想MIDIデバイスを登録してMIDIキーボードから受け取った信号を ちょっとだけ加工してMIDIインターフェースに送出したい
#include <iostream> #include <cstdlib> #include "RtMidi.h" void mycallback(double deltatime, std::vector<unsigned char> *message, void *userData) { unsigned int nBytes = message->size(); for (unsigned int i=0; i<nBytes; i++) { std::cout << "Byte " << i << " = " << (int)message->at(i) << ", "; } RtMidiOut *midiout = (RtMidiOut *)userData; if ( nBytes > 0 ) { std::cout << "stamp = " << deltatime << std::endl; /* これがちょっとだけ加工している内容 */ (*message)[1] = 64; midiout->sendMessage(message); } } int main() { RtMidiOut *midiout = new RtMidiOut(); // midiout = new RtMidiOut(); unsigned int outPorts = midiout->getPortCount(); if (outPorts == 0) { std::cout << "No out ports available!\n"; delete midiout; return 0; } midiout->openPort(0); RtMidiIn *midiin = new RtMidiIn(); unsigned int inPorts = midiin->getPortCount(); if (inPorts == 0) { std::cout << "No in ports available!\n"; delete midiout; delete midiin; return 0; } midiin->openPort(1); midiin->setCallback(&mycallback, midiout); // Don't ignore sysex, timing, or active sensing messages. midiin->ignoreTypes( false, false, false ); std::cout << "\nReading MIDI input ... press <enter> to quit.\n"; char input; std::cin.get(input); delete midiout; delete midiin; return 0; }
Raspberry Piで作るデバイスのメモ
Fluidsynth
$ (sudo) fluidsynth -a alsa -g 3 /{サウンドフォントファイル}
でMIDI Inputインターフェース待ち受け状態になる(立ち上げに数秒かかる) ※-g 3 はゲイン
MIDIインターフェース
インターフェース一覧表示
$ aconnect -o client 14: 'Midi Through' [type=kernel] 0 'Midi Through Port-0' client 20: 'microKEY-25' [type=kernel,card=1] 0 'microKEY-25 MIDI 1' client 128: 'FLUID Synth (659)' [type=user,pid=659] 0 'Synth input port (659:0)'
インターフェース同士を接続する
aconnect 20:0 128:0
音声出力
$ amixer -c ALSA contents numid=3,iface=MIXER,name='PCM Playback Route' ; type=INTEGER,access=rw------,values=1,min=0,max=2,step=0 : values=2 numid=2,iface=MIXER,name='PCM Playback Switch' ; type=BOOLEAN,access=rw------,values=1 : values=on numid=1,iface=MIXER,name='PCM Playback Volume' ; type=INTEGER,access=rw---R--,values=1,min=-10239,max=400,step=0 : values=-2000 | dBscale-min=-102.39dB,step=0.01dB,mute=1 numid=5,iface=PCM,name='IEC958 Playback Con Mask' ; type=IEC958,access=r-------,values=1 : values=[AES0=0x02 AES1=0x00 AES2=0x00 AES3=0x00] numid=4,iface=PCM,name='IEC958 Playback Default' ; type=IEC958,access=rw------,values=1 : values=[AES0=0x00 AES1=0x00 AES2=0x00 AES3=0x00]
よくわからんがこれの PCM Playback Route の values が出力先の変更? 1だとJack 2だとHDMI こいつのnumidを参照して変更する
$ amixer -c ALSA cset numid=3 1 numid=3,iface=MIXER,name='PCM Playback Route' ; type=INTEGER,access=rw------,values=1,min=0,max=2,step=0 : values=1
企業で働く外装エンジニアへ
昨今のソフトウェアの進化は早い。様々なアプリケーションが日々リリースされているが、エンジニアとして目をつけるべきをそれを可能としている環境が進化していることと思う。
3D CADにふれる機会は増えたし、導入のハードルはずいぶん下がった。それでもハードウェアエンジニアリングの進化はずいぶん遅い。
ソフトウェア開発の高速化、効率化が進んだ主な要因にコミュニティの形成しやすさがある。Web上には言語規約からサンプルコードまで蔓延し必要なデータシートにはすぐにアクセスできる。ばかりか、有能なツール群には間違いなく、FAQやユーザーコミュニティが存在し、様々な意見交換が行われている。同時にたくさんの方法論やフレームワークが提案され、それはWeb上で淘汰、進化しまた同時にそれらに合わせたツール群が開発される。
ハードウェアではかつては学会がそうだったと思う。しかし、今は研究の色が強くなり、方法論、実現方法、ツールや組織運営は企業のノウハウとして蓄積されるようになった。また、企業から組織に、組織からチームに、チームから個人に、ノウハウの蓄積はより細分化され、明文化されず、局所最適の様相を示しているのではないだろうか。
まず、私達は情報を発信し、受信し、共有する下地が必要だ。最初にフレームワークの話を進めるために、フレームワークの意味の共有をする。
> 開発・運用・意思決定を行う際に、その基礎となる規則・構造・アイデア・思想などの集合のこと。日本語では「枠組み」などと訳されることが多い。
「規則・構造・アイデア・思想などの集合」である。今のフレームワークはどうなっているか。
規則: 関連部署や、設計審査などのゲートを通過するために必要なドキュメントや承認経路である
構造: どのようなチーム編成、組織関連性で運営されているか。またどのようなツール、測定器、評価器でなされるか。
アイディア: 規則の最もらしさを担保する仮説、ないし事実である。例えば「品質には顕在化された品質と潜在的な品質が在る」などである。
思想: 組織をまとめる思考回路である。また評価関数を担保する指標でもある。「原価に適正な利益を乗せた額が売値である」、「納期厳守」などである。
続く