[Shader 入門 #03] Unity で最小の HLSL を書く:_Time で頂点と色を動かす(URP対応)

0. はじめに

Three.js #02 でやった

  • 時間で 頂点を上下に揺らす
  • 時間で 色を変える

これを Unity(URP) で 自分の手で再現する。

難しい水面はまだやらない
ライティングもいらない
「動いた」という事実を作るのが目的

この回は 「Unityの水・炎・発光シェーダーに進むための足場」。

前回の記事:

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)

  1. Unity の Assets フォルダを右クリック
  2. Create > Shader > Unlit Shader
  3. 名前を 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 の仕組みは半分わかったも同然。