Butadiene Works

ジンジャエールが飲みたいです@twitter:butadiene121

UnityでShaderを使ってカラフルなメタボール作ってみる。

はじめに

みなさんこんにちは。ブタジエンです。

今回レイマーチングの勉強としてUnityでShaderを使ってメタボールを作ってみました。

これはおもにShaderで実装されています。結構見てて楽しい表現なのかな・・・?

レイマーチングの勉強として作ったのですが、いろいろな基礎の技術要素が入った良い感じのShaderになったので自分の備忘録もかねて解説記事を書いてみました。

また、ある程度複雑なレイマーチング(ライティングが入っていたり、(一つか二つの式で表せるような)単純な図形ではない)のサンプルになるとも思います。

触れる内容はメタボールの(自分なりの)作り方、色の付け方、四面体ベースの法線、レイマーチングでの影、フォン鏡面反射、enhanced sphere tracing 、オブジェクトスペースでのレイマーチング、そしてデプスの書き込みになります。

【注意】この記事は専門家でない私が勉強したものをまとめたものです。間違いなどがあると思いますがその場合はブタジエン (@butadiene121) | Twitterまで指摘等頂けると幸いです。

今回のサンプルプロジェクトはGitHubにあげてあります。
github.com
プロジェクトの環境はUnity2018.2.10.f1ですが、Shaderしか入ってないのでまあだいたいどのバージョンでも動くと思います。

想定している読者層

Unityでレイマーチングを触ったことがある人、を想定しています。

サンプルについて

上に貼ったプロジェクト開けてmetaballsampleフォルダの中の"sample scene"を開ければわかると思います。

コード本体

以下が今回書いたShaderのコードになります。

github.com

// The MIT License
// Copyright © 2019 Butadiene
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// Use directional light
Shader "Butadiene/metaball"
{
	Properties
	{
		_ypos("floor height",float)=-0.25
		}
	SubShader
	{
		Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }
		LOD 100
		Cull Front
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			uniform float _ypos ;
			#include "UnityCG.cginc"
			// The MIT License
			// Copyright © 2013 Inigo Quilez
			// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
			//Making noise
			float hash(float2 p)  
			{
				p  = 50.0*frac( p*0.3183099 + float2(0.71,0.113));
				return -1.0+2.0*frac( p.x*p.y*(p.x+p.y) );
			}

			float noise( in float2 p )
			{
				float2 i = floor( p );
				float2 f = frac( p );
	
				float2 u = f*f*(3.0-2.0*f);

				return lerp( lerp( hash( i + float2(0.0,0.0) ), 
								 hash( i + float2(1.0,0.0) ), u.x),
							lerp( hash( i + float2(0.0,1.0) ), 
								 hash( i + float2(1.0,1.0) ), u.x), u.y);
			}			
			///////////////////////////////////////////////////////////////////////
											
			float smoothMin(float d1,float d2,float k)
			{
				return -log(exp(-k*d1)+exp(-k*d2))/k;
			}
						
			// Base distance function
			float ball(float3 p,float s)
			{
				return length(p)-s;
			}

			
			// Making ball status
			float4 metaballvalue(float i)
			{
				float kt = 3*_Time.y*(0.1+0.01*i);
				float3 ballpos = 0.3*float3(noise(float2(i,i)+kt),
					noise(float2(i+10,i*20)+kt),noise(float2(i*20,i+20)+kt));
				float scale = 0.05+0.02*hash(float2(i,i));
				return  float4(ballpos,scale);
			}
			// Making ball distance function
			float metaballone(float3 p, float i)
			{	
				float4 value = metaballvalue(i);
				float3 ballpos = p-value.xyz;
				float scale =value.w;
				return  ball(ballpos,scale);
			}

			//Making metaballs distance function
			float metaball(float3 p)
			{
				float d1;
				float d2 =  metaballone(p,0);
				for (int i = 1; i < 6; ++i) {
				
					d1 = metaballone(p,i);
					d1 = smoothMin(d1,d2,20);
					d2 =d1;
					}
				return d1;
			}
		
			// Making distance function
			float dist(float3 p)
			{	
				float y = p.y;
				float d1 =metaball(p);
				float d2 = y-(_ypos); //For floor
				d1 = smoothMin(d1,d2,20);
				return d1;
			}


			//enhanced sphere tracing  http://erleuchtet.org/~cupe/permanent/enhanced_sphere_tracing.pdf

			float raymarch (float3 ro,float3 rd)
			{
				float previousradius = 0.0;
				float maxdistance = 3;
				float outside = dist(ro) < 0 ? -1 : +1;
				float pixelradius = 0.02;
				float omega = 1.2;
				float t =0.0001;
				float step = 0;
				float minpixelt =999999999;
				float mint = 0;
				float hit = 0.01;
					for (int i = 0; i < 60; ++i) {

						float radius = outside*dist(ro+rd*t);
						bool fail = omega>1 &&
							step>(abs(radius)+abs(previousradius));
						if(fail){
							step -= step *omega;
							omega =1.0;
						}
						else{
							step = omega * radius;
						}
						previousradius = radius;
						float pixelt = radius/t;
						if(!fail&&pixelt<minpixelt){
							minpixelt = pixelt;
							mint = t;
						}
						if(!fail&&pixelt<pixelradius||t>maxdistance)
						break;
						t += step;
					}
				
					if ((t > maxdistance || minpixelt > pixelradius)&&(mint>hit)){
					return -1;
					}
					else{
					return mint;
					}
				
			}

			// The MIT License
			// Copyright © 2013 Inigo Quilez
			// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
			// https://www.shadertoy.com/view/Xds3zN

			//Tetrahedron technique  http://iquilezles.org/www/articles/normalsSDF/normalsSDF.htm
			float3 getnormal( in float3 p)
			{
				static const float2 e = float2(0.5773,-0.5773)*0.0001;
				float3 nor = normalize( e.xyy*dist(p+e.xyy) +e.yyx*dist(p+e.yyx) 
					+ e.yxy*dist(p+e.yxy ) + e.xxx*dist(p+e.xxx));
				nor = normalize(float3(nor));
				return nor ;
			}
			////////////////////////////////////////////////////////////////////////////

			// Making shadow
			float softray( float3 ro, float3 rd , float hn)
			{
				float t = 0.000001;
				float jt = 0.0;
				float res = 1;
				for (int i = 0; i < 20; ++i) {
					jt = dist(ro+rd*t);
					res = min(res,jt*hn/t);
					t = t+ clamp(0.02,2,jt);
				}
				return saturate(res);
			}
			
			// The MIT License
			// Copyright © 2013 Inigo Quilez
			// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
			// https://www.shadertoy.com/view/ld2GRz

			float4 material(float3 pos)
			{
				float4 ballcol[6]={float4(0.5,0,0,1),
								float4(0.0,0.5,0,1),
								float4(0,0,0.5,1),
								float4(0.25,0.25,0,1),
								float4(0.25,0,0.25,1),
								float4(0.0,0.25,0.25,1)};
				float3 mate = float3(0,0,0);
				float w = 0.01;
					// Making ball color
					for (int i = 0; i < 6; ++i) {
						float x = clamp( (length( metaballvalue(i).xyz - pos )
								-metaballvalue(i).w)*10,0,1 ); 
						float p = 1.0 - x*x*(3.0-2.0*x);
						mate += p*float3(ballcol[i].xyz);
						w += p;
					}
				// Making floor color
				float x = clamp(  (pos.y-_ypos)*10,0,1 );
				float p = 1.0 - x*x*(3.0-2.0*x);
				mate += p*float3(0.2,0.2,0.2);
				w += p;
				mate /= w;
				return float4(mate,1);
			}
			////////////////////////////////////////////////////
			
			//Phong reflection model ,Directional light
			float4 lighting(float3 pos)
			{	
				float3 mpos =pos;
				float3 normal =getnormal(mpos);
				
				pos =  mul(unity_ObjectToWorld,float4(pos,1)).xyz;
				normal =  normalize(mul(unity_ObjectToWorld,float4(normal,0)).xyz);
					
				float3 viewdir = normalize(pos-_WorldSpaceCameraPos);
				half3 lightdir = normalize(UnityWorldSpaceLightDir(pos));				
				float sha = softray(mpos,lightdir,3.3);
				float4 Color = material(mpos);
				
				float NdotL = max(0,dot(normal,lightdir));
				float3 R = -normalize(reflect(lightdir,normal));
				float3 spec =pow(max(dot(R,-viewdir),0),10);

				float4 col =  sha*(Color* NdotL+float4(spec,0));
				return col;
			}

			
		
				struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float2 uv : TEXCOORD0;
				float3 pos : TEXCOORD1;
				float4 vertex : SV_POSITION;
			};

			
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.pos= mul(unity_ObjectToWorld,v.vertex).xyz;
				o.uv = v.uv;
				UNITY_TRANSFER_FOG(o,o.vertex);
				return o;
			}
			
			struct pout
			{
				float4 pixel: SV_Target;
				float depth : SV_Depth;

			};

			pout frag (v2f i) 
			{
				float3 ro = mul( unity_WorldToObject,float4(_WorldSpaceCameraPos,1)).xyz;
				float3 rd = normalize(mul( unity_WorldToObject,float4(i.pos,1)).xyz
					-mul( unity_WorldToObject,float4(_WorldSpaceCameraPos,1)).xyz); 
				float t = raymarch(ro,rd);
				fixed4 col=0;

				if (t==-1) {
				clip(-1);
				}
				else{
				float3 pos = ro+rd*t;
				col = lighting(pos);
				}
				pout o;
				o.pixel =col;
				float4 curp = UnityObjectToClipPos(float4(ro+rd*t,1));
				o.depth = (curp.z)/(curp.w); //Drawing depth

				return o;
				
			}
			ENDCG
		}
		
	}
}

少し長いですが簡単に解説していきます。

コード解説

コードの構造

コードの流れとしては

  1. ライセンスやプロパティなどを記述
  2. ノイズ関数を記述(hash、noise)
  3. 球や床の接続などに使う関数を記述(smoothMin)
  4. 球の距離関数を記述(ball)
  5. ボールのステータスを作る関数を記述(metaballvalue)
  6. 球の距離関数とボールのステータスを受けてボールの距離関数を記述(metaballone)
  7. 上を受けてボール群の距離関数を記述(metaball)
  8. 床とボール群の距離関数を組み合わせて、最終的な距離関数を記述(dist)
  9. レイマーチングを行う関数の記述(raymarch)
  10. 法線を求める関数の記述(getnormal)
  11. 影を付ける関数の記述(softray)
  12. 球と床のマテリアルの色を決める関数の記述(material)
  13. ライティングをする関数の記述(lighting)
  14. シェーダ本体の記述(vert、frag)

という感じの構成になっています

コードに載せた注意書き

// Use directional light

ディレクショナルライトを使えということです。
僕にはまだ多様な光源に対応する能力がありませんのでとりあえずディレクショナルライト想定で作ってみました。

プロパティ

Properties
	{
		_ypos("floor height",float)=-0.25

		}

ここで床の高さを指定できるようにしています。

ノイズ関数

//Making noise
float hash(float2 p)  
{
	p  = 50.0*frac( p*0.3183099 + float2(0.71,0.113));
	return -1.0+2.0*frac( p.x*p.y*(p.x+p.y) );
}

float noise( in float2 p )
{
	float2 i = floor( p );
	float2 f = frac( p );
	
	float2 u = f*f*(3.0-2.0*f);

	return lerp( lerp( hash( i + float2(0.0,0.0) ), 
					 hash( i + float2(1.0,0.0) ), u.x),
				lerp( hash( i + float2(0.0,1.0) ), 
					 hash( i + float2(1.0,1.0) ), u.x), u.y);
}			

https://www.shadertoy.com/view/lsf3WH よりiqさんのノイズを使わせてもらっています シンプルなノイズです。今回はボールの移動に使わせてもらいました。


球や床の前後判定や接続

float smoothMin(float d1,float d2,float k)
{
	return -log(exp(-k*d1)+exp(-k*d2))/k;
}

ボールとボール・ボールと床が溶けて液体のようにつながるのに必要な関数です。 https://wgld.org/d/glsl/g016.html が詳しいと思います。

球の距離関数

// Base distance function
float ball(float3 p,float s)
{
	return length(p)-s;
}

ベースとなる球の距離関数を作成しています。参考:https://wgld.org/d/glsl/g009.html


ボールの位置、半径の決定とiとの紐づけ

// Making ball status
float4 metaballvalue(float i)
{
	float kt = 3*_Time.y*(0.1+0.01*i);
	float3 ballpos = 0.3*float3(noise(float2(i,i)+kt),
		noise(float2(i+10,i*20)+kt),noise(float2(i*20,i+20)+kt));
	float scale = 0.05+0.02*hash(float2(i,i));
	return  float4(ballpos,scale);
}

ボールのステートを作成する関数です。具体的にはfloat4(ボールの座標、ボールの大きさ)という形で出力します。
メタボールということで複数のボールを扱う必要があるわけですが、一つ一つのボールを数値iで区別して、(距離関数や色付けのところで)for文で全部まとめて出力してやろうという魂胆になっています。
ボールの位置や大きさはiを引数としてノイズや乱数で出力しています。

ボールのステータスを反映した距離関数

// Making ball distance function
float metaballone(float3 p, float i)
{	
	float4 value = metaballvalue(i);
	float3 ballpos = p-value.xyz;
	float scale =value.w;
	return  ball(ballpos,scale);
}

各ボールとの距離関数です。入力されたiをもとにしてボールのステータスを作成し、そこから距離を求めます。(日本語力が虚無になっていく・・)(そもそもこういうコードなんて上から読むものじゃない)

メタボール(ボール群)の距離関数

//Making metaballs distance function
float metaball(float3 p)
{
	float d1;
	float d2 =  metaballone(p,0);
	for (int i = 1; i < 6; ++i) {
				
		d1 = metaballone(p,i);
		d1 = smoothMin(d1,d2,20);
		d2 =d1;
		}
	return d1;
}

メタボール(ボール群)の距離関数です。ボールとの距離d1を求めてそれを1ループ前に計算したボールの距離d2とsmoothMinで比較するということをしてます。これによって6つのボール、そしてそれらが一部つながった状態のオブジェクトとの距離が出力されます。

最終的な距離関数

// Making distance function
float dist(float3 p)
{	
	float y = p.y;
	float d1 =metaball(p);
	float d2 = y-(_ypos); //For floor
	d1 = smoothMin(d1,d2,20);
	return d1;
}

最終的な距離関数です。メタボールまでの距離d1と床までの距離d2をsmoothMinで比較しています。これによって6つのボール群と床、そしてそれらが一部つながった状態のオブジェクトとの距離が出力されます。
なお、床の距離関数は

float d2 = y-(_ypos); //For floor

です。

レイマーチングを行う

求めた距離関数を使ってレイマーチングを実際に行う関数です
enhanced sphere tracing という論文を参考にしています。http://erleuchtet.org/~cupe/permanent/enhanced_sphere_tracing.pdf
簡単に言えば、レイを大きくすすめて通り過ぎてしまったときに少し戻る、というシステムによってレイを遠くへ届かせる技術と(前半部)、レイがオブジェクトすれすれを通ることによりループを消費してしまい衝突すべき点に衝突する前にループが終わって衝突していない判定になってしまう事象への対策のために、(スクリーンから見て)最もオブジェクトに近かった点を衝突点の候補とする、という技術(後半部)です。
※今回の場合、enhanced sphere tracingを使う必要性はそこまでないのですが(多分)、練習として組み込んでいます。

float raymarch (float3 ro,float3 rd)
{
	float previousradius = 0.0;
	float maxdistance = 3;
	float outside = dist(ro) < 0 ? -1 : +1;//レイのスタート地点が物体の中にあるか外にあるか
	float pixelradius = 0.02;
	float omega = 1.2;
	float t =0.0001;
	float step = 0;
	float minpixelt =999999999;
	float mint = 0;
	float hit = 0.01;
		for (int i = 0; i < 60; ++i) {

			float radius = outside*dist(ro+rd*t);
			bool fail = omega>1 &&
				step>(abs(radius)+abs(previousradius));
			if(fail){
				step -= step *omega;
				omega =1.0;
			}
			else{
				step = omega * radius;
			}
			previousradius = radius;
			float pixelt = radius/t;
			if(!fail&&pixelt<minpixelt){
				minpixelt = pixelt;
				mint = t;
			}
			if(!fail&&pixelt<pixelradius||t>maxdistance)
			break;
			t += step;
		}
				
		if ((t > maxdistance || minpixelt > pixelradius)&&(mint>hit)){
		return -1;
		}
		else{
		return mint;
		}
				
}

前半部のロジックについて(レイを大きくすすめて通り過ぎてしまったときに少し戻る というやつ)
最初のほうはomega=1.2がかかることによってレイが早く進むようになってます。

failの判定について。stepはここでは1ループ前に進んだ距離になっています。このstepの値が今進もうとしている距離radiusと1ループ前に進もうとした距離previousradius(omegaがかかっていないためstepと異なる値であることに注意)の和より大きければ進みすぎなので、omegaを1.0にして普通に進むこととします。なお、一度1.0が代入されるとomega> 1に常時はじかれるので以後はomega = 1.0のただのレイマーチングになります。

後半部のロジックについて((スクリーンから見て)最もオブジェクトに近かった点を衝突点の候補とするというやつ)
pixeltで求められてるのは、スクリーンに対するレイの進む半径の大きさです。minpixelt にはpixeltの最小値が、mintにはpixelt が一番小さい時のtの値が記録されます(上から二つ目のif文)。

ループの終了に関しては最大距離に加えて、「スクリーンに対するレイの進む半径の大きさ(すなわちpixelt)」がピクセルの大きさより小さいのならばループを終わってもよい。というのが加わっているのがわかると思います(上から3つ目のif文)。

ループを抜けた後はレイが衝突しているか否かの判定をするわけですが「スクリーンに対するレイの進む半径の大きさ(pixelt)」の最小値がピクセルの大きさより大きく、かつpixelt が一番小さい時のtの値mintが一定の値(ここではhit)より大きいときは当たっていない判定とします。当たっている判定とされたのなら、「スクリーンに対するレイの進む半径の大きさ(pixelt)」が一番小さかった時のt(すなわちpixelt が一番小さい時のtの値mint)を距離とします。

まあぶっちゃけ上で示した論文読むほうがいいと思います、はい。(私もあまり自信がありません・・・)
あと、この関数は最終的に視点からの距離を出力するわけですが、当たらなかったときは-1を返すようにしています。

四面体ベースで法線を求める

float3 getnormal( in float3 p)
{
	static const float2 e = float2(0.5773,-0.5773)*0.0001;
	float3 nor = normalize( e.xyy*dist(p+e.xyy) +
 		e.yyx*dist(p+e.yyx) + e.yxy*dist(p+e.yxy ) + e.xxx*dist(p+e.xxx));
	nor = normalize(float3(nor));
	return nor ;
}

法線を四面体ベースで求めることで距離関数を呼び出す回数を少なくしています。(普通にxyzで法線を求めると6回距離関数を呼び出すことになるがこれは4回) iqさんのコードを使わせてもらっています。
参照先はここです。http://iquilezles.org/www/articles/normalsSDF/normalsSDF.htm


影をつける

以下の関数では影を作ります。衝突地点からレイを光源の方向に飛ばし、なにかと当たった判定が出れば暗くします(つまりroにはレイの到達地点、rdには光の方向を入れます)。hnおよびその周りは影の周囲をぼかすための値です。(すれすれを通ってるとresに1と0の間の数値が出力される)

// Making shadow
float softray( float3 ro, float3 rd , float hn)
{
	float t = 0.000001;
	float jt = 0.0;
	float res = 1;
	for (int i = 0; i < 20; ++i) {
		jt = dist(ro+rd*t);
		res = min(res,jt*hn/t);
		t = t+ clamp(0.02,2,jt);
	}
	return saturate(res);
}

 

球と床のマテリアルの色の決定

色を作ります
https://www.shadertoy.com/view/ld2GRzを参考にしてボールの色を作っています。ただし、そのままでは動かなかったし、原理もわからないところがあったので少しいじっています。
(具体的には236行目の float x = clamp( length( blobs[i].xyz - pos )/blobs[i].w, 0.0, 1.0 ); が謎 なぜそこで割るのだ・・・。(多分想定しているblobsが少し違う気がする))

※この話なんですがphi16さんから大きい球はグラデーションに対する影響力がでかいように計算しているのでは、との話を伺いました。なるほど。

float4 material(float3 pos)
{
	float4 ballcol[6]={float4(0.5,0,0,1),
					float4(0.0,0.5,0,1),
					float4(0,0,0.5,1),
					float4(0.25,0.25,0,1),
					float4(0.25,0,0.25,1),
					float4(0.0,0.25,0.25,1)};
	float3 mate = float3(0,0,0);
	float w = 0.01;
		// Making ball color
		for (int i = 0; i < 6; ++i) {
			float x = clamp( (length( metaballvalue(i).xyz - pos )
					-metaballvalue(i).w)*10,0,1 ); 
			float p = 1.0 - x*x*(3.0-2.0*x);
			mate += p*float3(ballcol[i].xyz);
			w += p;
		}
	// Making floor color
	float x = clamp(  (pos.y-_ypos)*10,0,1 );
	float p = 1.0 - x*x*(3.0-2.0*x);
	mate += p*float3(0.2,0.2,0.2);
	w += p;
	mate /= w;
	return float4(mate,1);
}

一番最初にballcol[6]に各ボールの色を叩き込みます。
意味が分からんコードを自分なりに改編したコードがfor文の中身です。レイの最終地点が「ボールより0.1外側」より内側だった場合、色を描画するようにしています(だんだん濃くなってきてボールの表面より内側では完全にボールの色を使用)。xはこのままでは球の表面からその外側まで線形に色が落ちていく感じとなってしまうのでpにしていい感じの変化になるようにしています。mate += p*float3(ballcol[i].xyz);のところで全ボールの色を足していきます(重なってるところは色が足され、そうでないところはその場所にあるボールの色のみが表示される)。wでどれぐらい足したかを計測し、最後に割ることで、重なってる部分が明るくなるのではなくブレンドされるようになります。for文から抜けたら同じ要領で床の色も作ってしまいます。

ライティング

ライティングです。影と色を呼び出してきて、フォン鏡面反射と合わせて最終的な色を決めています フォン鏡面反射に関してはここが詳しいです。
http://nn-hokuson.hatenablog.com/entry/2016/11/04/104242
なお、レイマーチングには後述の通り、オブジェクトスペースを採用しているのですが、ライティングの計算のときはなにかとワールドスペースのほうが都合がいいのでその変換が最初に入ってます。

//Phong reflection model ,Directional light
float4 lighting(float3 pos)
{	
	float3 mpos =pos;
	float3 normal =getnormal(mpos);
	
	pos =  mul(unity_ObjectToWorld,float4(pos,1)).xyz;
	normal =  normalize(mul(unity_ObjectToWorld,float4(normal,0)).xyz);
					
	float3 viewdir = normalize(pos-_WorldSpaceCameraPos);
	half3 lightdir = normalize(UnityWorldSpaceLightDir(pos));	
	float sha = softray(mpos,lightdir,3.3);
	float4 Color = material(mpos);
				
	float NdotL = max(0,dot(normal,lightdir));
	float3 R = -normalize(reflect(lightdir,normal));
	float3 spec =pow(max(dot(R,-viewdir),0),10);

	float4 col =  sha*(Color* NdotL+float4(spec,0));
	return col;
}

  

シェーダ本体

出力です レイマーチングにはオブジェクトスペースを採用しています。
このシェーダを適用したオブジェクトのローカル座標において、視点の位置をレイの出発点とし、メッシュの位置と視点の位置を結ぶベクトルをレイの進む方向としています。
レイが衝突していない場合はクリップしています。最後にデプスを書き込んでおしまいです。お疲れ様でした!!

	struct appdata
{
	float4 vertex : POSITION;
	float2 uv : TEXCOORD0;
};

struct v2f
{
	float2 uv : TEXCOORD0;
	float3 pos : TEXCOORD1;
	float4 vertex : SV_POSITION;
};

			
			
v2f vert (appdata v)
{
	v2f o;
	o.vertex = UnityObjectToClipPos(v.vertex);
	o.pos= mul(unity_ObjectToWorld,v.vertex).xyz;
	o.uv = v.uv;
	UNITY_TRANSFER_FOG(o,o.vertex);
	return o;
}
			
struct pout
{
	float4 pixel: SV_Target;
	float depth : SV_Depth;

};

pout frag (v2f i) 
{
	float3 ro = mul( unity_WorldToObject,float4(_WorldSpaceCameraPos,1)).xyz;
	float3 rd = normalize(mul( unity_WorldToObject,float4(i.pos,1)).xyz
			-mul( unity_WorldToObject,float4(_WorldSpaceCameraPos,1)).xyz); 
	float t = raymarch(ro,rd);
	fixed4 col=0;

	if (t==-1) {
	clip(-1);
	}
	else{
	float3 pos = ro+rd*t;
	col = lighting(pos);
	}
	pout o;
	o.pixel =col;
	float4 curp = UnityObjectToClipPos(float4(ro+rd*t,1));
	o.depth = (curp.z)/(curp.w); //Drawing depth

	return o;
				
}		

 

課題

Unityのシーン上に影を「落とす」オブジェクトが存在した場合、うまく対応できません。(まあなんか原理的に無理な気もしなくもないですが・・・ Unityの影まったくわかんない・・・)

終わりに

今回はレイマーチングの勉強としてメタボールを作りました。いろいろな技術が扱えていい練習になったと思います。しかしレイマーチングは重いですね・・・。最適化法をもっと勉強してVRでも気軽にレイマーチングできるようになりたいです。

なにかあればブタジエン (@butadiene121) | Twitterまでご連絡ください。フィードバック等お待ちしております。

補記

  1. ライティング周りに不足していた点があり、バージョンが上がると動かないという指摘を受けましたので訂正をしました(2019/2/23)
  2. floating point division by zero 128というエラーが出ているという指摘を頂いたので修正しました。また、フラグメントシェーダ内でcolに値が代入されていない場合が存在したので修正しました。(2019/2/24)