Unity&Shader案例篇—绘制雪花

原文:http://www.manew.com/thread-98360-1-1.html

一、前言

之前有个案例讲到了Unity&Shader案例篇—绘制雨滴,没有看过的童鞋可以在回去看看,这一篇其实思路和绘制雨滴是一样的。首先,用代码C#生成顶点和面片,然后用Shader代码渲染,最后在用C#代码控制Shader的参数使得雪花飘起来。飘动的时候加点噪声处理,使得雪花的飘落更符合真实。

依然废话不多说先上效果图,切换不同的贴图可以得到不同的雪花:
这里写图片描述
这里写图片描述


二、制作步骤

1、C#代码生成顶点和面片:首先要了解生成顶点和网格面片,下面这个代码就是有三个顶点画一个三角形

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(MeshFilter),typeof(MeshRenderer))]
public class MeshTriangle : MonoBehaviour {

    private Mesh mesh;
        // Use this for initialization
        void Start () {

        mesh = GetComponent<MeshFilter>().mesh;
        mesh.Clear();
        mesh.vertices = new Vector3[] { Vector3.zero, new Vector3(0, 1, 0), new Vector3(1, 1, 0) };
        mesh.uv = new Vector2[] { Vector2.zero, Vector2.zero, Vector2.zero };
        mesh.triangles = new int[] { 0, 1, 2};
        }

        // Update is called once per frame
        void Update () {

        }
}

将这个代码随便赋给一个空的GameObject就可以得到如图所示的三角形,代码其实很简单,就是在空间中先定义三个顶点,然后贴图部分因为这里
这里写图片描述

加顶点和三角点的连线得到你想要的网格面片,修改代码如下会得到如下图所示的效果图

mesh.vertices = new Vector3[] { Vector3.zero, new Vector3(0, 1, 0), new Vector3(1, 1, 0),new Vector3(1,0,0) };
mesh.uv = new Vector2[] { Vector2.zero, Vector2.zero, Vector2.zero, Vector2.zero };
mesh.triangles = new int[] { 0, 1, 2,0,2,3 };

这里写图片描述

好了,有了绘制网格的基础,那么接下来开始进入主题。我们当然不需要让C#代码来给我们将雪花的样子的面片画出来了,其实只要画一个向上面

的正方形块就好了,然后通过Shader将一个雪花的贴图渲到正方形网格上九OK了。首先,生成顶点和面片,然后在Update函数里给Shader传递运动

参数,完整的代码如下:

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(MeshFilter),typeof(MeshRenderer))]
public class Snow : MonoBehaviour
{
        //Unity可以支持多达64000个顶点,如果一个雪花有4个顶点组成,则最多有16000个雪花
        const int SNOW_NUM = 16000;
        //顶点
        private Vector3[] m_vertices;
        //顶点构成的三角面
        private int[] triangles_;
        //雪花网格的贴图
        private Vector2[] uvs_;
        //雪花的范围
        private float range;
        //雪花范围的倒数,为了提高计算效率
        private float rangeR_;
        private Vector3 move_ = Vector3.zero;

        void Start ()
        {
                range = 16f;
                rangeR_ = 1.0f/range;
                m_vertices = new Vector3[SNOW_NUM*4];
                for (var 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);
                        m_vertices [i*4+0] = point;
                        m_vertices [i*4+1] = point;
                        m_vertices [i*4+2] = point;
                        m_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 (var i = 0; i < SNOW_NUM; ++i) {
                        uvs_ [i*4+0] = new Vector2 (0f, 0f);
                        uvs_ [i*4+1] = new Vector2 (1f, 0f);
                        uvs_ [i*4+2] = new Vector2 (0f, 1f);
                        uvs_ [i*4+3] = new Vector2 (1f, 1f);
                }
                Mesh mesh = new Mesh ();
                mesh.name = "MeshSnowFlakes";
                mesh.vertices = m_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 target_position = Camera.main.transform.TransformPoint(Vector3.forward * range);
                var mr = GetComponent<Renderer> ();
                mr.material.SetFloat("_Range", range);
                mr.material.SetFloat("_RangeR", rangeR_);
                mr.material.SetFloat("_Size", 0.1f);
                mr.material.SetVector("_MoveTotal", move_);
                mr.material.SetVector("_CamUp", Camera.main.transform.up);
                mr.material.SetVector("_TargetPosition", target_position);
                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 * 2f);
                move_.y = Mathf.Repeat(move_.y, range * 2f);
                move_.z = Mathf.Repeat(move_.z, range * 2f);
        }
}

2、Shader部分:这一部分做的工作不仅仅是将顶点生成的面片用贴图去渲染,其实还包括让雪花始终朝着摄像机方向

//从给定的局部座标到摄像机座标进行转换,目的是让顶点始终朝向摄像机
float3 eyeVector = ObjSpaceViewDir(float4(tv0, 0));
//float3 eyeVector = mv;
float3 sideVector = normalize(cross(eyeVector, diff));

雪花的生成范围始终是从摄像机的正上方落下,这个在C#代码部分通过将摄像机的正方向传递给Shader实现

mr.material.SetVector("_CamUp", Camera.main.transform.up);

Shader代码部分:

//让顶点始终保持在摄像机的正上方位置
float3 diff = _CamUp * _Size;
float3 finalposition;
float3 tv0 = mv;

当然还有运动部分:

float3 mv = v.vertex.xyz;
mv += _MoveTotal;
//顶点分布的区域应该是-_Range到_Range,因此target-mv的范围应该也是这个,因此此处的trip值的范围为,0~1,计算的最终目的还是为了让雪花始终在摄像机的正前方
trip = floor(((target - mv)*_RangeR + 1) * 0.5);
//经过前面的座标系的换算再次将范围扩大到2个_Range范围
trip *= (_Range * 2);
mv += trip;

完整的Shader代码如下:

Shader "Custom/snow" {
    Properties {
                _MainTex ("Base (RGB)", 2D) = "white" {}
    }
        SubShader {
                   Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
                ZWrite Off
                Cull Off
                // alpha blending
                //float4 result = fragment_output.aaaa * fragment_output + (float4(1.0, 1.0, 1.0, 1.0) - fragment_output.aaaa) * pixel_color;
                //用前一个队列的输出的Alpha通道作为不透明度
                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)
                        {
                                //摄像机正前方距离为Range的位置
                                float3 target = _TargetPosition;
                                float3 trip;
                                float3 mv = v.vertex.xyz;
                                mv += _MoveTotal;
                                //顶点分布的区域应该是-_Range到_Range,因此target-mv的范围应该也是这个,因此此处的trip值的范围为,0~1,计算的最终目的还是为了让雪花始终在摄像机的正前方
                                trip = floor(((target - mv)*_RangeR + 1) * 0.5);
                                //经过前面的座标系的换算再次将范围扩大到2个_Range范围
                                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 eyeVector = mv;
                                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 = mul(UNITY_MATRIX_MVP, float4(finalposition, 1));
                                o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord);
                                return o;
                        }

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

            ENDCG
        }
    }
}

三、尾语

总结来说这种方式实现的大量粒子性的效果要比直接使用粒子系统在性能上的靠小要少很多,我在调试模式下的运行参数如图所示,总之,是一个
这里写图片描述
实用且有效的Shader案例。

为了方便大家学习参考,附上百度工程文件如下:
http://pan.baidu.com/s/1i44IER7

好了,感谢原作者的分享。

每天进步一点点!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章