こんにちは、インタラクティブコミュニケーショングループ(ICT3)の井上です。前回はC#スクリプトとVFX Graphを使い、「ストレンジアトラクタ」の点群描画を検証しました。
今回はそのステップアップとして、GPU(シェーダー)の計算だけで立体や空間を描き出す 「レイマーチング(Raymarching)」 を検証しました。
距離関数を操り、複雑な数理表現をUnityで描く
レイマーチングについて
レイマーチングを簡単に言うと、「カメラから画面に向けてビーム(レイ:光線)を飛ばし、空間に配置した物体にぶつかったかを判定して描画する仕組み」 です。検証を終えた今でも理屈を完璧に説明するのは難しいのですが(笑)、「正しい理屈を100%理解していなくても、動くものが作れてしまう」のが面白いところです。
数式だけでサイケデリックなアニメーションを作る「Shader芸」でよく使われる一方、ボリュームレンダリング(雲や煙の密度表現)や、水滴・スライムのような「メタボール」など、実案件にも応用できます。
レイマーチングをHLSLで実装する
インターネット上のレイマーチング解説やサンプルコードの多くは、Webブラウザ用の 「GLSL」 で書かれています。Unityで使うには、「HLSL」 に書き直す必要があります。
画面に「円」を1つ描画するシンプルなシェーダーを例に比較します。
GLSLでの記述例
precision mediump float;
uniform vec2 resolution;
float circle(vec2 p, float r) {
return length(p) < r ? 1.0 : 0.0;
}
void main() {
vec2 position = (gl_FragCoord.xy * 2.0 - resolution.xy) / min(resolution.x, resolution.y);
vec3 color = vec3(circle(position, 0.5));
gl_FragColor = vec4(color, 1.0);
}
HLSL(Unity用)での記述例
Shader "_ShaderGei/01_Circle" {
Properties {}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 100
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
// 円の距離関数
float circle(float2 p, float r) {
return (length(p) < r ? 1 : 0);
}
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
// フラグメントシェーダー(この中を編集します)
fixed4 frag (v2f i) : SV_Target {
// UV座標の中心を (0, 0) にずらす
float2 position = float2(i.uv.x - 0.5, i.uv.y - 0.5);
float c = circle(position, 0.5);
fixed4 color = float4(c, c, c, 1);
return color;
}
ENDCG
}
}
}
Unityでは、頂点シェーダー(`vert`)は変更せず、フラグメントシェーダー(`frag`)を書き換えます。UV座標を `i.uv.x – 0.5` のようにして、画面の中心を原点 `(0, 0)` に合わせるのがポイントです。
成果物の紹介
今回の3Dシーンは、主に以下の3つで構成されています。
- エネルギー球:中央で開いたり閉じたりする複雑な球体
- 天井・床:六角形(ヘキサゴン)のタイルが敷き詰められた平面
- 浮遊するキューブ(パーティクル):空間を上下に移動する多数の立方体の枠線



さらにカメラやオブジェクトを回転・移動させ、SF映画のような空間にしています。
今回作成した成果物の完成シーン
距離関数(SDF)の仕組み
レイマーチングで物体を描くときには、「距離関数(SDF:Signed Distance Function)」 という数式を使います。「レイの先端の座標」と「物体のパラメータ(サイズなど)」から「物体からレイの先端までの最短距離」を返す関数で、立体の配置位置はレイの座標をスライドさせるだけで調整できます(「レイの座標=立体の中心位置」とイメージすると分かりやすいでしょう)。
距離関数の大きなメリットは、数式を少し調整するだけで物体の「複製(繰り返し配置)」や「加算・減算(足し引き)」ができることです。CSG表現(組み合わせ・削り取り)は距離関数同士を「比較」して行います。まずは加算・減算から見ていきます。
1. 基本的な足し算と掛け算(min と max)
丸みを帯びた箱(ラウンドボックス)と、ドーナツ型(トーラス)の距離関数を例に見てみます。
- 加算(論理和:min)
2つの距離関数のうち小さい方(近い方)を取ることで、両方を描画できます。複数の物体もminで足していきます。
float sdMin(float3 p, float3 b, float r, float2 t){
return min(sdRoundBox(p, b, r), sdTorus(p, t));
}
3つ以上なら、以下のように min を重ねます。
return min(sdA, min(sdB, min(sdC, sdD)));
- 重なり部分のみ(論理積:max)
大きい方の値を取ることで、「お互いが重なっている部分だけ」を描画します。
float sdMax(float3 p, float3 b, float r, float2 t){
return max(sdRoundBox(p, b, r), sdTorus(p, t));
}
2. くり抜き(減算)
- 一方からもう一方を削る
距離関数にマイナス(-)をつけると「その物体が存在しない範囲」を表せます。これと max を組み合わせると、立体をくり抜けます。
float sdMinusMax(float3 p, float3 b, float r, float2 t){
return max(sdRoundBox(p, b, r), -sdTorus(p, t));
}
「ボックスがある場所、かつトーラスが無い場所」となるため、ボックスからトーラスの形がくり抜かれます。ベン図を思い浮かべると分かりやすいでしょう。
逆に、ボックス側にマイナスをつければ結果も逆転し、「ボックスが無いところ、かつトーラスがあるところ」を表せます。
float sdMinusMax2(float3 p, float3 b, float r, float2 t){
return max(-sdRoundBox(p, b, r), sdTorus(p, t));
}
- 結合部分を滑らかにつなぐ(smoothMin)
単純に min でくっつけると角がパキッと割れますが、smoothMin を使うと、粘土やスライムのように滑らかにつなげられます。
float smoothMin(float d1, float d2, float k){
float h = exp(-k * d1) + exp(-k * d2);
return -log(h) / k;
}
3. 応用:エネルギー球の作り方
中央のエネルギー球は、これらの加算・減算を組み合わせて作っています。
- まず、シンプルな球体(
sdSphere)と、3つの直方体を十字に組み合わせた立体(sd3dBox)を用意します。 - 球体の距離関数と、直方体の距離関数(マイナス付き)の論理積を取ります。
max(sdSphere, -sd3dBox)
これで球体の中心部が3方向(十字)からくり抜かれます。 - くり抜いた空洞部分に、一回り小さい球体を論理和(
min)で重ねます。 - 3つの直方体の厚みや外側の球の半径に、Unityの時間経過パラメータ
abs(sin(_Time.y))を掛け、外側の球殻がパクパクと開閉するアニメーションにしています。
距離関数の複製(繰り返し配置)について
レイマーチングの強力な武器が、無限に物体を配置する「複製」処理です。剰余(あまり)を計算する mod 関数を使います。
1. 球体を無限に並べる
通常なら1万個の球に1万回の計算が必要ですが、レイマーチングでは座標を mod で周期的にループさせるだけで、1つの計算式から無限に並べられます。
float sdRepeatSphere(float3 p, float r){
// 座標 p を 4 の周期でループさせ、範囲を -2 〜 2 に調整する
return sdSphere(mod(p, 4) - 2, r);
}
- HLSL(Unity)実装時の落とし穴
GLSLには mod 関数がありますが、UnityのHLSLにある fmod は、マイナスの値が入ったときの挙動がGLSLと異なるという罠があります。そのため、HLSLでは以下の自作 mod 関数を定義して使うのが無難です。
float mod(float x, float y){
return x - y * floor(x / y);
}
この複製球とエネルギー球の「くり抜き(負の論理積)」を組み合わせ、外側にぶつぶつとしたドットパターンの穴を繰り返し開けています。
2. 天井と床の六角形(ヘキサゴン)タイルの敷き詰め
天井と床の六角形も同じ仕組みです。上下に無限平面(Plane)を配置し、六角柱(Hexagonal Prism)を mod で無限複製して論理積でくり抜きますが、単純に並べるだけでは隅に隙間ができ、ハニカム状に綺麗に敷き詰まりません。そこで mod の引数を調整し、①基本の六角形、②x座標を半分・z座標を1個分ずらした六角形など、位置違いの4つの六角形を定義して論理和(min)を取ることで、隙間なく敷き詰めました。
パーティクル(浮遊するキューブ)について
空間をふわふわと浮かぶ無数のキューブは、基本的な立体描画の後に追加した応用表現です。実装では、さやちゃんぐbot氏のスライド資料 『球を出したあとのレイマーチング入門』 を参考にし、ほぼ解説通りに実装しています。シリンダー(円柱)の距離関数を、立方体のフレーム(枠線)の距離関数(sdBoxFrame)に置き換えることで、「浮遊するキューブ」を表現しています。
色の付け方(ライティングとエフェクト)について
形状ができたら、ピクセルカラーを決めます。今回実践した5つの色付けテクニックを紹介します。
1. 最もシンプルな単色パターン
レイが物体に衝突した(距離が極めて0に近くなった)ピクセルに直接色を割り当て、影などのないフラットな単色にします。
2. 影と立体感を出すパターン(陰影・法線ライティング)
空間に「光源の位置」を定義し、衝突地点の「傾き(法線:Normal)」から光の当たり具合に応じた明暗をつけます。どんな形状でも、法線を求める計算式(mainNormal)は共通コードで取得できます。
float3 mainNormal(float3 p) {
float v = 0.001; // 微小な差分
return normalize(float3(
centerSphereSDF(p) - centerSphereSDF(float3(p.x - v, p.y, p.z)),
centerSphereSDF(p) - centerSphereSDF(float3(p.x, p.y - v, p.z)),
centerSphereSDF(p) - centerSphereSDF(float3(p.x, p.y, p.z - v))
));
}
この法線と光源ベクトルの内積(dot)で、3Dの立体感を出せます。
3. 奥行きを出すパターン(グラデーション・フォグ)
奥に進むほど暗くなるよう、レイが衝突するまでの総距離(total)で色を計算します。
color = exp(-0.1 * total) * fixed4(0, 0, 1, 1);
指数関数(exp)の特性により、総距離が大きいほど値が小さくなり、奥が自然に暗く溶け込む「霧(フォグ)」の効果が得られます。巨大な空間や無限配置に有効です。
4. 周りをぼんやり光らせる発光パターン(グロー効果)
物体そのものではなく、「周りの空間」を光らせる手法です。レイと物体の距離(distance)が「0に近いほど大きな値になる式」をループごとに足し込みます。
emission += exp(abs(distance) * -0.2);
これにより輪郭付近に強い光が乗り、ネオンのようなグローエフェクトを表現できます。
5. 複数の物体を塗り分けるパターン
球体とボックスを同じ画面に配置すると、そのままでは一色で塗られてしまいます。そこで、それぞれの距離関数の値(d1:球、d2:ボックス)を比較し、
if (d1 < d2)なら球の色if (d1 > d2)ならボックスの色
と条件分岐させることで、物体ごとに色を分けられます。3つ以上の場合も、残った最小値と次の距離関数を比較して判定します。
考察と今後の実務への展望
今回のレイマーチング開発を通じ、いくつかの気づきがありました。
- 「見た目の良さ」と「開発スピード」のバランス
ビジュアル表現の領域に「絶対の正解」はありません。開発案件として取り入れる場合、限られた時間で数式を組み合わせ、「格好いい表現」を効率的に作ることが重要だと感じました。 - GPU処理負荷の課題
検証中は最適化不足もあり、GPU負荷が想像以上に高くなりました。実案件では、体験デバイス(PCやスマートデバイス)のマシンスペックを慎重に選定・注視する必要があります。
ためになる・お世話になったリンク集
最後に、今回一からレイマーチングを学ぶ際に特に助けられたリンクを紹介します。上から順に学ぶのがおすすめです。
入門用・学習用リンク
- wgld.org | GLSL: レイマーチングで球体を描く
WebGLの定番サイト。基本的な図形の描画、複製、加算・減算を基礎から学べます。 - Unity板ポリレイマーチング入門 | さやちゃんぐbotブログ
Unityの板ポリ(Quad)でレイマーチングのコードを書き、色を付けるところまで解説しています。 - 球を出したあとのレイマーチング入門 | Googleスライド(さやちゃんぐbot氏)
基本的な球を描いた後、画面を派手にするテクニック(パーティクルや配置の工夫)がまとまっています。 - シェーダだけで世界を創る!three.jsによるレイマーチング | Speaker Deck(がむ氏)
複雑なオブジェクトや背景が数式(SDF)の組み合わせで構成されるイメージを視覚的に掴めます。
数学的に役立つリンク
- Inigo Quilez :: computer graphics, maths, shaders, fractals
レイマーチングの創始者の一人である著者のサイト。さまざまな立体のSDFが網羅され、他の解説サイトのリンク元にもなっています。 - Graphtoy
自作の数式を簡単にグラフ化できるサイト。アニメーションやグラデーションの数値変化(イージング)の調整に便利です。 - 関数図書館 | Graphary
イージングの挙動や数値変化を調整したい時に使える数学的な関数がまとまっています。
今回の検証で、シェーダーだけで空間を構築するレイマーチングのポテンシャルを体験できました。このR&D成果を、今後のインタラクティブコンテンツや3Dグラフィックス表現の提案に活かしていきます。





