Unity板ポリレイマーチング入門
Unityゆるふわサマーアドベントカレンダー 2019 ゆるふわアドカレ
コード全文へのリンク
Unityの板ポリでレイマーチングを描こう
GLSLでレイマーチングを描く人たちのように、Unityでレイマーチングを試してみよう!
この入門ページでは基本的な手順を説明していきます。
下準備
https://gyazo.com/8b8dec0bf9cd5ffe3f2b5e520b6effbb
Quadをシーンに配置します。Positionは(0, 0, 0)にします。
この四角形がカメラに映るように、Main CameraのPositionは(0, 0, -1)くらいに調整します。
https://gyazo.com/6bfcaa8c023e2e9a60b88bb3dfb04886
レイマーチングを描くためのシェーダーを用意します。アセットにUnlit Shaderを作成します。
作成したシェーダーを右クリックし、マテリアルを作成するとそのシェーダーが適用されたマテリアルを作ることができます。
マテリアルをドラッグしてインスペクターのQuadに持っていくと下準備は完了です。あとはシェーダーを描いて保存するたびに、Unityエディタの板ポリに絵が出るようになります。
レイマーチングを行うために座標を調整する
https://gyazo.com/54589e3168fc9b54d47ec1b6ea7e53a8
Unityでシェーダーをダブルクリックするとエディタで開かれます。
シェーダーファイルの内容で、板ポリのレイマーチングに使うのはフラグメントシェーダーだけです。
エディタで下の方にあるfrag関数がそれです。
デフォルトのUnlitシェーダーは一枚の画像ファイルを楽に扱いやすい状態になっています。
uv座標は四角形の左下が(0, 0)、右上が(1, 1)になります。
https://gyazo.com/d5c769ebc6fa0a1c4a5d041eb1d041b7
この原点座標(0, 0)を四角形の真ん中に変更します。
色の変数を変更する
レイマーチングはなかなか絵が出ないものです。まずは辛抱強く写経していきましょう。
https://gyazo.com/41f2dc68f439403d7769cadce2aba8b1
色を格納する入れ物を変えていきます。
フラグメントシェーダーでのお絵描きでアルファ値は扱いません。ですので、RGBだけを変数として使いやすくするため、float3の入れ物に変更します。
RGBすべてをゼロにした状態でシェーダーを保存し、いったん絵を確認してみましょう。このフラグメントシェーダーはfixed4型をリターンすることで絵が出ます。
https://gyazo.com/781b2aa1956992e4d4c8743092ced16c
黒い板ポリが見えるようになりましたか?
https://gyazo.com/9bc29f1759fedaf20c4b912612d195f4
もしエラーがある場合はピンク色になります。
シェーダーを選択している場合はインスペクターにエラー内容と行番号が表示されます。またはコンソールを開いてログを確認しても良いでしょう。
該当の行を修正していきます。
https://gyazo.com/73c49616a696dbc5c6dab1986b9161f5
色を変えてみることにしましょう。ここでは色のRGに座標を設定しています。
https://gyazo.com/4e186e864b662119b3bb01137d32294a
板ポリにグラデーションが表示されました。何が起きているのでしょうか?
これは座標を色で見るやり方です。
板ポリの座標は左下が(-1, -1)、右上が(1, 1)です。右にいくほどにuの値が増えるので赤くなり、上にいくほどvの値が大きくなるため緑に近づきます。
色が出せることがわかったので、板ポリは黒い状態に戻します。
float3 col = float3(0, 0, 0);
カメラとレイを定義する
ここでコードのコメントを整備しながら見てみましょう。
https://gyazo.com/c1803cb787ed1d7107cab43cb3eb2e8b
レイを定義する行を追加しました。
奥行きのzが増えていることに注意してください。
レイの初期位置roを原点よりも手前(zがマイナス)に定義しています。この場所からレイはrdで定めた方向に進んでいきます。
レイの進むxy方向をpにあわせています。そして奥行きのzは1です。レイは出発地点から板ポリの全ピクセルに向けて大量に発射されるようなイメージを持ってください。
球を出す
マーチングループを描いていきましょう。
レイはマーチングループで決められた回数だけ前に進みます。
https://gyazo.com/4f5a070e3b8a98185ae2b8cb03125060
https://gyazo.com/72f8d4e274d1e120f2d33ee4adcb6494
何行か書き足してみると円が表示されました。実はこれは球です。
https://gyazo.com/2d878c1f99280fec97179acd99fe9397
https://gyazo.com/aee04152017e21f1d709f948b23f5a42
map関数に切り出します。
ライティングしてみる
https://gyazo.com/e2dc0fd17c871296259778de18363ea7
法線を算出するnormal関数を追加します。
https://gyazo.com/f899481f6d75f1de28346f367019a8b4
衝突したタイミングで法線を求めます。
でも、これをどう使えばいいのでしょうか?
https://gyazo.com/afbb33f1b2a5d02d145f7d8a4f2dd9ac
https://gyazo.com/fa1b1f3c750bf33d2d2f53197c16c830
ランバートでライティングしてみます。dを使ったものは明るすぎるので、いったんコメントアウトします。
https://gyazo.com/06798021207ed39cbcbb1633fd485c89
https://gyazo.com/62dc5896958c5e622d54c89cf9b239cf
別のライティング方法も見てみましょう。これはレイマーチングならではのアンビエントオクルージョンです。
※実際にはこれはフォグであり、AOとは逆のはたらきをしています
球以外のものを出す
https://gyazo.com/031e371b0314008c038362250218c734
https://gyazo.com/6e52b32bda40f65d3e05a9a82f3af4da
形状を扱いやすくするために関数を分けてみましょう。ここでは球の距離関数をsphere、箱の距離関数をboxとしています。
map関数から箱の距離関数を呼び出して表示してみましょう。
回転させる
https://gyazo.com/d7596d026e2cc319ea18ccd6f90ac283
https://gyazo.com/4383942af9dbf4211f50431b2d71a9d3
https://gyazo.com/e1963a78ab5d390088b61487c6d02e97
回転させてみます。
繰り返しや折り畳みを使って複雑な形状にする
https://gyazo.com/269d8dc206308faa0448fee8d8472537
https://gyazo.com/f23cb9ebb7a4750e230fd80b961809d6
座標を加工します。
座標のabsを取ってから引き算をし、回転するという操作をループで何回か行うと複雑な形状が生まれます。
https://gyazo.com/827be189ce4b47042a63590419c57001
別の回転のやり方も見てみましょう。これはpolar mod、あるいはfold rotateと呼ばれるテクニックです。
https://gyazo.com/168b2c4715db2ac4b603c5659253fbac
https://gyazo.com/b260f6542f57429b2c27fa918eee99ca
カメラを調整する
FOVを導入する
ここまではFOVを1.0で扱ってきました。これを変えてみます。
1.8
https://gyazo.com/97293125f43b819e90165860f3cf9c02
https://gyazo.com/68143dda1d4742a591f4f24846c83573
0.5
https://gyazo.com/f91142e9dde1f47cb05079463e86cf40
https://gyazo.com/6adda42755630687546dd3c595c02fb3
レイマーチングでは画面の情報量が多いほどルックが向上しがちです。
魚眼ルック
fov = dot(p, p);
https://gyazo.com/5f0915bc39d58ff38166e32cfd91c5d4
https://gyazo.com/b17364ff6e09557b4ad46032ce0407be
fov = 1.0 - fov;
https://gyazo.com/30e4ab7ddb580c28fa29167391aa546e
https://gyazo.com/4ceee1df117e84d0dde86b05a4b80f48
LookAtを設定する
https://gyazo.com/ba05bc5b1d285fbc3c3b11dc163a046e
カメラを動かしてみる
カメラはtargetを向くようになっているので面白い動きのルックが作れる
https://gyazo.com/2f36c7061ae18c0d15878b0a95365a67
https://gyazo.com/9cb5b8c4fdc8203f8fcc2d309f97a04c
光源ベクトルにカメラ座標を利用して定義する
https://gyazo.com/2dce33a6f819fc16eb533252056f9284
https://gyazo.com/a729ec4a7bfd09c60bdcb92406a87a6e
https://gyazo.com/6c4e53bb3465d9ddf45ab1bf696385f2.png
【GLSL】【シェーダー】文系で数学なんもわからんエンジニアだって、シェーダー完全に理解したい!!〜レイマーチング入門編②〜
https://gyazo.com/cf6774cb26603a11ac4befa502010979
HSV
https://gyazo.com/4c552072ba77ec144d1390fd130d5128
https://gyazo.com/5159d1520aa2699aecceb60027f29880
https://gyazo.com/e1aa76c0dfa63a005ca7214da21d7571
https://gyazo.com/5ca23b989cc234406adb0bfc19a5f920
ランバート
https://gyazo.com/128655732b1c1758eb670b57a3b1700a
https://gyazo.com/47fcfdc44996199243460f66305ce205
FOG
https://gyazo.com/522cd6a9b4b97bdf26918a470078b1bb
https://gyazo.com/e48dc7681a57d799ec9533a48799f4aa
SSS
https://gyazo.com/e8332a75389198c369292cfd253f86db
https://gyazo.com/16036186c695ca59b523e06aec6678d6
https://gyazo.com/ba14a0061247d58eff4ad41b547cef4d
カメラ
https://gyazo.com/6958fa967780151846f859340887a71a
コード
https://gyazo.com/0ac36056525f8a7e7401f55ddcf6f58f
code:QuadRaymarching.HLSL
Shader "Unlit/QuadRaymarching"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
// make fog work
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
// MOD
float mod(float x, float y)
{
return x - y * floor(x / y);
}
float2 mod(float2 x, float2 y)
{
return x - y * floor(x / y);
}
float3 mod(float3 x, float3 y)
{
return x - y * floor(x / y);
}
float4 mod(float4 x, float4 y)
{
return x - y * floor(x / y);
}
// SMIN
float smin(float a, float b, float k)
{
float res = exp2(-k * a) + exp2(-k * b);
return -log2(res) / k;
}
float noise(float p)
{
float fl = floor(p);
float fc = frac(p);
return lerp(frac(sin(fl)), frac(sin(fl + 1.0)), fc);
}
// 回転
float2 rot(float2 p, float a) {
return float2(p.x*cos(a) - p.y*sin(a), p.x*sin(a) + p.y*cos(a));
}
float3 qrot(float3 p, float3 v, float a)
{
float4 q = float4(sin(a / 2.0) * v, cos(a / 2.0)); // quaternion
return cross(cross(p, q.xyz) - q.w * p, q.xyz) * 2.0 + p;
}
// pmod(polar mod, fold rotate)
float2 pmod(float2 p, float s) {
float a = UNITY_PI / s - atan2(p.x, p.y);
float n = UNITY_TWO_PI / s;
a = floor(a / n) * n;
return rot(p, a);
}
// box
float box(float3 p, float3 q, float r) {
float3 p2 = abs(p) - q;
return length(max(p2, float3(0, 0, 0)));
}
// sphere
float sphere(float3 p, float3 q, float r) {
length(p - q) - r;
}
// Torus
float torus(float3 p, float q, float r)
{
return length(float2(length(p.xz) - q, p.y)) - r;
}
// map
float map(float3 p) {
float3 q = p;
q /= 2;
for (int k = 0; k < 5; ++k) {
q = abs(q) - 2;
q.xy = pmod(q.xy, 2.4);
q.xz = pmod(q.xz, -3.3);
}
/*
for (int k = 0; k < 5; ++k) {
q = abs(q) - 2;
q.xy = rot(q.xy, 2.4);
q.xz = rot(q.xz, -3.3);
}
*/
return box(q, float3(1, 1, 1), 1);
}
// 法線
float3 normal(float3 p) {
float2 xy = float2(0.001, 0);
return normalize(float3(
map(p + xy.xyy) - map(p - xy.xyy),
map(p + xy.yxy) - map(p - xy.yxy),
map(p + xy.yyx) - map(p - xy.yyx)
));
}
fixed4 frag (v2f i) : SV_Target
{
// 原点座標を四角形の中心に変更する
float2 p = 2 * i.uv - 1; // position
// 色を用意する
float3 col = float3(0, 0, 0); // color
// レイの初期位置
float3 ro = float3(1.5, 0.5, -3.0 + 3.0 * sin(_Time.y)); // ray origin
// 暫定の「上」
float3 up = float3(0, 1, 0); // Y-up
// ターゲット(カメラ向き)
float3 ta = float3(0, 0, 0); // target
// 前
float3 fwd = normalize(ta - ro); // forward
// 横
float3 side = normalize(cross(up, fwd)); // side
// 上
up = normalize(cross(fwd, side));
// FOV
float fov = 1.0; // fov
// レイの進む方向
float3 rd = normalize(p.x * side + p.y * up + fov * fwd); // ray direction
// レイ
float3 ray = ro;
// 距離を格納する変数
float d;
// 法線
float3 N;
// ループの進んだ回数(アンビエントオクルージョン)
int k;
// マーチングループ
for (int j = 0; j < 192; ++j) {
d = map(ray);
if (d < 0.001) {
N = normal(ray);
break;
}
k = j;
ray += d * rd;
}
// 衝突したところを明るくする
//col += 1 / d;
// アンビエントオクルージョン
col += (float)k / 192;
// カメラ座標を光源のベクトルに利用する
float3 L = normalize(ray - ro);
// ランバートのライティング
col += max(0,dot(N, L));
// 色の出力
return fixed4(col, 1.0);
}
ENDCG
}
}
}
https://gyazo.com/ebc9f529038dba432ce3c3209b9dab53
code:ColorfulSphereRaymarching.HLSL
Shader "Unlit/ColorfulModSpheres"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
sampler2D _MainTex;
float4 _MainTex_ST;
float mod(float y, float x) { return y - floor(y / x)*x; }
float3 mod(float3 p, float a) { return p - floor(p / a)*a; }
// hsv
// 最初はsとvにはとりあえず1.0を与え、座標などからhに与える値を作り出して使います
float3 hsv(float h, float s, float v) {
float4 t = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
float3 p = abs(frac((h).xxx+t.xyz) * 6.0 - t.www);
return v * lerp(t.xxx, clamp(p - t.xxx, 0.0, 1.0), s);
}
// 2次元座標での回転
float2 rot(float2 p, float a) { return float2(p.x*cos(a) - p.y*sin(a), p.x*sin(a) + p.y*cos(a)); }
float ind; // object's index
float distanceFunction(float3 pos, float size) { //距離関数
return length(pos) - size;
}
float map(float3 pos, float size) {
// マンハッタン距離で色のインデックスを作る
float ti = floor(mod(2.5 * _Time.y, 4));
float ia = ti * UNITY_TWO_PI / 4.;
float3 ipos = pos + float3(1.5, 2., .0) * ti / 2.;
ipos.xy = rot(ipos.xy, ia);
ipos.yz = rot(ipos.yz, ia);
ind = floor(1.5*ceil(.1*ipos.x) + 5.*ceil(.1*ipos.y) + 5.*ceil(.1*ipos.z));
return distanceFunction(mod(pos, 10.) - 5., size);
}
float3 normal(float3 p, float size) { //法線の取得
float2 e = float2(.01, 0);
// 四面体アプローチでの法線推定
// refer iq's article
return normalize(.000001 + map(p, size) - float3(map(p - e.xyy, size), map(p - e.yxy, size), map(p - e.yyx, size)));
}
float SSS(float3 ray, float3 rd, float d,float size) { return clamp(distanceFunction(ray + rd * d, size)*3., .0, 1.); }
fixed4 frag (v2f_img i) : SV_Target
{
float ht = .5*_Time.y;
//正規化
float2 uv = (i.uv.xy*2.0 - 1.);
float size = clamp(1.25+sin(_Time.y*2.), 1., 2.); //オブジェクトのサイズ
float3 CameraPos = float3(UNITY_PI + 7.*sin(_Time.y), UNITY_PI + 36.*sin(ht), -UNITY_PI + 24.*sin(ht)); //カメラのポジション
float3 ta = (.0).xxx;
CameraPos.xz = rot(CameraPos.xz, _Time.y);
ta.xz = rot(ta.xz, _Time.y);
float3 fwd = normalize(ta - CameraPos)
, up = float3(.0, 1., .0)
, side = normalize(cross(fwd, up));
up = normalize(cross(side, fwd));
float screenZ = UNITY_PI; //スクリーンポジション
float3 RayDir = normalize(uv.x*side + uv.y*up + fwd * screenZ); //レイの方向
float3 lightDir; //ライトの位置
float depth = 0.0; //レイの進んだ距離
float dist = 0.0;
float3 rayPos = (0.0).xxx; //レイの位置
float3 col = (0.0).xxx; //色
int j = 1;
for (int i = 0; i < 99; i++) {
rayPos = CameraPos + (RayDir * depth); //レイの位置を計算
dist = map(rayPos, size); //一番近いオブジェクトまでの距離を測る
if (dist < 0.0001) break; //オブジェクトに例がぶつかったらfor文を抜ける
depth += dist; //レイの進んだ距離の更新
j = i + 1;
}
lightDir = normalize(-rayPos + CameraPos);
// インデックスをhueに与えてhsvで着色
float indh = ind * .15;
col = hsv(indh, .8, .8);
float diff=.0;
if (dist < 0.0001) {
float3 n = normal(rayPos, 1.0); //法線の取得
diff = max(dot(n, lightDir), .01); //法線と光の内積取得
}
col *= diff; //色を混ぜる
col += float(j) / 64.; // fog
col += SSS(rayPos, RayDir, .05, size)*.005 + SSS(rayPos, RayDir, .025, size)*.0025; // SSS
return fixed4(col, 1.0);
}
ENDCG
}
}
}