🏃

Unityでシンプルな波シミュレーション 第0回準備編

に公開

概要

NVIDIAが公開しているGPU Gems Chapter 1を参考に、簡単な波のシミュレーションをUnityで実装してみる。

https://developer.nvidia.com/gpugems/gpugems/part-i-natural-effects/chapter-1-effective-water-simulation-physical-models

GPU Gemsの手法では、正弦波の足し合わせをベースにしたシンプルなモデルで波のシミュレーションを行う。

これをUnity上で実装するにあたって、まずは波の表示に使うメッシュや効率よくシェーダコードを書いていくための準備をしていく。

環境

一応参考として、今回実装していく作業環境を書いておく。

  • OS:Windows 11
  • コードエディタ:Visual Studio 2022
  • Unity バージョン:Unity 6000.0.55f1 (LTS)
  • プロジェクトテンプレート:Universal 3D

メッシュ生成

今回は、主に頂点シェーダを使って頂点を上下させることで波を表現する。したがって、ある程度頂点数のある平面のメッシュが必要になる。Unity標準のPlaneメッシュでもおそらく事足りるが、後々のことを考えて任意の頂点数やサイズでメッシュを生成できるようにしてみる。

メッシュの生成スクリプトは、ChatGPTに作ってもらった。

https://chatgpt.com/share/6898701c-57d4-8007-b624-a9f4dc624f95

using UnityEngine;
using UnityEditor;
using System.IO;

public class PlaneMeshGenerator : EditorWindow
{
    int xSegments = 10;
    int zSegments = 10;
    float width = 10f;
    float height = 10f;
    string assetName = "GeneratedPlane";

    [MenuItem("Tools/Generate Plane Mesh")]
    static void OpenWindow()
    {
        GetWindow<PlaneMeshGenerator>("Plane Mesh Generator");
    }

    void OnGUI()
    {
        GUILayout.Label("Plane Settings", EditorStyles.boldLabel);

        xSegments = EditorGUILayout.IntField("X Segments", Mathf.Max(1, xSegments));
        zSegments = EditorGUILayout.IntField("Z Segments", Mathf.Max(1, zSegments));
        width = EditorGUILayout.FloatField("Width", width);
        height = EditorGUILayout.FloatField("Height", height);
        assetName = EditorGUILayout.TextField("Asset Name", assetName);

        if (GUILayout.Button("Generate Mesh Asset"))
        {
            CreateMeshAsset();
        }
    }

    void CreateMeshAsset()
    {
        Mesh mesh = new Mesh();
        mesh.name = assetName;

        int vertCountX = xSegments + 1;
        int vertCountZ = zSegments + 1;

        Vector3[] vertices = new Vector3[vertCountX * vertCountZ];
        Vector2[] uv = new Vector2[vertices.Length];
        int[] triangles = new int[xSegments * zSegments * 6];

        for (int z = 0; z < vertCountZ; z++)
        {
            for (int x = 0; x < vertCountX; x++)
            {
                float xPos = ((float)x / xSegments - 0.5f) * width;
                float zPos = ((float)z / zSegments - 0.5f) * height;
                vertices[z * vertCountX + x] = new Vector3(xPos, 0, zPos);
                uv[z * vertCountX + x] = new Vector2((float)x / xSegments, (float)z / zSegments);
            }
        }

        int index = 0;
        for (int z = 0; z < zSegments; z++)
        {
            for (int x = 0; x < xSegments; x++)
            {
                int v00 = z * vertCountX + x;
                int v10 = v00 + 1;
                int v01 = v00 + vertCountX;
                int v11 = v01 + 1;

                triangles[index++] = v00;
                triangles[index++] = v01;
                triangles[index++] = v10;

                triangles[index++] = v10;
                triangles[index++] = v01;
                triangles[index++] = v11;
            }
        }

        mesh.vertices = vertices;
        mesh.uv = uv;
        mesh.triangles = triangles;
        mesh.RecalculateNormals();

        // 保存先を指定
        string path = EditorUtility.SaveFilePanelInProject(
            "Save Plane Mesh",
            assetName,
            "asset",
            "Please enter a file name to save the mesh asset to"
        );

        if (!string.IsNullOrEmpty(path))
        {
            AssetDatabase.CreateAsset(mesh, path);
            AssetDatabase.SaveAssets();
            EditorUtility.DisplayDialog("Success", "Mesh asset created!", "OK");
        }
    }
}

最後がSuccess一択なのがちょっと気に入らないが、まあ実用上はさほど問題なさそうだ。

OnGUI()で頂点数などのプレビューも入れてやると少し親切かもしれない。

GUILayout.Label("Preview", EditorStyles.boldLabel);

GUILayout.Label($"Vertices: {(xSegments + 1) * (zSegments + 1)}");
GUILayout.Label($"Triangles: {xSegments * zSegments * 2}");

ChatGPTはAssets/Editorフォルダに入れる、と言っているがEditorフォルダであればどこでもいいのでAssets/Scripts/Editorあたりに入れた。

このようなウインドウが表示される。

今回は16x16に分割したメッシュを生成した。今後しばらくはこのメッシュを使っていく。

シェーダ

端的に言って、Unityでシェーダを作成・編集するのは結構不便である。基本的にはノードベースのShaderGraphが推されているが、それは直感的な操作をウリにしたものであり、今回のように数式ベースの処理を落とし込むのにはあまり向いてない。(というか、私がノードベースのGUIがニガテ!)

一方でコードで書こうとすると、Unityは独自シェーダファイルフォーマットであるShaderLabを使っているためエディタ上でシンタックスハイライトなどの恩恵を十分に受けられないという問題がある。

今回はなるべく双方のいいとこ取りをして、なるべく作業量が少なくなるようにする。具体的には、CustomFunctionノードを使って具体的な処理はコードで書けるようにしたうえで、ライティングなど今回カスタムしない部分はUnity標準に任せることでコードの記述量を減らす。またCustomFunctionは一般的な形式であるhlslをソースとして関数を定義できるので、ファイルフォーマットの問題も解決する。

https://docs.unity3d.com/ja/Packages/com.unity.shadergraph@10.0/manual/Custom-Function-Node.html

さて前置きが長くなったが、ここからが本題である。

Unityには新規hlslファイルを作成する機能がない。 ないものは作るしかない。ということで、カスタムのスクリプトテンプレートを追加することにする。

https://support.unity.com/hc/en-us/articles/210223733-How-to-customize-Unity-script-templates

スクリプトテンプレートについては上記の記事を参考にした。上記の記事ではスクリプトテンプレートのパスはC:\Program Files\Unity\Editor\Data\Resources\ScriptTemplatesとされているが、実際にはC:\Program Files\Unity\Hub\Editor\6000.0.55f1\Editor\Data\Resources\ScriptTemplatesだった。(Unity Hubが登場したことでEditorフォルダの位置が変わったのだろう)

今回はここに直接スクリプトテンプレートファイルを追加してしまう。ただ権限の関係なのか、エクスプローラー上でこのフォルダを開いて直接新規作成することはできなかった。しかし、適当な別フォルダで作成してから、コピーペーストすることでファイルを追加することに成功した。(要管理者権限)

ファイル名は、<優先度>-<メニューカテゴリ名>__<項目名>-<初期ファイル名>.txtといったフォーマットになっていそうである。私は上記のようにしてみた。ファイルの中身は必要ないので空にしてある。

これで、UnityのCreateメニューから空のhlslファイルを作成できるようになった。

ちなみに、スクリプトテンプレートについては以下の記事も参考になる。

https://tat1kun.hatenablog.com/entry/edit_template

今回は別のプロジェクトでも使えることを優先してプロジェクトごとのフォルダでなくEditorフォルダ内に直接追加した。

おわり

ここまでで準備は一段落とする。次回は実際にUnity上で生成したメッシュを動かすところまでやってみる。

→次回:

https://zenn.dev/kakilemon/articles/1047d0d04c1f00

Discussion