[Shader 入門 #02] Three.js で最小のシェーダーを書く:GLSL の“動く”感覚を掴む

ステップ1:RawShaderMaterial で最小構成を知る

Three.js で 最も“生の GLSL”に近い書き方ができるのが RawShaderMaterial です。

普通の ShaderMaterial は Three.js が内部で

uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;

などの 便利ユニフォームを自動で追加してくれます。

しかし RawShaderMaterial では、 「GPU に渡すものを全部、自分で書く」 必要があります。

言い換えると、

  • WebGL(OpenGL)の素の書き方に最も近い
  • Three.js の介入が最小限
  • “GPU と直接話す”感覚が一番分かりやすい

これが、シェーダーの最初のつかみとして最適なんです。


最小の RawShaderMaterial

const material = new THREE.RawShaderMaterial({
  vertexShader: vs,
  fragmentShader: fs,
  uniforms: {
    uTime: { value: 0 }
  }
});

ここで重要なのは 3つだけ。

🟦 1. vertexShader: vs

頂点をどう変形させるか(=形を変える処理)。

🟩 2. fragmentShader: fs

最終的にピクセルの色をどう塗るか(=見た目の処理)。

🟥 3. uniforms

CPU → GPU に渡す変数(時間・色・行列・テクスチャなど)。

「便利機能を捨てる」ことで得られるもの

RawShaderMaterial を使うと、Three.js が普段やっている…

  • 自動で行列を入れてくれる
  • 自動で varying を補完してくれる
  • 自動でフラグメントの精度宣言を入れてくれる

…といった “親切機能が一切ない状態” になります。

最初は不便ですが、 実はこれが シェーダー学習の最強の環境 です。

なぜなら、

「GLSL を書くと画面が変わる」 という ダイレクトな体験 が得られるから。

Three.js のレイヤを挟むほど “GPU がどう動いているのか” は見えづらくなります。

RawShaderMaterial は 💡 GPUへの道筋が一番シンプルで分かりやすい という点が最大の利点です。


このステップで到達したいゴール

  • GLSL が「ただの関数」だと理解する
  • GPU が「大量の頂点に同じ処理をする」ことを感じる
  • “動く” → “反応する” → “楽しい” のサイクルを作る

このあと書く 頂点を揺らすサンプル が、 まさにこの RawShaderMaterial の真価を感じられる部分。

Three.js の経験が長いあなたでも、 「シェーダーの核心ってこれか」と実感できるはず。

ステップ2:最小の Vertex / Fragment シェーダー

ここでは 「Three.js が用意した便利機能を全て取り払った状態」 の 純粋な GLSL を書いていきます。

この 2つのシェーダーだけで
👉 “自分の GLSL が GPU 上で動いている”
という実感が得られます。


Vertex Shader(頂点を画面に送るだけの最小形)

precision highp float;

attribute vec3 position;

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;

void main() {
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

これが「最小の頂点シェーダー」

ポイントは4つ。

  1. precision GPU に「float の精度」を指定する。 WebGL では 書かないとエラー になる。

  2. attribute vec3 position; メッシュの頂点座標がここに入ってくる。 Three.js → GPU に自動で渡される。

  3. uniform mat4 modelViewMatrix / projectionMatrix Three.js が自動で毎フレーム渡してくれる行列(RawShader でも渡る)。

    • modelViewMatrix = モデル位置 × カメラ位置
    • projectionMatrix = 遠近感(FOV)
  4. gl_Position GPU が「画面に描く座標」。 頂点シェーダーの最終アウトプット。


Fragment Shader(赤色を塗るだけの最小形)

precision highp float;

void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

これ以上シンプルにはできない

  • precision(精度)
  • 出力する gl_FragColor(RGBA)

これだけ。


このステップで「何が分かるか?」

あなたのように Three.js も Unity もやっている人でも、 この最小形を書くと、GPU が何を要求しているのかが一気に見える。

特に重要なのは以下の3つ。


① 「頂点 → gl_Position」さえあれば描画される

どんなシェーダーでも核は同じ。

頂点が来る
行列で変換する
gl_Position に入れる

どれだけ難しい水面・炎・発光シェーダーでも、 この“根っこ”は一生変わらない。


② fragment shader は“画家”にすぎない

Vertex シェーダーが頂点を決めて、 Fragment シェーダーがその後ろから色を塗る。

これを理解すると 👉 頂点で形が作られる 👉 fragment は形の内側を塗るだけ

という “役割の分業” が自然に見えてくる。


③ Three.js の「便利機能」が全部外れた感覚を得る

RawShaderMaterial の良さは:

  • 行列を自分で受け取る
  • 頂点を自分で gl_Position に入れる
  • 精度指定も自分で書く
  • varying も自分で作る必要がある

この “不親切な素の環境” こそが GLSL を理解する最短ルート なんだよね。

ステップ3:頂点を “揺らす”(sin 波)

ここからいよいよ “形を動かす” 体験に入ります。

Three.js 時代にあなたが磨いた sin / cos の直感 が、そのまま頂点変形に直結する瞬間です。

下の頂点シェーダーは、 1行だけ頂点を加工して「揺れる面」を作る最小の例 です。

頂点を上下に揺らす GLSL(sin 波による変形)

uniform float uTime;
attribute vec3 position;

void main() {
  vec3 pos = position;
  pos.z += sin(pos.x * 4.0 + uTime * 2.0) * 0.2;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}

この1行がすべての魔法

pos.z += sin(pos.x * 4.0 + uTime * 2.0) * 0.2;

pos.x

横方向に対して波の模様を作る。

* 4.0

波の細かさ。(値を大きくすると波が増える)

uTime * 2.0

時間で波を“動かす”。 → Three.js 側で uniforms.uTime.value += delta; すればアニメする。

* 0.2

振幅。小さいほど“繊細な揺れ”。


ゲーム開発のあらゆる場面で使われる

sin 波の変形は、たった1行なのに応用範囲が異常に広い。

① 海・湖・川の水面(周期的な波)

波の揺れ方はほぼこれの応用。

② 草・木の揺れ(風アニメ)

pos.ypos.x + pos.z を使うと風でそよぐ。

③ キャラの呼吸アニメ(ULTRAKILL でも使われる手法)

胸のボーンを揺らすのと同じこと。

④ 髪のふわっとした揺れ

小さい振幅の sin を複数混ぜるだけ。

⑤ 光、UI の脈動エフェクト

sin で“弱→強→弱”が自然に作れる。


Three.js のあなたなら分かる“本質”

Three.js の Canvas 時代に、 マウス追従や波紋や弾幕で何度も書いたこの式:

y = sin(x + time)

あの感覚が GPU の空間でそのまま動く。

しかも CPU と違い、 数千~数万頂点に同時に sin を掛けても平気 (GPU が並列で処理するため)。

だからシェーダーは「大量のオブジェクトを一瞬で動かせる」わけです。

ここが “シェーダー開眼ポイント”

  • 頂点を受け取る
  • 位置を少し変える
  • gl_Position に戻す

ただそれだけ。

ステップ4:色を “時間で変える”

頂点を動かすのが Vertex Shader なら、 色を動かすのが Fragment Shader です。

ここでは「時間 uTime をそのまま色に変換する」だけで、 “画面が生きて見える” 状態を作ります。

時間で色を変える最小 Fragment Shader

uniform float uTime;

void main() {
  float r = abs(sin(uTime));
  gl_FragColor = vec4(r, 0.5, 1.0, 1.0);
}

何が起きているか(超シンプル)

sin(uTime)

時間が進むにつれて -1〜+1 の間を往復する。

abs(…)

マイナスを消して 0〜1 の範囲に整える。 → 明るさ(強さ)として使いやすくなる。

float r

赤成分を 0〜1で変化させる。 だから色が呼吸するように脈動する。


これが “ネオンっぽさ” の正体

ネオンや発光っぽい表現って、実は難しいことをしてない。

  • 明るさが周期的に揺れる
  • 一定の色味を保ちながら脈動する

これだけで “それっぽく” 見える。

そしてこの abs(sin(time)) は、 その最小構成。


応用が効く理由

この1行を使い回すだけで、いろんな表現が作れる:

UI の点滅(注意・警告・選択)

r をアルファに入れるだけで “点滅UI”。

体力が減った時の危険色

赤だけ脈動 → 危機感が出る。

アイテムの発光・レア感

発光の強弱 → それだけで価値が上がって見える。

魔法陣・オーラ・エフェクト

“脈動”が入ると生き物っぽくなる。


このステップのゴール

  • Fragment Shader は「塗るだけ」ではなく 動かせる
  • 時間 uTime を入れるだけで アニメーションになる
  • “シェーダー=難しい” の抵抗感が消える

ステップ5:なぜ GLSL は “生の理解” に繋がるのか?

ここまでで 「頂点を動かす」「色を動かす」 を体験しました。

では、なぜ GLSL を書くと 3D の理解が一気に深まるのか?

理由はシンプルに、この3つです。

① 行列を自分で扱うから、仕組みを“強制的に”理解する

GLSL では、頂点を画面に出すために必ず

  • modelMatrix(オブジェクトの世界位置)
  • viewMatrix(カメラ)
  • projectionMatrix(遠近感)

この3つを毎フレーム掛け合わせる必要があります。

gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

Three.js は行列を全部そのまま見せる。 Unity は逆で、

  • UnityObjectToWorld
  • UNITY_MATRIX_VP
  • TransformObjectToHClip

など、抽象化した関数を使うため「見えにくい」。

だから Three.js で GLSL を通ると 空間変換の仕組みが丸見えになる。

これは Unity のシェーダーを読む時に ものすごく強い味方になる。


② vec2 / vec3 / vec4 「型の感覚」が直に掴める

GLSL は C言語に近い “硬い” 言語で、 型を誤ると絶対にコンパイルが通りません。

  • UV → vec2
  • 位置 → vec3
  • 色 → vec4
  • 行列 → mat4

こうした 3D の基礎型が身体レベルで理解できるのが、 GLSL を書く最大の副産物です。

Unity(HLSL)でも型の考え方は全く同じなので、 GLSL を触った後だと HLSL の吸収速度が爆速になります。


③ GPU で何が走っているかを意識できるようになる

GLSL は 「GPU のための低レベル言語」 と言っていい。

  • for を回すと GPU がその回数だけ計算する
  • 分岐は遅い
  • テクスチャはサンプルするだけでコストがある
  • sin/cos は比較的高コスト
  • 計算は頂点単位・ピクセル単位で並列に実行される

こういった性質が コードに直接影響する世界 だからこそ、 「GPUとは何か?」が自動的に理解されていく。

普通の高級言語では味わえない感覚。


まとめ:GLSL = 3D理解の“地力”を作る最短ルート

GLSL を書くことで…

  • 行列
  • 座標変換
  • 法線
  • 時間
  • 並列計算
  • GPU の特性

これら3Dの土台すべてが 一ヶ所に集まって見える。

Three.js の経験を持っているあなたにとって、 GLSL はまさに

「すべての技術を一本の線で繋ぐ言語」

になっていく。

ステップ6:Three.js 側で uTime を更新して、実際に動かす

ここまででシェーダー側には

  • uTime(時間の uniform)
  • sin での頂点アニメーション
  • 色を変える fragment アニメーション

が揃っています。

最後に必要なのは、 JavaScript(Three.jsの animate ループ)で時間を毎フレーム更新すること。


Three.js の animate ループで uTime を加算する

function animate(time) {
  requestAnimationFrame(animate);

  material.uniforms.uTime.value = time * 0.001; // 秒に変換

  renderer.render(scene, camera);
}
animate();

解説:なぜ time * 0.001 するの?

timerequestAnimationFrame によって ミリ秒(ms)で渡される ため、そのままだと数値が大きすぎる。

  • 1000
  • 2000
  • 3000

…とどんどん増えるので、 sin(time) の周期が速すぎて “点滅地獄” になる。

* 0.001 で 秒(s)に変換すると、 人間が見て心地よいスピードになる。


完全な minimum 例(読者がコピペで動かせる)

(※記事に貼る時は「全コードは GitHub に置いてます」としてもOK)

// 平面
const geometry = new THREE.PlaneGeometry(2, 2, 100, 100);

const material = new THREE.RawShaderMaterial({
  vertexShader: vs,
  fragmentShader: fs,
  uniforms: {
    uTime: { value: 0 }
  },
  wireframe: false
});

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// アニメーションループ
function animate(time) {
  requestAnimationFrame(animate);

  material.uniforms.uTime.value = time * 0.001;

  renderer.render(scene, camera);
}
animate();

これで「動く GLSL シェーダー」が完成する

このステップまで来ると読者は、

  • 三角形が波打ち始める
  • 色が脈動する
  • CPU の update と GPU の shader が連動する

という “初めてのシェーダー体験” を確実に味わえる。

あなたも Three.js から Unity に移る時に、 この“CPU → GPU の感覚”が分かった瞬間、理解が一気に進んだはず。


このステップ6で読者が得られるもの

  • シェーダーは“静的なコード”ではなく“動く部品”
  • JavaScript と GLSL が一緒に動いて作品を作る
  • 自分のコードで GPU がアニメする感動
  • Unity/HLSL に行く前の最高の準備

Unity × URP:最小の「波シェーダー」(HLSL)

ファイル名は 📄 MinimalWave.shader として Project に入れればOK。


MinimalWave.shader(URP / 頂点 sin 波 / 色変化)

Shader "Lain/MinimalWave"
{
    Properties
    {
        _Color("Base Color", Color) = (1,1,1,1)
        _Amplitude("Wave Amplitude", Float) = 0.2
        _Frequency("Wave Frequency", Float) = 4.0
        _Speed("Wave Speed", Float) = 2.0
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue"="Geometry" }

        Pass
        {
            // URP と互換性のあるシェーダーヘッダ
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            // Properties → HLSL に転送される
            float4 _Color;
            float _Amplitude;
            float _Frequency;
            float _Speed;

            // 時間(Unity が自動で供給)
            UNITY_DECLARE_TIME;

            struct Attributes
            {
                float4 positionOS : POSITION;  // Object Space 頂点
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION; // Homogeneous Clip Space
                float3 color : TEXCOORD0;
            };

            Varyings vert(Attributes IN)
            {
                Varyings OUT;

                float t = UNITY_TIME.y;   // 秒ベースの時間
                float3 pos = IN.positionOS.xyz;

                pos.y += sin(pos.x * _Frequency + t * _Speed) * _Amplitude;

                OUT.positionHCS = TransformObjectToHClip(float4(pos, 1.0));

                OUT.color = float3(
                    abs(sin(t)),
                    0.5,
                    1.0
                );

                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                return half4(IN.color, 1.0) * _Color;
            }

            ENDHLSL
        }
    }
}

Three.js の GLSL と “1:1 対応” の解説

あなたの Three.js の GLSL を思い出してほしい。

pos.z += sin(pos.x * 4.0 + uTime * 2.0) * 0.2;

Unity 版はこれ:

pos.y += sin(pos.x * _Frequency + t * _Speed) * _Amplitude;

位置が z→y に変わっただけで、 計算は 完全に同じ。


Three.js と Unity の対応表

Three.js(GLSL) Unity(HLSL) 役割
position positionOS オブジェクト座標の頂点
gl_Position positionHCS 画面座標
projectionMatrix * modelViewMatrix TransformObjectToHClip() 行列変換
uniform float uTime UNITY_TIME.y 時間
sin() sin() 全く同じ
vec3 float3 型もほぼ同じ

あなたの GLSL の理解がそのまま HLSL に移植できている。


Unity側の設定(これだけで動く)

  1. Plane を作る

  2. Material を作る

    • Shader → Lain / MinimalWave
  3. Plane に割り当てる

  4. Sceneに置いて再生するだけ

→ 頂点が波打つ → 色が脈動する → Three.js と同じシェーダーが Unity で動く をそのまま確認できる。


なぜこれは“①+②ミックス”と言える?

① Three.js とほぼ同じロジック

  • sin 波
  • time
  • 頂点変形
  • 色変化 100% 同じコンセプト

② URPで動く

  • Core.hlsl 使用
  • TransformObjectToHClip
  • Properties → HLSL
  • Universal RP互換 あなたの Unity6 のプロジェクトと完全一致。

まとめ:Three.js で「シェーダーの基礎体験」を自分の手で掴む

第2回では、 Three.js の RawShaderMaterial を使って “最小のシェーダー” を動かしました。

やったことは非常にシンプルでしたが、 GPU と直接対話する感覚がハッキリ分かる、もっとも重要なステップです。


1. RawShaderMaterial で “素のGLSL” を触った

Three.js の便利機能をすべて外し、 行列・型・出力すべてを自分で書く環境を体験しました。

これが シェーダーの本質。


2. 頂点を動かして「形が変わる瞬間」を見た

sin 波によって、 平面の 頂点が生き物のように揺れる 経験をしました。

これは、

  • 呼吸
  • 魔法エフェクト
  • UI の脈動

あらゆる表現の“種”になります。


3. 色を変えて「fragment も動かせる」ことを理解

abs(sin(time)) のような シンプルな式が強力なアニメーションになることを確認しました。

これはゲームの世界で本当によく使われる技法です。


4. JavaScript → GLSL の連動で、初めて“作品”になる

uTime を animate ループで更新することで、 CPU と GPU が協力して画面を作ることが体感できます。

これは Three.js でも Unity でも共通する “本質” です。


5. GLSL を学ぶと、3Dの本質が一気に繋がる

  • 行列
  • 座標変換
  • 頂点
  • ピクセル
  • 並列計算
  • 時間

すべてが 一本の理屈で統合されるため、 3Dの理解が急に深まります。

この“生の理解”は、 Unity/HLSL に進んだときに 最大の武器 になります。


次回(第3回):Unity – 同じ処理を HLSL で書いてみる

Three.js で書いた GLSL を Unity の HLSL に書き換えるだけで、 全く同じ波・色変化を再現できます。

さらに Unity 版では:

  • Properties
  • SubShader
  • Pass
  • URP の行列関数
  • _Time
  • HLSL の型(float2/float3/float4)

など、Three.jsとの“違いと対応”が一気に見えるようになります。

ここが Three.js と Unity の二刀流が完成する回です。