Unity 6(URP 17)で作る Planar Reflection 水面

– Skybox とオブジェクトが正確に映る動的反射 –

この記事では、Unity 6(6000.2.x)+ URP17 環境で

  • Skybox
  • シーン内のオブジェクト

鏡のように水面へ映り込む Planar Reflection を実装します。

波・Foam・深度などのリッチ表現は一切入れず、「反射だけを最小構成で正しく動かす」ことに集中したチュートリアルです。

ここを押さえておくと、後で高機能な水シェーダーに差し替えるのが非常に楽になります。


0. 動作環境

  • Unity 6.2(6000.2.xx 系)
  • Universal Render Pipeline 17.x(3D URP テンプレートプロジェクト)
  • テスト OS:Windows / macOS

URP テンプレートから作った新規プロジェクトを前提にします。

この場合、すでに URP アセット・カメラ設定・DepthTexture などはよしなに設定されているので、本記事では追加の URP 設定は行いません。


1. Fantasy Skybox FREE をインポートする

水面に映す「空」が欲しいので、Asset Store から Skybox を追加します。

  1. Window → Package Manager
  2. 左上のプルダウンを My Assets
  3. 検索欄に Fantasy Skybox FREE と入力
  4. Download → Import
  5. すべてのファイルをインポート

インポート後、Project 内に

Assets/Fantasy Skybox FREE/

フォルダが出来ていれば OK です。


2. シーンを用意する

2-1. 新規シーン作成

  1. File → New Scene
  2. URP 用 3D シーンを選択(テンプレートに依存)

PlanarReflectionScene などの名前で保存しておきます。


2-2. Skybox を設定する

  1. Window → Rendering → Lighting を開く
  2. Environment タブ
  3. Skybox Material に Fantasy Skybox のマテリアルを指定(例:FS000_Day_01)

Scene ビューの空がファンタジーなスカイボックスに変われば OK です。


3. 水面(Plane)とレイヤー設定

3-1. 水面を作成

  1. Hierarchy → 右クリック3D Object → Plane
  2. 名前を WaterPlane に変更
  3. Transform を次のように設定
  • Position:(0, 0, 0)
  • Rotation:(0, 0, 0)
  • Scale:(10, 1, 10) (サイズはお好みで)

3-2. Water レイヤーを確認 / 設定

目的:反射カメラで “水面自身” を描画しないようにする。

URP テンプレートでは多くの場合、Layer 4 に “Water” が既に定義されています。

  1. WaterPlane を選択
  2. Inspector → Layer
  3. プルダウンから Water を選択

もし Water が無ければ:

  1. Layer → Add Layer…
  2. 使っていないユーザレイヤー(User Layer)に Water と入力
  3. WaterPlane の Layer に Water を設定

4. メインカメラの調整(Main Camera)

  1. Hierarchy から Main Camera を選択
  2. Transform を適当に調整(例)
    • Position:(0, 3, -10)
    • Rotation:(10, 0, 0)

Game ビューで水面と空が良い感じに映る位置にしておきます。

注意

Main Camera の Tag は 必ず MainCamera のままにしておきます
(ここが MainCamera でないと Camera.main が機能しません)


5. PlanarReflectionCamera を作成(反射専用カメラ)

反射用に、Main Camera をコピーしてカメラを 1 台追加します。

5-1. 複製して作る

  1. Main Camera を選択
  2. Ctrl + D(Duplicate)
  3. 名前を PlanarReflectionCamera に変更

5-2. Tag と Audio Listener に注意

このステップが一番ハマりやすいポイントです。

1. Tag を Untagged に変更

複製すると Tag もそのままコピーされるため、PlanarReflectionCamera にも MainCamera タグが付いている場合があります。

Camera.main は「MainCamera タグの付いた最初のカメラ」を返すため、2 台とも MainCamera だと、どちらが main になるか分からずバグの元になります。

  1. PlanarReflectionCamera を選択
  2. Inspector → Tag → Untagged を選択

これで Camera.main は必ず Main Camera の方だけを指します。

2. Audio Listener を削除

複製したカメラにも Audio Listener が付いています。

Unity は「Audio Listener はシーンに 1 つだけ」を前提としており、2 つあると警告が出ます。

  1. PlanarReflectionCamera の Inspector でAudio Listener コンポーネントの右上「…」→ Remove Component

Main Camera の Audio Listener は残したままにします。


5-3. PlanarReflectionCamera の設定

PlanarReflectionCamera は画面には描画せず、RenderTexture にだけ描画する“隠しカメラ”として使います。

  1. PlanarReflectionCamera を選択
  2. Inspector → Camera コンポーネント
  • Enabled:OFF→ Render() をスクリプトから手動で呼び出すため
  • Culling Mask:Everything から→ チェックを外して Water レイヤーを OFF にする(これを忘れると水面が水面に映り込み続けて無限ループになります)
  • それ以外の設定(Projection/FOV など)は Main Camera と同じで OK

6. RenderTexture を作成する

反射カメラの描画先となる RenderTexture を用意します。

  1. Project ウィンドウでフォルダを右クリックCreate → Render Texture
  2. 名前を RT_PlanarReflection にする
  3. Inspector でパラメータを確認(デフォルトでほぼ OK)
  • Size:1024 x 1024
  • Anti-aliasing:None(必要なら 2x, 4x)
  • Color Format:R8G8B8A8_UNorm
  • Depth Buffer:D32_SFLOAT_S8_UINT(デフォルト)
  • Mip Maps:OFF でも可
  • Filter:Bilinear
  • Wrap:Clamp

6-1. PlanarReflectionCamera に割り当て

  1. PlanarReflectionCamera を選択
  2. Camera コンポーネントの Output セクション
  3. Output Texture に RT_PlanarReflection をドラッグ

これで PlanarReflectionCamera → RT_PlanarReflection への描画ルートが確立しました。


7. 水面用マテリアルと Shader Graph

ここでは、反射テクスチャをそのまま貼るだけのシンプルな水面を作ります。

(波・Foam などは入れません)

7-1. Shader Graph を作成

  1. Project で Assets/PlanarReflection/Water/ShaderGraph/ などのフォルダを作成
  2. そのフォルダ上で右クリック →Create → Shader Graph → URP → Lit Shader Graph
  3. 名前:Water_PlanarDebug_SG

7-2. PlanarReflectionTex プロパティを追加

  1. Shader Graph をダブルクリックして開く
  2. 左の Blackboard で + → Texture2D を追加
  3. 名前:PlanarReflectionTex
  4. Reference:_PlanarReflectionTex(自動で入る名前で OK)
  5. デフォルトは white のままで良い

7-3. グラフのノード構成(超シンプル版)

  1. グラフの空いているところで Space キー → Sample Texture 2D を作成
  2. そのノードの Texture スロットに、Blackboard の PlanarReflectionTex をドラッグ
  3. Sample Texture 2D の RGBA 出力 をFragment → Base Color に接続
  4. Smoothness や Metallic はお好みで固定値を入れても良いですが、まずはデフォルトのままで OK

これで「_PlanarReflectionTex をそのまま貼るだけ」の水面シェーダーができました。


7-4. マテリアルを作成して WaterPlane に適用

  1. Shader Graph を右クリック → Create → Material
  2. 名前:M_Water_PlanarDebug
  3. WaterPlane を選択 → Mesh Renderer の Materials にM_Water_PlanarDebug を割り当て

8. PlanarReflectionController スクリプト

最後に、PlanarReflectionCamera を制御するスクリプトを作成します。

8-1. スクリプトファイルを作成

  1. Assets/PlanarReflection/Water/Scripts/ フォルダを作成
  2. その中で右クリック → Create → C# Script
  3. 名前:PlanarReflectionController
  4. 中身を次のコードで上書き保存します。
using UnityEngine;

[RequireComponent(typeof(Renderer))]
public class PlanarReflectionController : MonoBehaviour
{
    [Header("Targets")]
    public Camera mainCamera;
    public Camera reflectionCamera;
    public RenderTexture reflectionTexture;

    [Header("Water Plane")]
    public Vector3 planeNormal = Vector3.up;
    public float planeHeight = 0f; // 水面のY座標

    Material _material;
    static readonly int PlanarTexId = Shader.PropertyToID("_PlanarReflectionTex");

    void Awake()
    {
        _material = GetComponent<Renderer>().sharedMaterial;

        // Inspectorで指定されていなければ Main Camera を自動取得
        if (mainCamera == null)
            mainCamera = Camera.main;
    }

    void LateUpdate()
    {
        if (mainCamera == null || reflectionCamera == null || reflectionTexture == null)
            return;

        // 1. 水面の平面(法線と一点)を定義
        var n = planeNormal.normalized;
        var planePoint = new Vector3(0f, planeHeight, 0f);

        // 2. メインカメラの位置と向きを反転
        Vector3 camPos = mainCamera.transform.position;
        Vector3 camForward = mainCamera.transform.forward;
        Vector3 camUp = mainCamera.transform.up;

        // 平面までの距離
        float d = Vector3.Dot(n, camPos - planePoint);
        Vector3 reflPos = camPos - 2f * d * n;
        Vector3 reflForward = Vector3.Reflect(camForward, n);
        Vector3 reflUp = Vector3.Reflect(camUp, n);

        reflectionCamera.transform.position = reflPos;
        reflectionCamera.transform.rotation = Quaternion.LookRotation(reflForward, reflUp);

        // 3. クリッププレーンを調整(簡易版だが十分実用的)
        Vector4 plane = new Vector4(n.x, n.y, n.z, -Vector3.Dot(n, planePoint));
        var reflectionMat = CalculateReflectionMatrix(plane);
        reflectionCamera.worldToCameraMatrix = mainCamera.worldToCameraMatrix * reflectionMat;

        // 投影行列はメインカメラと揃える
        reflectionCamera.projectionMatrix = mainCamera.projectionMatrix;

        // 4. 描画
        reflectionCamera.targetTexture = reflectionTexture;
        reflectionCamera.enabled = false;  // 念のためOFF
        reflectionCamera.Render();

        // 5. マテリアルにテクスチャを渡す
        _material.SetTexture(PlanarTexId, reflectionTexture);
    }

    // 平面に対する反射行列(Unity標準実装と同等の簡易版)
    Matrix4x4 CalculateReflectionMatrix(Vector4 plane)
    {
        Matrix4x4 m = Matrix4x4.identity;

        m.m00 = 1f - 2f * plane[0] * plane[0];
        m.m01 = -2f * plane[0] * plane[1];
        m.m02 = -2f * plane[0] * plane[2];
        m.m03 = -2f * plane[3] * plane[0];

        m.m10 = -2f * plane[1] * plane[0];
        m.m11 = 1f - 2f * plane[1] * plane[1];
        m.m12 = -2f * plane[1] * plane[2];
        m.m13 = -2f * plane[3] * plane[1];

        m.m20 = -2f * plane[2] * plane[0];
        m.m21 = -2f * plane[2] * plane[1];
        m.m22 = 1f - 2f * plane[2] * plane[2];
        m.m23 = -2f * plane[3] * plane[2];

        m.m30 = 0f;
        m.m31 = 0f;
        m.m32 = 0f;
        m.m33 = 1f;
        return m;
    }
}

8-2. WaterPlane にアタッチし、参照を設定

  1. Hierarchy → WaterPlane を選択
  2. Inspector → Add Component → PlanarReflectionController
  3. フィールドを次のように設定
  • Main Camera:Main Camera をドラッグ(空の場合は Awake で Camera.main が自動設定されます)
  • Reflection Camera:PlanarReflectionCamera
  • Reflection Texture:RT_PlanarReflection
  • Plane Normal:(0, 1, 0)(デフォルト)
  • Plane Height:水面の Y 座標(今回なら 0)

9. オブジェクトを配置して動作確認

  1. Hierarchy で 3D Object → Cube や Capsule を追加
  2. Position を (0, 1, 0) などにして水面の上に置く
  3. 再生(Play)

期待する結果:

  • 水面に 空(Skybox)と Cube/Capsule が上下反転で映る
  • カメラを動かすと映り込みもリアルタイムで変化する
  • 無限反射やチラつきは発生しない

もし水面が自分自身を映しているような挙動になった場合は:

  • PlanarReflectionCamera の Culling Mask から Water レイヤーが外れているか
  • WaterPlane の Layer が Water になっているか

をもう一度確認してください。


10. ここまでで得られた構成

フォルダ構成の一例:

Assets/
  PlanarReflection/
    Water/
      Materials/
        M_Water_PlanarDebug.mat
      RenderTextures/
        RT_PlanarReflection.renderTexture
      Scripts/
        PlanarReflectionController.cs
      ShaderGraph/
        Water_PlanarDebug_SG.shadergraph
  Fantasy Skybox FREE/
  Scenes/
    PlanarReflectionScene.unity

シーン内オブジェクト:

  • Main Camera(Tag: MainCamera)
  • PlanarReflectionCamera(Tag: Untagged, Audio Listener 無し)
  • Directional Light
  • Global Volume(URP テンプレの既定)
  • WaterPlane(Layer: Water, PlanarReflectionController attached)
  • Cube / Capsule など

11. まとめと発展

このチュートリアルでは、Unity 6 / URP17 で

  • Planar ReflectionCamera + RenderTexture
  • 水面用 ShaderGraph
  • 制御スクリプト PlanarReflectionController

を組み合わせることで、

「シンプルだが正しく動く動的水面反射」

を構築しました。

この構成の上に、

  • Gerstner 波(頂点アニメーション)
  • 深度グラデーション(浅瀬/深部)
  • Foam(白波)
  • Fresnel 強調
  • ReflectionProbe の Cubemap を加えたハイブリッド反射

などを載せていくと、原神風〜リアル系まで自由に拡張できます。


訪問数 1 回, 今日の訪問数 14回

Shader,Unity

Posted by hidepon