テッセレーションで草

環境

Unity2021.2.18f1

概要

metalがジオメトリシェーダに対応していないらしいので使わずに草を生やしてみます。

テッセレーションでは点から三角(もしくは四角)形を生成できるのでやってみました。

1チャンネルのテクスチャからMeshTopology.Pointsのメッシュを作成して、テッセレーションシェーダで草にしています。

距離で分割数を変えているので手前は割と分割されていますが、少し後ろに行くとただの四角形になります。

Shader "Custom/TessellationGrass" 
{
    Properties
    {
        _Color("Color", color) = (12, 173, 0, 1)
        _UnderColor("UnderColor", color) = (0, 0, 0, 1)
        _FlowTex("Flow Texture", 2D) = "white" {}
        _MinDist("Min Distance", Range(0.1, 100)) = 0.1
        _MaxDist("Max Distance", Range(0.1, 100)) = 2
        _TessFactor("Tessellation", Range(1, 5000)) = 10 //分割レベル
        _PositionScale("Position Scale", Range(0.1, 100)) = 10
        _Width("Width", Range(0.01, 0.5)) = 0.05
        _Height("Height", Range(0.1, 1)) = 0.5
        _FlowPower("FlowPower", Range(0.1, 1)) = 0.1
        _FlowSpeed("_FlowSpeed", Range(0.1, 1)) = 0.1
        [KeywordEnum(integer, fractional_even, fractional_odd, pow2)] _Partitioning("Partitioning", int) = 2
    }
    SubShader 
    {
        Tags
        {
            "RenderType" = "Opaque"
            "LightMode" = "ForwardBase"
        }
        Cull Off
        Pass 
        { 
            CGPROGRAM
            #include "Tessellation.cginc"
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            #pragma shader_feature _PARTITIONING_INTEGER _PARTITIONING_FRACTIONAL_EVEN _PARTITIONING_FRACTIONAL_ODD _PARTITIONING_POW2

            #pragma vertex vertex_shader
            #pragma hull hull_shader
            #pragma domain domain_shader
            #pragma fragment pixel_shader
 
            fixed4 _Color;
            fixed4 _UnderColor;
            sampler2D _FlowTex;
            float4 _FlowTex_ST;
            float _TessFactor;
            float _MinDist;
            float _MaxDist;
            float _PositionScale;
            float _Width;
            float _Height;
            float _FlowPower;
            float _FlowSpeed;

            struct appdata
            {
                float4 position : POSITION;
            };

            struct VS_OUTPUT
            {
                float4 position : POSITION;
                float2 flow : TEXCOORD0;
            };
 
            struct HS_CONSTANT_DATA_OUTPUT
            {
                float Edges[4] : SV_TessFactor;
                float Inside[2] : SV_InsideTessFactor;
            };
 
            struct HS_OUTPUT
            {
                float4 position : POS;
                float2 flow : TEXCOORD0;
                float2 basePosXZ : TEXCOORD1; 
            };

            struct DS_OUTPUT
            {
                float4 position : SV_Position;
                float4 color : Color;
            };

            VS_OUTPUT vertex_shader(appdata i)
            {
                VS_OUTPUT o;
                o.position = i.position;
                o.position.xyz *= _PositionScale;
                o.position.w = i.position.w / 255.0;
                float2 uv = frac(i.position.xz + 0.5/*/ _TexelSize.xy*/ + _Time.y * _FlowSpeed);
                o.flow = (tex2Dlod(_FlowTex, float4(uv, 0, 0)).xy * 2 - 1) * _FlowPower;
                return o;
            }
#define INPUT_PATCH_SIZE 1
#define OUTPUT_PATCH_SIZE 4
            HS_CONSTANT_DATA_OUTPUT constantsHS(InputPatch<VS_OUTPUT, INPUT_PATCH_SIZE> i)
            {
                float tessFactor = UnityCalcDistanceTessFactor(i[0].position, _MinDist, _MaxDist, _TessFactor);

                HS_CONSTANT_DATA_OUTPUT o;
                o.Edges[0] = 1;
                o.Edges[1] = tessFactor;
                o.Edges[2] = 1;
                o.Edges[3] = tessFactor;
                o.Inside[0] = tessFactor;
                o.Inside[1] = 1;
                return o;
            }
 
            [domain("quad")]
#if _PARTITIONING_INTEGER
            [partitioning("integer")]
#elif _PARTITIONING_FRACTIONAL_EVEN
            [partitioning("fractional_even")]
#elif _PARTITIONING_FRACTIONAL_ODD
            [partitioning("fractional_odd")]
#else//elif _PARTITIONING_POW2
            [partitioning("pow2")]
#endif
            [outputtopology("triangle_cw")]
            [outputcontrolpoints(OUTPUT_PATCH_SIZE)]
            [patchconstantfunc("constantsHS")]
            
            HS_OUTPUT hull_shader(InputPatch<VS_OUTPUT, INPUT_PATCH_SIZE>i, uint id: SV_OutputControlPointID, uint patchID: SV_PrimitiveID)
            {
                HS_OUTPUT o;
                uint loopIndex = (id / INPUT_PATCH_SIZE);
                uint inIndex = id % INPUT_PATCH_SIZE;
                uint index = loopIndex * INPUT_PATCH_SIZE + inIndex;
                float widthH = _Width * 0.5;
                float3 offsets[] = {float3(-widthH,0,0), float3(-widthH,_Height,0), float3(widthH,0,0), float3(widthH, _Height, 0)};
                o.position = float4(i[inIndex].position.xyz + offsets[index], i[inIndex].position.w);
                o.position.y *= i[inIndex].position.w;
                o.basePosXZ = i[inIndex].position.xz;
                o.flow = i[inIndex].flow;
                return o;
            }
 
            [domain("quad")]
            DS_OUTPUT domain_shader(HS_CONSTANT_DATA_OUTPUT iConst, const OutputPatch<HS_OUTPUT, OUTPUT_PATCH_SIZE> i, float2 bary: SV_DomainLocation)
            {
                DS_OUTPUT o;
                float3 pa = lerp(i[0].position.xyz, i[1].position.xyz, bary.x);
                float3 pb = lerp(i[2].position.xyz, i[3].position.xyz, bary.x);
                float3 pos = lerp(pa, pb, bary.y);

                float height = _Height * i[0].position.w;
                float minH = height * 0.3;
                float maxH = height;
                float r = saturate((pos.y - minH) / (maxH - minH));
                float fr = sin(saturate(pos.y / height) * UNITY_HALF_PI);
                pos.xz = i[0].basePosXZ + (pos.xz - i[0].basePosXZ) * (1.0 - r) + i[0].flow * fr;

                o.position = UnityObjectToClipPos(float4(pos, 1.0));

                float cminH = height * 0;
                float cmaxH = height * 1;
                float cr = saturate((pos.y - cminH) / (cmaxH - cminH));
                o.color = lerp(_UnderColor, _Color, cr);
                return o;
            }

            fixed4 pixel_shader(DS_OUTPUT i) : SV_TARGET
            {
                return i.color;
            }
 
            ENDCG
        }
    }
}
using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
using UnityEngine.Rendering;

public class Texture2DToPointCloudedMesh : MonoBehaviour
{
    [SerializeField] private Texture2D _texture = null;

    [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
    struct Vertex
    {
        public Vector4 Position;
    }
    private unsafe void OnEnable()
    {
        NativeArray<byte> bytes = _texture.GetPixelData<byte>(0);
        var pBytes = (byte*)NativeArrayUnsafeUtility.GetUnsafePtr(bytes);

        var mesh = new Mesh();
        var layout = new[]
        {
            new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 4),
        };
        var vertexCount = _texture.width * _texture.height;
        mesh.SetVertexBufferParams(vertexCount, layout);

        var verts = new NativeArray<Vertex>(vertexCount, Allocator.Temp);
        var pVerts = (Vertex*)NativeArrayUnsafeUtility.GetUnsafePtr(verts);
        var indices = new NativeArray<UInt16>(vertexCount, Allocator.Temp);
        var pIndices = (UInt16*)NativeArrayUnsafeUtility.GetUnsafePtr(indices);
        for(int i = 0; i < vertexCount; i++)
        {
            var x = -0.5f + (float)(i % _texture.width) / (float)_texture.width;
            var z = -0.5f + (float)(i / _texture.width) / (float)_texture.height;
            pVerts[i].Position = new Vector4(x, 0, z, pBytes[i]);
            pIndices[i] = (UInt16)i;
        }
        mesh.SetVertexBufferData(verts, 0, 0, vertexCount);
        mesh.SetIndexBufferParams(vertexCount, IndexFormat.UInt16);
        mesh.SetIndices<UInt16>(indices, MeshTopology.Points, 0);

        mesh.bounds = new Bounds(Vector3.zero, new Vector3(1000, 1000, 1000));

        var meshFilter = gameObject.GetComponent<MeshFilter>();
        meshFilter.sharedMesh = mesh;
    }
}

感想

・QuadじゃなくてTriangleで良かったです。

・今回はMeshにしましたが、頂点データが無駄です。

参考

テッセレーション