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

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

每天進步一點點!!!

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