Unity Shader - Noise 噪点图 - 实现简单山脉

141 篇文章 31 订阅
74 篇文章 4 订阅

学习记录一下噪点应用

噪点相关知识,可以看文章最下面的:References

后面有基于这篇文章重构过:Unity Shader - 简单山脉 - 顶点着色器重构法线

运行效果

在这里插入图片描述
在这里插入图片描述

噪点图

可以写了个C# 噪点图生产器,学习用,且方便生成噪点图
在这里插入图片描述

CSharp Code

主要是根据材质中的"NoiseTex"纹理来生成山脉网格的脚本,注释写的比较清楚
本来是想在shader 的vs阶段采样纹理做为顶点高度偏移处理,就可以实时的处理高度
但是,试过会有shader编译错误,所以我就放在csharp脚本来处理

我看openGL的资料说是可以在sm3.0以上就可以在VS采样纹理了,但结果还是不行:#pragma target 5.0都不行

下面脚本中的参数:rows, cols注意,分别都不要超过250,否则会有显示问题,原因不明,可能是因为单个mesh的顶点数太多,Unity底层限制单次传输的数量吗?

// jave.lin 2019.08.22
using UnityEngine;

public class CreateMountainPlane : MonoBehaviour
{
    public Material mat;
    // 不知道为哈:cols>300 && rows>300(反正超过一定数量),就会显示不正常,不会报错,不知道什么原因
    public int cols = 250;
    public int rows = 250;
    public float uvScale = 1;                               // 纹理坐标缩放
    public float xz_gaps = 5f;
    public float height_scale = 50f;

    public Texture2D noiseTex;
    public bool brightnessSaturate;                         // 亮度饱和调整,将亮度的[n~m]调整到[0,1]==>([n,m]-n)*1f/m == [0,1]

    private MeshFilter mf;
    private MeshRenderer mr;

    void Start()
    {
        noiseTex = noiseTex == null ? mat.GetTexture("_NoiseTex") as Texture2D : noiseTex;
        if (brightnessSaturate)
        {
            var min = 1f;
            var max = 0f;
            // 先得到最低、最高亮度
            for (int i = 0; i < noiseTex.width; i++)
            {
                for (int j = 0; j < noiseTex.height; j++)
                {
                    var v = noiseTex.GetPixel(i, j).r;
                    if (v < min) min = v;
                    if (v > max) max = v;
                }
            }

            if (max > 0)
            {
                // 将亮度的[n~m]调整到[0,1]==>([n,m]-n)*1f/(m-n) == [0,1]
                var scale = 1f / (max - min);
                for (int i = 0; i < noiseTex.width; i++)
                {
                    for (int j = 0; j < noiseTex.height; j++)
                    {
                        var v = noiseTex.GetPixel(i, j).r;
                        v -= min;
                        v *= scale;
                        noiseTex.SetPixel(i, j, new Color(v, v, v));
                    }
                }
                noiseTex.Apply(true, false);
            }
        }

        mf = gameObject.GetComponent<MeshFilter>();
        if (mf == null) mf = gameObject.AddComponent<MeshFilter>();

        mr = gameObject.GetComponent<MeshRenderer>();
        if (mr == null) mr = gameObject.AddComponent<MeshRenderer>();

        mr.sharedMaterial = mat;
        mf.sharedMesh = CreateMesh(cols, rows);
    }

    private Mesh CreateMesh(int cs, int rs)
    {
        var result = new Mesh();
        var quad_count = cs * rs;                           // 正方形数量
        var triangle_count = quad_count * 2;                // 三角形数量
        var vertex_rows = rs + 1;                           // 行数
        var vertex_cols = cs + 1;                           // 列数
        var vertices_count = vertex_rows * vertex_cols;     // 顶点数
        var vertices = new Vector3[vertices_count];         // 顶点数组
        var uvs = new Vector2[vertices_count];              // uv数组

        var indices_count = triangle_count * 3;             // 索引数量
        var indices = new int[indices_count];               // 三角索引

        for (int rIdx = 0; rIdx < vertex_rows; rIdx++)
        {
            for (int cIdx = 0; cIdx < vertex_cols; cIdx++)
            {
                var idx = rIdx * vertex_cols + cIdx;
                var u = (float)cIdx / cs * uvScale;
                var v = (float)rIdx / rs * uvScale;
                uvs[idx] = new Vector2(u, v);
                // 本想在vertex shader中来调整偏移的,但是,不知道unity中如何提高sm版本,因为使用#pragma target 5.0也不能在vs在采样纹理
                // 所以我就在csharp脚本中来处理了
                var height = noiseTex.GetPixelBilinear(u, v).r * height_scale;
                vertices[idx] = new Vector3(cIdx * xz_gaps, height, rIdx * xz_gaps);
                //Debug.Log($"x:{rIdx}, y:{cIdx}, u:{u}, v:{v}, height:{height}, vertices[{idx}]:{vertices[idx]}");
            }
        }

        // 设置三角索引
        var idx1 = 0;
        for (int rIdx = 0; rIdx < rs; rIdx++)
        {
            for (int cIdx = 0; cIdx < cs; cIdx++)
            {
                /*
                 * 我们下面是逐个Quad来设置,不使用strip
                 * 所以每个Quad需要6个顶点(索引)
               2______3     上一行
                |   /|
                |  / |
                | /  |
                |/   |
               0------1     本行
                 * */
                var t0 = (rIdx + 1) * vertex_cols + cIdx;           // idx:2, 上一行的第col index个,先偏移(rIdx + 1)*vertex_cols个(每行vertex_cols个,所以需要乘以vertex_cols)
                var t1 = (rIdx + 1) * vertex_cols + cIdx + 1;       // idx:3, 上一行的第col index + 1个
                var t2 = rIdx * vertex_cols + cIdx;                 // idx:0, 本行第col index个

                var t3 = (rIdx + 1) * vertex_cols + cIdx + 1;       // idx:3, 上一行的第col index + 1个
                var t4 = rIdx * vertex_cols + cIdx + 1;             // idx:1, 本行第col index + 1个
                var t5 = rIdx * vertex_cols + cIdx;                 // idx:0, 本行第col index个

                // unity正面是clock(顺时方向),所以索引组合要注意顺序
                indices[idx1++] = t0;                               // idx:2
                indices[idx1++] = t1;                               // idx:3
                indices[idx1++] = t2;                               // idx:0
                indices[idx1++] = t3;                               // idx:3
                indices[idx1++] = t4;                               // idx:1
                indices[idx1++] = t5;                               // idx:0
            }
        }

        result.vertices = vertices;
        result.triangles = indices;
        result.uv = uvs;
        result.RecalculateNormals();                                // 重新计算法线
        result.RecalculateTangents();                               // 重新计算切线
        result.RecalculateBounds();                                 // 重新计算AABB

        result.UploadMeshData(true);                                // 上传到GPU RAM,并销毁CPU RAM数据

        return result;
    }
}

生成网格效果

在这里插入图片描述

200x200的网格

4W+个顶点,8W个三角面
在这里插入图片描述

Shader Code

山脉的shader

// jave.lin 2019.08.22
Shader "Test/NoiseMountain" {
    Properties {
        _NoiseTex ("NoiseTex", 2D) = "white" {}                                                     // 主要影响草地、岩石、峰谷雪地的噪点数据
        [NoScaleOffset] _NoiseTex_Blend ("NoiseTex_Blend", 2D) = "white" {}                         // 用于混合影响_NoiseTex的另一张噪点数据,可以不设置,就不会有影响
        _NoiseTex_Blend_Intensity ("NoiseTex_Blend_Intensity", Range(0,1)) = 1                      // 用于设置_NoiseTex_Blend_Intensity对_NoiseTex影响的强度,0:完全不影响,如同没设置一样,1:完全影响,即:完全取代_NoiseTex;其实就插值过渡
        _Noise1_GrassHasRock_Tex ("Noise1_GrassHasRock_Tex", 2D) = "black" {}                       // 用于混合_NoiseTex中,属于Grass草地那段数据范围,可然山谷出出现岩石纹理的混合
        _Noise1_GrassHasRock_Tex_Intensity ("Noise1_GrassHasRock_Tex_Intensity", Range(0,10)) = 1   // _Noise1_GrassHasRock_Tex 草地中混合岩石的强度
        _GrassTex ("GrassTex", 2D) = "white" {}                                                     // 草地纹理
        _RockTex ("RockTex", 2D) = "white" {}                                                       // 岩石纹理
        _PeakColor ("_PeakColor", Color) = (0.95,0.95,1,1)                                          // 山峰色调
        _Valley ("Valley", Range(0,1)) = 0.3                                                        // 山谷划分界限
        _Peak ("Peak", Range(0,1)) = 0.85                                                           // 山峰划分界限
        _GradientThresold ("GradientThresold", Range(0,1)) = 0.1                                    // 山谷、山腰、山峰的过渡幅度阈值
        _Gradient ("Gradient", Range(0,1)) = 1                                                      // 过渡强度,0:完全硬生生的,没有渐变过渡,1:完全渐变过渡,一般0.5就好
        _SpecularGlossy ("SpecularGlossy", Range(0, 1)) = 0.16                                      // 整体山脉高光反射平滑度,山谷、山腰、山峰,分别在fs中有硬编码的反射强度系数控制,当然也可以在properties中公开设置
        _SpecularIntensity ("SpecularIntensity", Range(0, 1)) = 0.5                                 // 整体山脉高度反射强度
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass {
            CGPROGRAM
            #pragma target 5.0
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"
            #include "UnityCG.cginc"
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
            };
            struct v2f {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : TEXCOORD1;
                float3 wPos : TEXCOORD2;
                float4 valley_peak_uv : TEXCOORD3;          // 山谷、山峰的纹理UV
                float4 grass_has_rock_uv : TEXCOORD4;       // 草地混合岩石的纹理UV
            };
            sampler2D _NoiseTex;
            sampler2D _NoiseTex_Blend;
            fixed _NoiseTex_Blend_Intensity;
            float4 _NoiseTex_ST;
            sampler2D _Noise1_GrassHasRock_Tex;
            float4 _Noise1_GrassHasRock_Tex_ST;
            fixed _Noise1_GrassHasRock_Tex_Intensity;
            sampler2D _GrassTex;
            float4 _GrassTex_ST;
            sampler2D _RockTex;
            float4 _RockTex_ST;
            fixed4 _PeakColor;
            float _Valley;
            float _Peak;
            float _Gradient;
            float _GradientThresold;
            fixed _SpecularGlossy;
            fixed _SpecularIntensity;
            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _NoiseTex);
                o.valley_peak_uv = fixed4(
                    TRANSFORM_TEX(v.uv, _GrassTex),
                    TRANSFORM_TEX(v.uv, _RockTex));
                o.grass_has_rock_uv = fixed4(
                    TRANSFORM_TEX(v.uv, _Noise1_GrassHasRock_Tex), 0, 0);
                o.normal = UnityObjectToWorldNormal(v.normal);
                o.wPos = mul(unity_ObjectToWorld, v.vertex);
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                // sample the texture
                const fixed grassSpecScale = 0.3;           // 草地高光强度系数
                const fixed rockSpecScale = 0.75;           // 岩石高光强度系数
                const fixed peakSpecScale = 1;              // 山峰高光强度系数
                fixed4 col = tex2D(_NoiseTex, i.uv) *       // 混合_NoiseTex与_NoiseTex_Blend,根据:_NoiseTex_Blend_Itensity
                    lerp(fixed4(1,1,1,1), tex2D(_NoiseTex_Blend, i.uv), _NoiseTex_Blend_Intensity);
                fixed specularScale = peakSpecScale;
                float d = 0;
                float t = 0;
                float n = col.r;
                fixed4 rockCol = tex2D(_RockTex, i.valley_peak_uv.zw);
                fixed4 pColor = lerp(rockCol, _PeakColor, _PeakColor.a);
                if (n <= _Valley) {                         // 小于山谷高度的处理
                    d = _Valley - n;                        // 与山谷界限处的距离
                    t = saturate(d / _GradientThresold * _Gradient);        // 根据距离与阈值作为插值的百分比系数
                    col = lerp(rockCol, tex2D(_GrassTex, i.valley_peak_uv.xy), t * _Noise1_GrassHasRock_Tex_Intensity); // 岩石与草地的插值
                    specularScale = lerp(rockSpecScale,grassSpecScale,t);   // 岩石与草地高光系数过渡
                    fixed4 noise1 = tex2D(_Noise1_GrassHasRock_Tex, i.grass_has_rock_uv.xy);    // 岩石与草地的纹理混合噪点过渡
                    col = lerp(col, rockCol, pow(noise1.r, _Noise1_GrassHasRock_Tex_Intensity));
                } else if (n > _Valley && n < _Peak) {      // 大于山谷、小于山峰高度处理
                    col = rockCol;
                    specularScale = rockSpecScale;
                } else {                                    // 山峰处理
                    d = n - _Peak;
                    t = d / _GradientThresold;
                    specularScale = lerp(rockSpecScale,peakSpecScale,t);    // 岩石与山峰高光系数插值过渡
                    col = lerp(rockCol, pColor, t);         // 岩石与山峰颜色插值过渡
                }

                // diffuse
                half3 L = normalize(_WorldSpaceLightPos0.xyz);
                half3 N = normalize(i.normal);
                half LdotN = dot(L, N);// * 0.5 + 0.5;
                fixed3 diffuse = col.rgb * LdotN;

                // specular
                half3 specular = 0;
                half3 V = normalize(_WorldSpaceCameraPos.xyz - i.wPos);
                half3 H = normalize(L + V);
                if (LdotN > 0) {
                    half HdotN = max(0, dot(H, N)); // blinn-phone
                    specular = _LightColor0.rgb * pow(HdotN, _SpecularGlossy * 100) * _SpecularIntensity;
                    specular *= specularScale;
                }

                // ambient
                half3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
                
                return fixed4(diffuse + specular + ambient, 1);
            }
            ENDCG
        }
    }
}

Project

  • Unity project
    TestShaderToy_NoiseMountain 提取码: i2gj
    打开场景:NoiseMoutain.unity

  • NoiseTesting C# 项目后面再上传,还在添加其他的噪点测试内容

References

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值