0. はじめに
Three.js #02 でやった
- 時間で 頂点を上下に揺らす
- 時間で 色を変える
これを Unity(URP) で 自分の手で再現する。
難しい水面はまだやらない
ライティングもいらない
「動いた」という事実を作るのが目的
この回は 「Unityの水・炎・発光シェーダーに進むための足場」。
前回の記事:
[Shader 入門 #02] Three.js で最小のシェーダーを書く:GLSL の“動く”感覚を掴む
Shader 入門 第2回。Three.js の RawShaderMaterial を使い、最小の GLSL シェーダーを自分で書いて動かしてみる。頂点を上下に揺らす sin 波アニメーション、時間経過による色変化など、GPU の挙動を直接感じられるステップ …
https://humanxai.info/posts/shader-intro-02-threejs-minimal-glsl/1. Unityでシェーダーを書く前に知っておくこと
Unityのシェーダーは Three.js より「外側が多い」
Three.js では、だいたいこんな感じだった。
void main() {
gl_Position = ...
}
Unityでは、この中身(計算)を書く前に 「どこで・どう使われるシェーダーか」を宣言する必要がある。
そのために出てくるのが ShaderLab。
2. Unityのシェーダーは3段構え
Unityのシェーダーは、だいたいこの3層でできている。
① ShaderLab(外枠・設定)
- このシェーダーの名前
- URP用かどうか
- どのパスで描画するか
👉 Unity専用の設定言語
② SubShader / Pass(描画ルール)
- Forward描画?
- Unlit?Lit?
- URPに対応している?
👉 レンダリングのルール
③ HLSL(計算の中身)
- 頂点をどう動かすか
- 色をどう計算するか
- 時間
_Timeをどう使うか
👉 ここが GLSL とほぼ同じ役割
3. 今回は「Unlit + 最小構成」だけ使う
初心者が最初にやるべきなのは Unlit。
理由:
- 光源を考えなくていい
- 法線も一旦無視できる
- 「頂点が動く」「色が変わる」が分かりやすい
つまり今回は:
URP対応の Unlit シェーダーを1つ書く
4. まずはシェーダーファイルを作る
手順(そのままやってOK)
- Unity の Assets フォルダを右クリック
- Create > Shader > Unlit Shader
- 名前を UnlitWobbleURP にする
※ 中身は 全部書き換える(怖がらなくていい)
5. 完成形(まずは全文を見る)
まずは 全体像を一気に見る。 意味は次の章で説明する。
Shader "Lain/UnlitWobbleURP"
{
Properties
{
_BaseColor ("Base Color", Color) = (1,1,1,1)
_Amp ("Amplitude", Range(0, 0.5)) = 0.05
_Freq ("Frequency", Range(0, 20)) = 6
_Speed ("Speed", Range(0, 10)) = 2
}
SubShader
{
Tags
{
"RenderPipeline"="UniversalPipeline"
"RenderType"="Opaque"
"Queue"="Geometry"
}
Pass
{
Name "ForwardUnlit"
Tags { "LightMode"="UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
float _Amp;
float _Freq;
float _Speed;
CBUFFER_END
struct Attributes
{
float4 positionOS : POSITION;
};
struct Varyings
{
float4 positionHCS : SV_POSITION;
};
Varyings vert (Attributes IN)
{
Varyings OUT;
float t = _Time.y * _Speed;
float3 pos = IN.positionOS.xyz;
pos.y += sin(pos.x * _Freq + t) * _Amp;
OUT.positionHCS = TransformObjectToHClip(pos);
return OUT;
}
half4 frag (Varyings IN) : SV_Target
{
float wave = 0.5 + 0.5 * sin(_Time.y);
float3 col = _BaseColor.rgb * lerp(0.4, 1.2, wave);
return half4(col, 1);
}
ENDHLSL
}
}
}
まず確認すること(超重要)
① マテリアルを作る
- Create > Material
- Shader を Lain/UnlitWobbleURP に変更
② Plane に貼る
- Plane or Cube にマテリアルを適用
③ 動いているか?
- 頂点が 波打つ
- 色が 時間で変わる
- Inspector で
_Amp / _Freq / _Speedを動かすと変化する
👉 ここまで来たら この回は成功。
チェックリスト
- Shaderファイルを自分で作った
- Planeが波打つ
- 色が時間で変わる
- _Amp/_Freq/_Speed を触って挙動が変わる
- 「Object→Clip」の意味が雑にでもわかる
6. ShaderLab / HLSL を1行ずつ読む
(Three.js との対応も入れる)
以下のコードを手元に置いて進めてほしい👇 ※小分けにして解説するので安心して。
Shader "Lain/UnlitWobbleURP"
{
Properties
{
_BaseColor ("Base Color", Color) = (1,1,1,1)
_Amp ("Amplitude", Range(0, 0.5)) = 0.05
_Freq ("Frequency", Range(0, 20)) = 6
_Speed ("Speed", Range(0, 10)) = 2
}
6-1. Properties = Three.js の uniform
Three.js ではこんな感じで uniform を渡す。
uniforms: {
uAmp: { value: 0.05 },
uFreq: { value: 6.0 },
uSpeed: { value: 2.0 },
}
Unity ではこれが Properties。
- Material から編集可能
- C# からも SetFloat で渡せる
- HLSL 内では自動で変数に展開される
つまり:
Properties = Material / C# から操作できる “uniform” の宣言
6-2. SubShader / Tags
SubShader
{
Tags
{
"RenderPipeline"="UniversalPipeline"
"RenderType"="Opaque"
"Queue"="Geometry"
}
“URP用のシェーダー”
→ "RenderPipeline"="UniversalPipeline"
“不透明物として扱う”
→ "RenderType"="Opaque"
“描画順は Geometry(普通のオブジェクト)”
→ "Queue"="Geometry"
この3つさえ覚えておけば、初心者はOK。
URP使ってるなら 必ず RenderPipeline を書く → これを書かないと、シーンに貼っても 真っ黒 になる
6-3. Pass(ここから本番)
Pass
{
Name "ForwardUnlit"
Tags { "LightMode"="UniversalForward" }
ここは レンダリングの流れに参加する入り口。
- UniversalForward : URP の標準パス
- Unlit:光計算しない
今は「LightMode に参加する場所」とだけ覚えてOK。
6-4. HLSLPROGRAM の中が “Three.js の GLSL” に相当
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
これは単純に:
- 頂点シェーダー →
vert - フラグメントシェーダー →
frag
を使います、という宣言。
Three.js ならこれ:
vertexShader: rawVertexShader,
fragmentShader: rawFragmentShader,
完全に同じ概念。
6-5. Unity の行列まとめがここに入っている
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
この1行はめっちゃ重要。
Three.js だと GLSL に自分でこう書く:
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
Unityは 行列関数をセットにしたライブラリを include で読み込むだけ。
ここから TransformObjectToHClip() が使える。
TransformObjectToHClip = モデル行列 × ビュー行列 × プロジェクション行列
Three.js のこれと完全に同じ:
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
6-6. Property の実体は CBUFFER に格納される
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
float _Amp;
float _Freq;
float _Speed;
CBUFFER_END
これは Unity の仕様で、
Properties の値を GPU に渡す “入れ物” = Constant Buffer
Three.js の uniform と同じ。 この中身が HLSL でそのまま使える。
6-7. Attributes / Varyings
(=Three.js の attribute / varying)
struct Attributes
{
float4 positionOS : POSITION;
};
struct Varyings
{
float4 positionHCS : SV_POSITION;
};
Three.js の GLSL ではこうだった:
attribute vec3 position;
varying vec3 vPos;
Unity では:
- positionOS = Object Space(ローカル座標)
- positionHCS = Homogeneous Clip Space(クリップ空間)
この “OS → HCS” の変換を TransformObjectToHClip がやる。
6-8. 頂点シェーダー(vert)
ここが今回の 主役。
Varyings vert (Attributes IN)
{
Varyings OUT;
float t = _Time.y * _Speed;
float3 pos = IN.positionOS.xyz;
pos.y += sin(pos.x * _Freq + t) * _Amp;
OUT.positionHCS = TransformObjectToHClip(pos);
return OUT;
}
Three.js の GLSL と対応づける:
Three.js #02(復習)
vec3 pos = position;
pos.y += sin(pos.x * uFreq + uTime * uSpeed) * uAmp;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
Unity 版:
_Time.y
Unityが提供する “経過時間(秒)”。
Three.js の uTime と同じ。
頂点を sin で上下させる
pos.y += sin(pos.x * _Freq + t) * _Amp;
Three.js と同じ方程式。 書く場所が違うだけ。
TransformObjectToHClip
OUT.positionHCS = TransformObjectToHClip(pos);
これが Three.js のコレ:
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
数学的に同じ処理を Unity 用にラップしたもの。
6-9. フラグメントシェーダー(frag)
half4 frag (Varyings IN) : SV_Target
{
float wave = 0.5 + 0.5 * sin(_Time.y);
float3 col = _BaseColor.rgb * lerp(0.4, 1.2, wave);
return half4(col, 1);
}
やっていることは Three.js と同じ:
色を「時間でゆっくり明るくしたり暗くしたり」
Three.js GLSL の例:
float wave = 0.5 + 0.5 * sin(uTime);
vec3 col = uBaseColor * mix(0.4, 1.2, wave);
Unity 版は lerp で同じ式。
ここまでの理解ができたら、もう “Unity の水面” に行ける
- 頂点を揺らす
- 色を変える
- TransformObjectToHClip が理解できた
- Property → uniform の関係が分かった
これで Unity の URP 水面(sin 波 + ライティング)をやれる下地が整った。
7.Unity – 最小 HLSL シェーダーのまとめ
① Three.js の GLSL と Unity の HLSL は “中身が同じ”
- どっちも 頂点シェーダー(vert) と フラグメントシェーダー(frag)
- 三角形 → 頂点座標 → 行列計算 → 画面座標 → ピクセル色 という流れは 100%同じ
Unityは ShaderLab が “設定の外側” を包んでるだけ。
② Properties = Three.js の uniform
Three.js:
uniforms: { uAmp: { value: 0.1 } }
Unity:
Properties { _Amp ("Amplitude", Float) = 0.1 }
→ Material で編集可能 → C# からも SetFloat で変更可能 → HLSL では CBUFFER に自動展開される
=uniform と同じ
③ TransformObjectToHClip が “モデル×ビュー×プロジェクション” を全部やる
Three.js の GLSL:
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
Unity:
OUT.positionHCS = TransformObjectToHClip(pos);
この関数が “行列4段階の全部” をまとめてくれてる。
これを理解すると、 Unity の頂点シェーダーが一気に簡単に見える。
④ sin で頂点を動かす処理は Three.js と完全に同じ式
Three.js:
pos.y += sin(pos.x * uFreq + uTime * uSpeed) * uAmp;
Unity:
pos.y += sin(pos.x * _Freq + _Time.y * _Speed) * _Amp;
→ 使う変数名と参照場所が違うだけ → 数学と動きの本質は同じ
「頂点を上下させる」という体験が 環境を超えて共通の学び になった。
⑤ 色の変化(frag)も Three.js と全く同じ
Three.js:
float wave = 0.5 + 0.5 * sin(uTime);
vec3 col = baseColor * mix(0.4, 1.2, wave);
Unity:
float wave = 0.5 + 0.5 * sin(_Time.y);
float3 col = _BaseColor.rgb * lerp(0.4, 1.2, wave);
唯一違うのは mix → lerp。
数学と構造は同じ。
この回で身についた力
✔ Unity のシェーダーが“読める”ようになる
✔ sin 波のアニメーションが理解できた
✔ 頂点移動・色変化の両方を実装できた
✔ ShaderLab(外側)と HLSL(中身)の関係がわかった
✔ Three.js と Unity の橋が繋がった
この回の内容を消化できれば、
SimpleWater や StylizedWater の仕組みは半分わかったも同然。
💬 コメント