知識0からのUnityShader勉強

知識0からのUnityShader勉強

UnityのShaderをメインとして、0から学んでいくブログです。

【UnityShader】雪を降らせる【1】#38

前回の成果

雪を積もらせた。

soramamenatan.hatenablog.com


今回やること

雪を降らせます。

qiita.com


ソースコード

Script
using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Snow : MonoBehaviour {
    private const int SNOW_NUM = 16000;
    private Vector3[] _vertices;
    private int[] _triangles;
    private Vector2[] _uvs;

    private float _range;
    private float _rangeR;
    private Vector3 _move;

    void Start() {
        _range = 16.0f;
        _rangeR = 1.0f / _range;
        _vertices = new Vector3[SNOW_NUM * 4];

        for (int i = 0; i < SNOW_NUM; i++) {
            float x = Random.Range(-_range, _range);
            float y = Random.Range(-_range, _range);
            float z = Random.Range(-_range, _range);
            var point = new Vector3(x, y, z);
            _vertices[i * 4 + 0] = point;
            _vertices[i * 4 + 1] = point;
            _vertices[i * 4 + 2] = point;
            _vertices[i * 4 + 3] = point;
        }
        _triangles = new int[SNOW_NUM * 6];
        for (int i = 0; i < SNOW_NUM; i++) {
            _triangles[i * 6 + 0] = i * 4 + 0;
            _triangles[i * 6 + 1] = i * 4 + 1;
            _triangles[i * 6 + 2] = i * 4 + 2;
            _triangles[i * 6 + 3] = i * 4 + 2;
            _triangles[i * 6 + 4] = i * 4 + 1;
            _triangles[i * 6 + 5] = i * 4 + 3;
        }
        _uvs = new Vector2[SNOW_NUM * 4];
        for (int i = 0; i < SNOW_NUM; i++) {
            _uvs[i * 4 + 0] = new Vector2(0.0f, 0.0f);
            _uvs[i * 4 + 1] = new Vector2(1.0f, 0.0f);
            _uvs[i * 4 + 2] = new Vector2(0.0f, 1.0f);
            _uvs[i * 4 + 3] = new Vector2(1.0f, 1.0f);
        }

        Mesh mesh = new Mesh();
        mesh.name = "MeshSnowFlakes";
        mesh.vertices = _vertices;
        mesh.triangles = _triangles;
        mesh.uv = _uvs;
        mesh.bounds = new Bounds(Vector3.zero, Vector3.one * 99999999);
        var mf = GetComponent<MeshFilter>();
        mf.sharedMesh = mesh;
    }

    void LateUpdate() {
        var targetPosition = Camera.main.transform.TransformPoint(Vector3.forward * _range);
        var renderer = GetComponent<Renderer>();
        renderer.material.SetFloat("_Range", _range);
        renderer.material.SetFloat("_RangeR", _rangeR);
        renderer.material.SetFloat("_Size", 0.1f);
        renderer.material.SetVector("_MoveTotal", _move);
        renderer.material.SetVector("_CamUp", Camera.main.transform.up);
        renderer.material.SetVector("_TargetPosition", targetPosition);
        float x = (Mathf.PerlinNoise(0f, Time.time * 0.1f) - 0.5f) * 10f;
        float y = -2f;
        float z = (Mathf.PerlinNoise(Time.time*0.1f, 0f)-0.5f) * 10f;
        _move += new Vector3(x, y, z) * Time.deltaTime;
        _move.x = Mathf.Repeat(_move.x, _range * 2.0f);
        _move.y = Mathf.Repeat(_move.y, _range * 2.0f);
        _move.z = Mathf.Repeat(_move.z, _range * 2.0f);
    }
}
Shader
Shader "Unlit/fallSnow" {
    Properties {
        _MainTex("MainTex", 2D) = "white" {}
    }
    SubShader {
        Tags {
            "Queue" = "Transparent"
            "IgnoreProjector" = "True"
            "RenderType" = "Transparent"
        }
        ZWrite Off
        Cull Off
        Blend SrcAlpha OneMinusSrcAlpha

        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0
            #include "UnityCG.cginc"

            uniform sampler2D _MainTex;

            struct appdata_custom {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            float4x4 _PrevInvMatrix;
            float3 _TargetPosition;
            float _Range;
            float _RangeR;
            float _Size;
            float3 _MoveTotal;
            float3 _CamUp;

            v2f vert(appdata_custom v) {
                float3 target = _TargetPosition;
                float3 trip;
                float3 mv = v.vertex.xyz;
                mv += _MoveTotal;
                trip = floor(((target - mv) * _RangeR + 1) * 0.5f);
                trip *= (_Range * 2);
                mv += trip;

                float3 diff = _CamUp * _Size;
                float3 finalPosition;
                float3 tv0 = mv;
                tv0.x += sin(mv.x*0.2) * sin(mv.y*0.3) * sin(mv.x*0.9) * sin(mv.y*0.8);
                tv0.z += sin(mv.x*0.1) * sin(mv.y*0.2) * sin(mv.x*0.8) * sin(mv.y*1.2);

                float3 eyeVector = ObjSpaceViewDir(float4(tv0, 0));
                float3 sideVector = normalize(cross(eyeVector, diff));
                tv0 += (v.texcoord.x - 0.5f) * sideVector * _Size;
                tv0 += (v.texcoord.y - 0.5f) * diff;
                finalPosition = tv0;

                v2f o;
                o.pos = UnityObjectToClipPos(finalPosition);
                o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord);
                return o;
            }

            fixed4 frag(v2f i) :SV_TARGET {
                return tex2D(_MainTex, i.uv);
            }
            ENDCG
        }
    }
}

結構量が多いので、少しずつ理解していきます。
まずはScriptの解説からしていきます。


SNOW_NUM

private const int SNOW_NUM = 16000;

これが16,000なのには理由があります。
Unityでは1回のドローコールで64,000の頂点と64,000のindexを扱うことができます。
※ OpenGLESでは48,000のindex、macOSでは32,000のindex

そして、今回の降らせる雪は1つあたり4頂点なので、64,000に4を除算して16,000となります。


ドローコール

噛み砕くと、描画フローが呼ばれた回数のことです。
描画では、

  1. CPUからGPUにテクスチャ、マテリアル、座標などの値を渡す
  2. GPUを走らせて、ポリゴンをレンダリング
  3. CPUに処理を戻す

のフローを行っています。

wordpress.notargs.com


雪の位置

_range = 16.0f;
_rangeR = 1.0f / _range;
_vertices = new Vector3[SNOW_NUM * 4];

for (int i = 0; i < SNOW_NUM; i++) {
    float x = Random.Range(-_range, _range);
    float y = Random.Range(-_range, _range);
    float z = Random.Range(-_range, _range);
    var point = new Vector3(x, y, z);
    _vertices[i * 4 + 0] = point;
    _vertices[i * 4 + 1] = point;
    _vertices[i * 4 + 2] = point;
    _vertices[i * 4 + 3] = point;
}

この部分に関して説明していきます。


変数の説明

まずはここで使用している変数の意味です。

// 雪を降らせる範囲
_range = 16.0f;
// _rangeの逆数
_rangeR = 1.0f / _range;
// 雪の頂点
_vertices = new Vector3[SNOW_NUM * 4];

_range

赤枠で囲っているのが雪が降っている範囲です。

_rangeが5.0f

f:id:soramamenatan:20200125190742p:plain

_rangeが50.0f

f:id:soramamenatan:20200125190745p:plain

逆数

逆数とは、その値に対して乗算をすると1になる数のことです。
3の逆数は1/3ですし、16の逆数は1/16です。

乗算と除算では、圧倒的に乗算の方が速いので逆数にしています。

noiu.hatenablog.com


Random.Range

指定された範囲の乱数を返すメソッドです。
intとfloatで返す範囲が異なるので気をつけてください。

// 1.0以上 ~ 10.0以下の乱数
Random.Range(1.0f, 10.0f);
// 1以上 ~ 10未満の乱数
Random.Range(1, 10);


_vertices

var point = new Vector3(x, y, z);
_vertices[i * 4 + 0] = point;
_vertices[i * 4 + 1] = point;
_vertices[i * 4 + 2] = point;
_vertices[i * 4 + 3] = point;

これは頂点のことです。
下記の画像の0~3が各頂点で、この頂点を結ぶことにより今回の雪を描画することができます。

f:id:soramamenatan:20200202185649p:plain Unity 動的にメッシュを作成する ~まずは四角形だ編~ - おねむゲーマーの備忘録:より引用

一般的には、各頂点をズラすのですが今回はそれをshader側で行っているので、同じVector3の値を入れています。


各頂点を結ぶ

_triangles = new int[SNOW_NUM * 6];
for (int i = 0; i < SNOW_NUM; i++) {
    _triangles[i * 6 + 0] = i * 4 + 0;
    _triangles[i * 6 + 1] = i * 4 + 1;
    _triangles[i * 6 + 2] = i * 4 + 2;
    _triangles[i * 6 + 3] = i * 4 + 2;
    _triangles[i * 6 + 4] = i * 4 + 1;
    _triangles[i * 6 + 5] = i * 4 + 3;
}

頂点の位置を定義したら、次にその頂点をどのような順番で結ぶかを決めます。


四角形は、2つの三角形から描画されています。
下記のソースコードの一番最後に足している数字が、頂点の番号です。

// 1個目の三角形
_triangles[i * 6 + 0] = i * 4 + 0;
_triangles[i * 6 + 1] = i * 4 + 1;
_triangles[i * 6 + 2] = i * 4 + 2;
// 2個目の三角形
_triangles[i * 6 + 3] = i * 4 + 2;
_triangles[i * 6 + 4] = i * 4 + 1;
_triangles[i * 6 + 5] = i * 4 + 3;

注意してほしいことは、各頂点の順番は時計回りにしてください。
これを半時計周りにすると逆向きに描画されてしまいます。

f:id:soramamenatan:20200202191659p:plain

【Unity】四角形のポリゴンを作ってテクスチャを貼る - おもちゃラボ:より引用


UV値を決める

_uvs = new Vector2[SNOW_NUM * 4];
for (int i = 0; i < SNOW_NUM; i++) {
    _uvs[i * 4 + 0] = new Vector2(0.0f, 0.0f);
    _uvs[i * 4 + 1] = new Vector2(1.0f, 0.0f);
    _uvs[i * 4 + 2] = new Vector2(0.0f, 1.0f);
    _uvs[i * 4 + 3] = new Vector2(1.0f, 1.0f);
}

雪のTextureを貼るので、必要となってきます。


Meshに登録する

Mesh mesh = new Mesh();
mesh.name = "MeshSnowFlakes";
mesh.vertices = _vertices;
mesh.triangles = _triangles;
mesh.uv = _uvs;
mesh.bounds = new Bounds(Vector3.zero, Vector3.one * 99999999);
var mf = GetComponent<MeshFilter>();
mf.sharedMesh = mesh;

頂点の座標と順番、UV値を決めたのでそれを登録します。


name

Meshに名前をつけることができます。
InspecterのMeshFilterに表示されます。

f:id:soramamenatan:20200202193044p:plain


vertices, triangles, uv

先ほど制作した、頂点(vertices)、朝tんの順番(triangles)、UV値(uv)を登録しています。


bounds

これはMeshの大きさを表しています。

定義は以下のようになっています。

// 中央の位置、大きさ
Bounds(Vector3 center, Vector3 size)

sizeに大きな値を乗算しているのは、視錐台カリングを無効にするためです。


sharedMesh

これは共有のMeshです。
今回の場合は、MeshFilterに制作したMeshを渡しているだけです。


ソースコードにコメントを付与

using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Snow : MonoBehaviour {
    // ドローコールの限界/4
    private const int SNOW_NUM = 16000;
    private Vector3[] _vertices;
    private int[] _triangles;
    private Vector2[] _uvs;

    private float _range;
    private float _rangeR;
    private Vector3 _move;

    void Start() {
        // 雪を降らせる範囲
        _range = 16.0f;
        // _rangeの逆数
        _rangeR = 1.0f / _range;
        // 雪の頂点
        _vertices = new Vector3[SNOW_NUM * 4];
        // 頂点の位置
        for (int i = 0; i < SNOW_NUM; i++) {
            float x = Random.Range(-_range, _range);
            float y = Random.Range(-_range, _range);
            float z = Random.Range(-_range, _range);
            var point = new Vector3(x, y, z);
            _vertices[i * 4 + 0] = point;
            _vertices[i * 4 + 1] = point;
            _vertices[i * 4 + 2] = point;
            _vertices[i * 4 + 3] = point;
        }
        // 頂点を結ぶ順番
        _triangles = new int[SNOW_NUM * 6];
        for (int i = 0; i < SNOW_NUM; i++) {
        _triangles[i * 6 + 0] = i * 4 + 0;
        _triangles[i * 6 + 1] = i * 4 + 1;
        _triangles[i * 6 + 2] = i * 4 + 2;
        // ここで分割
        _triangles[i * 6 + 3] = i * 4 + 2;
        _triangles[i * 6 + 4] = i * 4 + 1;
        _triangles[i * 6 + 5] = i * 4 + 3;
        }
        // UV値
        _uvs = new Vector2[SNOW_NUM * 4];
        for (int i = 0; i < SNOW_NUM; i++) {
            _uvs[i * 4 + 0] = new Vector2(0.0f, 0.0f);
            _uvs[i * 4 + 1] = new Vector2(1.0f, 0.0f);
            _uvs[i * 4 + 2] = new Vector2(0.0f, 1.0f);
            _uvs[i * 4 + 3] = new Vector2(1.0f, 1.0f);
        }
        // Meshの生成
        Mesh mesh = new Mesh();
        mesh.name = "MeshSnowFlakes";
        mesh.vertices = _vertices;
        mesh.triangles = _triangles;
        mesh.uv = _uvs;
        // 視錐台カリングを無効化
        mesh.bounds = new Bounds(Vector3.zero, Vector3.one * 99999999);
        var mf = GetComponent<MeshFilter>();
        mf.sharedMesh = mesh;
    }

今回はここまでとなります。
ここまでご視聴ありがとうございました!