unity天气系统_天气系统(一)

知乎视频​www.zhihu.com

https://medium.com/@andresgomezjr89/rain-snow-with-geometry-shaders-in-unity-83a757b767c1

接下来我们尝试添加天气系统。主要是雨滴和雪花下落。

我们可以使用几何着色器来批量创建粒子,可以减少从CPU送往GPU顶点数据的瓶颈问题,让大多数的工作在GPU中完成。我们可以在一个绘制命令中批处理所有的雨滴和雪花。

我们首先编写一个移动摄像机的脚本来帮助我们观察场景:

 using UnityEngine;
 ​
 public class FreeCamera : MonoBehaviour
 {
     public float moveSpeed = 5;
     public float moveSpeedUp = 20;
     public float turnSpeed = 75;
     
     float GetAxis (KeyCode pos, KeyCode neg) {
         float r = 0;
         if (Input.GetKey(pos)) r += 1;
         if (Input.GetKey(neg)) r -= 1;
         return r;
     }
 ​
     float ClampAngle(float angle, float min, float max) {
         // remap from [0, 360] to [-180, 180]
         return Mathf.Clamp(((angle + 180f) % 360f - 180f), min, max);
     }
 ​
     void Update () {
         // handle rotation
         float tSpeed = turnSpeed * Time.deltaTime;
         Vector3 angles = transform.rotation.eulerAngles;
         
         // clamp up down look, so we cant do somersaults
         angles.x = ClampAngle(angles.x + GetAxis (KeyCode.UpArrow, KeyCode.DownArrow) * tSpeed, -89, 89);
         angles.y += GetAxis (KeyCode.RightArrow, KeyCode.LeftArrow) * tSpeed;
         angles.z = 0;
         transform.rotation = Quaternion.Euler(angles);
         
         // handle movmeent
         Vector3 side = transform.right * GetAxis (KeyCode.D, KeyCode.A);
         Vector3 upDown = transform.up * GetAxis (KeyCode.E, KeyCode.Q);
         Vector3 fwd = transform.forward * GetAxis (KeyCode.W, KeyCode.S);
         
         float mSpeed = (Input.GetKey(KeyCode.LeftShift) ? moveSpeedUp : moveSpeed) * Time.deltaTime;
         transform.position += (side + upDown + fwd) * mSpeed;
     }
 }

将脚本附在摄像机上就可以随意移动。

接下来实现网格系统。为了节省性能消耗,我们可以只渲染摄像机一定范围内的效果。这样的话随摄像机移动,效果也会随之移动渲染。但这会有问题,当摄像机移动时,我们可能会在某些位置看到雨雪分界线。

我们可以在世界空间完成一个网格,其中只有网格坐标内的区域会渲染雨滴:

unity天气系统_天气系统(一)_第1张图片

不过在玩家到达边界时会发生问题:

unity天气系统_天气系统(一)_第2张图片

此时玩家会看到明显的雨雪分界线。

为了修复这一点我们可以向每个方向扩大一个网格,在下面的​网格渲染效果,这样的话就不会得到边界效果:

unity天气系统_天气系统(一)_第3张图片

这样在世界空间中,我们会为摄像机的活动范围设置一个​的网格立方体。我们可以定义一个脚本来显示这一网格范围,帮助我们观察:

 using UnityEngine;
 using System;
 ​
 [ExecuteInEditMode] public class GridHandler : MonoBehaviour
 {
     [Tooltip("How large (in meters) one grid block side is")]
     public float gridSize = 10f;
     
     [Tooltip("The player's transform to track")]
     public Transform playerTransform;
     
     // a callback to subscribe to when the player grid changes
     public event Action onPlayerGridChange;
     
     Vector3Int lastPlayerGrid = new Vector3Int(-99999,-99999,-99999);
     
     // Update runs once per frame.
     void Update () {
         if (playerTransform == null) {
             Debug.LogWarning("Grid Handler Has No Player Transform!");
             return;
         }
       
         // calculate the grid coordinate where the player currently is
         Vector3 playerPos = playerTransform.position;
         Vector3Int playerGrid = new Vector3Int(
             Mathf.FloorToInt(playerPos.x / gridSize),
             Mathf.FloorToInt(playerPos.y / gridSize),
             Mathf.FloorToInt(playerPos.z / gridSize)
         );
       
         // check if the player changed grid coordinates since the last check
         if (playerGrid != lastPlayerGrid) {
           
             // if it has, then broadcast the new grid coordinates
             // to whoever subscribed to the callback
             if (onPlayerGridChange != null)
                 onPlayerGridChange(playerGrid);
             
             lastPlayerGrid = playerGrid;
         }
     }
     
     // calculate the center position of a certain grid coordinate
     public Vector3 GetGridCenter(Vector3Int grid) {
         float halfGrid = gridSize * .5f;
         return new Vector3(
             grid.x * gridSize + halfGrid,
             grid.y * gridSize + halfGrid,
             grid.z * gridSize + halfGrid
         );
     }
     
     // draw gizmo cubes around teh grids where the player is
     // so we can see it in the scene view
     void OnDrawGizmos () {
         // loop in a 3 x 3 x 3 grid
         for (int x = -1; x <= 1; x++) {
             for (int y = -1; y <= 1; y++) {
                 for (int z = -1; z <= 1; z++) {
                   
                     bool isCenter = x == 0 && y == 0 && z == 0;
                     Vector3 gridCenter = GetGridCenter(lastPlayerGrid + new Vector3Int(x, y, z));
                     
                     // make the center one green and slightly smaller so it stands out visually
                     Gizmos.color = isCenter ? Color.green : Color.red;
                     Gizmos.DrawWireCube(gridCenter, Vector3.one * (gridSize * (isCenter ? .95f : 1.0f)));
                 }
             }
         }
     }
 }

创建一个空物体PrecipitationSystem,并将脚本赋予它,将摄像机赋予该脚本组件。

unity天气系统_天气系统(一)_第4张图片

接下来我们创建要渲染的网格:

 using System.Collections.Generic;
 using UnityEngine;
 using UnityEditor;
 ​
 [ExecuteInEditMode] public class PrecipitationManager : MonoBehaviour 
 {
     // 65536 (256 x 256) vertices is the max per mesh
     [Range(2, 256)] public int meshSubdivisions = 200;
 ​
     GridHandler gridHandler;
     Mesh meshToDraw;
 ​
     void OnEnable () {
         gridHandler = GetComponent();
         gridHandler.onPlayerGridChange += OnPlayerGridChange;
     }
 ​
     void OnDisable() {
         gridHandler.onPlayerGridChange -= OnPlayerGridChange;
     }
     
     void OnPlayerGridChange(Vector3Int playerGrid) {
           
     }
 ​
     void Update() {
         // update the mesh automatically if it doesnt exist
         if (meshToDraw == null)
             RebuildPrecipitationMesh();
     }
 ​
     // the mesh created has a 
     // center at [0,0], 
     // min at [-.5, -.5] 
     // max at [.5, .5]
     public void RebuildPrecipitationMesh() {
         Mesh mesh = new Mesh ();
         List indicies = new List();
         List vertices = new List();
         List uvs = new List();
             
         // use 0 - 100 range instead of 0 to 1
         // to avoid precision errors when subdivisions
         // are to high
         float f = 100f / meshSubdivisions;
         int i  = 0;
         for (float x = 0.0f; x <= 100f; x += f) {
             for (float y = 0.0f; y <= 100f; y += f) {
       
                 // normalize x and y to a value between 0 and 1
                 float x01 = x / 100.0f;
                 float y01 = y / 100.0f;
         
                 vertices.Add(new Vector3(x01 - .5f, 0, y01 - .5f));
         
                 uvs.Add(new Vector3(x01, y01, 0.0f));
         
                 indicies.Add(i++);
             }    
         }
             
         mesh.SetVertices(vertices);
         mesh.SetUVs(0,uvs);
         mesh.SetIndices(indicies.ToArray(), MeshTopology.Points, 0);
     
         // give a large bounds so it's always visible, we'll handle culling manually
         mesh.bounds = new Bounds(Vector3.zero, new Vector3(500, 500, 500));
     
         // dont save as an asset
         mesh.hideFlags = HideFlags.HideAndDontSave;
     
         meshToDraw = mesh;
     } 
 }
 ​
 #if UNITY_EDITOR
 // create a custom editor with a button
 // to trigger rebuilding of the render mesh
 [CustomEditor(typeof(PrecipitationManager))] 
 public class PrecipitationManagerEditor : Editor {
 ​
     public override void OnInspectorGUI() {
         base.OnInspectorGUI();
           
         if (GUILayout.Button("Rebuild Precipitation Mesh")) {
             (target as PrecipitationManager).RebuildPrecipitationMesh();
             // set dirty to make sure the editor updates
             EditorUtility.SetDirty(target);
         }
     }
 }

该脚本可以创建网格,在更新时可以使用按钮控制网格的生成。

注意UV是三维,第三个值用于其它元素的存储。

接下来我们进行渲染,雨雪的shader很简单:

 Shader "Snow" {
     Properties { }
     SubShader{
         Tags{ 
             "Queue" = "Transparent" 
             "RenderType" = "Transparent" 
             "IgnoreProjector" = "True" 
         }
         CULL FRONT
         Blend SrcAlpha OneMinusSrcAlpha
         ZWrite Off
         Pass {
             CGPROGRAM
             #pragma multi_compile_instancing
             #pragma fragmentoption ARB_precision_hint_fastest
             #pragma vertex vert
             #pragma fragment frag
             #pragma geometry geom
             #pragma target 4.0
             #include "Precipitation.cginc"  
             ENDCG
         }
     }
 }
 Shader "Rain" {
     Properties { }
     SubShader{
         Tags{ 
             "Queue" = "Transparent" 
             "RenderType" = "Transparent" 
             "IgnoreProjector" = "True" 
         }
         CULL OFF
         Blend SrcAlpha OneMinusSrcAlpha
         ZWrite Off
         Pass {
             CGPROGRAM
             #pragma multi_compile_instancing        
             #pragma fragmentoption ARB_precision_hint_fastest
             #pragma vertex vert
             #pragma fragment frag
             #pragma geometry geom
             #pragma target 4.0
             #define RAIN
             #include "Precipitation.cginc"       
             ENDCG
         }
     }
 }

我们把相关结构体和着色器放于头文件中:

 #include "UnityCG.cginc"
 ​
 float _GridSize;
 ​
 struct MeshData {
     float4 vertex : POSITION;
     float4 uv : TEXCOORD0;
     uint instanceID : SV_InstanceID;
 };
 ​
 // vertex shader, just pass along the mesh data to the geometry function
 MeshData vert(MeshData meshData) {
     return meshData; 
 }
 ​
 // structure that goes from the geometry shader to the fragment shader
 struct g2f {
     UNITY_POSITION(pos);
     float4 uv : TEXCOORD0; // uv.xy, opacity, color variation amount
     UNITY_VERTEX_OUTPUT_STEREO
 };
 ​
 void AddVertex (inout TriangleStream stream, float3 vertex, float2 uv, float colorVariation, float opacity) {      
     // initialize the struct with information that will go
     // form the vertex to the fragment shader
     g2f OUT;
 ​
     // unity specific
     UNITY_INITIALIZE_OUTPUT(g2f, OUT);
     UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
     OUT.pos = UnityObjectToClipPos(vertex);   
 ​
     // transfer the uv coordinates
     OUT.uv.xy = uv;    
 ​
     // we put `opacity` and `colorVariation` in the unused uv vector elements
     // this limits the amount of attributes we need going between the vertex
     // and fragment shaders, which is good for performance
     OUT.uv.z = opacity;
     OUT.uv.w = colorVariation;
 ​
     stream.Append(OUT);
 }
 ​
 void CreateQuad (inout TriangleStream stream, float3 bottomMiddle, float3 topMiddle, float3 perpDir, float colorVariation, float opacity) {    
     AddVertex (stream, bottomMiddle - perpDir, float2(0, 0), colorVariation, opacity);
     AddVertex (stream, bottomMiddle + perpDir, float2(1, 0), colorVariation, opacity);
     AddVertex (stream, topMiddle - perpDir, float2(0, 1), colorVariation, opacity);
     AddVertex (stream, topMiddle + perpDir, float2(1, 1), colorVariation, opacity);
     stream.RestartStrip();
 }
 ​
 /*
     this geom function actually builds the quad from each vertex in the
     mesh. so this function runs once for each "rain drop" or "snowflake"
 */
 #if defined(RAIN)
 [maxvertexcount(8)] // rain draws 2 quads
 #else
 [maxvertexcount(4)] // snow draws one quad that's billboarded towards the camera
 #endif
 void geom(point MeshData IN[1], inout TriangleStream stream) {    
 ​
     MeshData meshData = IN[0];
 ​
     UNITY_SETUP_INSTANCE_ID(meshData);
 ​
     // the position of the snowflake / raindrop
     float3 pos = meshData.vertex.xyz;
 ​
     // make sure the position is spread out across the entire grid, the original vertex position
     // is normalized to a plane in the -.5 to .5 range
     pos.xz *= _GridSize;
 ​
     // make sure the position originates from the top of the local grid
     pos.y += _GridSize * .5;
 ​
     float opacity = 1.0;
 ​
     // temporary values
     float colorVariation = 0;
     float2 quadSize = float2(.05, .05);
 ​
     float3 quadUpDirection = float3(0,0,1);
     float3 topMiddle = pos + quadUpDirection * quadSize.y;
     float3 rightDirection = float3(.5 * quadSize.x, 0, 0);
 ​
     CreateQuad (stream, pos, topMiddle, rightDirection, colorVariation, opacity);
 }
 ​
 float4 frag(g2f IN) : SV_Target {
     float4 color = float4(IN.uv.xy, 0, 1);
 ​
     return color;
 }
 ​

接下来修改PrecipitationManager脚本来可视化创建的网格:

 using System.Collections.Generic;
 using UnityEngine;
 using UnityEditor;
 ​
 // NEW =================================================
 using UnityEngine.Rendering;
 // NEW =================================================
 ​
 [ExecuteInEditMode] public class PrecipitationManager : MonoBehaviour 
 {
     [Range(2, 256)] public int meshSubdivisions = 200;
     GridHandler gridHandler;
     Mesh meshToDraw;
         
     // NEW =================================================
     Matrix4x4[] renderMatrices = new Matrix4x4[3 * 3 * 3];
 ​
     Material rainMaterial, snowMaterial;
     // automatic material creation
     static Material CreateMaterialIfNull(string shaderName, ref Material reference) {
         if (reference == null) {
             reference = new Material(Shader.Find(shaderName));
             reference.hideFlags = HideFlags.HideAndDontSave;
             reference.renderQueue = 3000;
             reference.enableInstancing = true;
         }
         return reference;
     }
     // NEW =================================================
 ​
     void OnEnable () {
         // [ UNCHANGED ]
     }
     void OnDisable() {
         // [ UNCHANGED ]
     }
     
     // NEW =================================================
     /*
         set all our render matrices to be positioned
         in a 3x3x3 grid around the player
     */
     void OnPlayerGridChange(Vector3Int playerGrid) {
 ​
         // index for each individual matrix
         int i = 0;
 ​
         // loop in a 3 x 3 x 3 grid
         for (int x = -1; x <= 1; x++) {
             for (int y = -1; y <= 1; y++) {
                 for (int z = -1; z <= 1; z++) {
 ​
                     Vector3Int neighborOffset = new Vector3Int(x, y, z);
                     
                     // adjust the rendering position matrix, leaving rotation and scale alone
                     renderMatrices[i++].SetTRS(
                         gridHandler.GetGridCenter(playerGrid + neighborOffset), 
                         Quaternion.identity, 
                         Vector3.one
                     );
                 }
             }
         }
     }
     // NEW =================================================
 ​
     void Update()
     {
         if (meshToDraw == null)
             RebuildPrecipitationMesh();
 ​
         // NEW =================================================
         // render the rain and snow
         RenderEnvironmentParticles(CreateMaterialIfNull("Hidden/Environment/Rain", ref rainMaterial));
         RenderEnvironmentParticles(CreateMaterialIfNull("Hidden/Environment/Snow", ref snowMaterial));
         // NEW =================================================
       
     }
     
     // NEW =================================================
     void RenderEnvironmentParticles(Material material) {
             
         material.SetFloat("_GridSize", gridHandler.gridSize);
      
         Graphics.DrawMeshInstanced(meshToDraw, 0, material, renderMatrices, renderMatrices.Length, null, ShadowCastingMode.Off, true, 0, null, LightProbeUsage.Off);
     }
     // NEW =================================================
 ​
     public void RebuildPrecipitationMesh() {
         // [ UNCHANGED ]
     } 
 }
 ​
 #if UNITY_EDITOR
 //[ CUSTOM EDITOR UNCHANGED ] 

我们实现OnPlayerGridChange函数,创建一个简单的函数来从着色器创建材质,并创建一个方法来渲染环境粒子(雨雪),在Update函数中使用Graphics.DrawMeshInstanced来绘制实例。

效果如下:

unity天气系统_天气系统(一)_第5张图片

我们不需要每时每刻渲染所有的四边形,因此我们可以基于粒子的数量、距摄像机的距离和是否在摄像机后来设置剔除。

首先更新脚本,包含一个内置设置类:

 using System.Collections.Generic;
 using UnityEngine;
 using UnityEditor;
 using UnityEngine.Rendering;
 ​
 [ExecuteInEditMode] public class PrecipitationManager : MonoBehaviour 
 {
     // NEW =================================================
     [System.Serializable] public class EnvironmentParticlesSettings
     {
         [Range(0, 1)] public float amount = 1.0f;
         public Color color = Color.white;
 ​
         [Tooltip("Alpha = variation amount")]
         public Color colorVariation = Color.white;
         public float fallSpeed;
         public Vector2 cameraRange; 
         public Vector2 flutterFrequency;
         public Vector2 flutterSpeed;
         public Vector2 flutterMagnitude;
         public Vector2 sizeRange;
         
         public EnvironmentParticlesSettings (Color color, Color colorVariation, float fallSpeed, Vector2 cameraRange, Vector2 flutterFrequency, Vector2 flutterSpeed, Vector2 flutterMagnitude, Vector2 sizeRange) {
             this.color = color;
             this.colorVariation = colorVariation;
             this.fallSpeed = fallSpeed;
             this.cameraRange = cameraRange;
             this.flutterFrequency = flutterFrequency;
             this.flutterSpeed = flutterSpeed;
             this.flutterMagnitude = flutterMagnitude;
             this.sizeRange = sizeRange;
         }
     }
     // NEW =================================================
     
     [Range(2, 256)] public int meshSubdivisions = 200;
 ​
     // NEW =================================================
     // populate the settings with some initial values
     public EnvironmentParticlesSettings rain = new EnvironmentParticlesSettings(
         Color.white, Color.white, 3,  // color, colorVariation, fall speed
         new Vector2(0,15), //camera range
         new Vector2(0.988f, 1.234f), //flutter frequency
         new Vector2(.01f, .01f), //flutter speed
         new Vector2(.35f, .25f), //flutter magnitude
         new Vector2(.5f, 1f)//, //size range 
     );
     
     public EnvironmentParticlesSettings snow = new EnvironmentParticlesSettings(    
         Color.white, Color.white, .25f,  // color, colorVariation, fall speed
         new Vector2(0,10), //camera range
         new Vector2(0.988f, 1.234f), //flutter frequency
         new Vector2(1f, .5f), //flutter speed
         new Vector2(.35f, .25f), //flutter magnitude
         new Vector2(.05f, .025f)//, //size range 
     );
     // NEW =================================================
     
     GridHandler gridHandler;
     Matrix4x4[] renderMatrices = new Matrix4x4[3 * 3 * 3];
     Mesh meshToDraw;
     Material rainMaterial, snowMaterial;
     static Material CreateMaterialIfNull(string shaderName, ref Material reference) {
         // [ UNCHANGED ]
     }
     void OnEnable () {
         // [ UNCHANGED ]
     }
     void OnDisable() {
         // [ UNCHANGED ]
     }
     void OnPlayerGridChange(Vector3Int playerGrid) {
         // [ UNCHANGED ]
     }
 ​
     void Update() {
         if (meshToDraw == null)
             RebuildPrecipitationMesh();
 ​
         // NEW =================================================
         // render the rain and snow
         RenderEnvironmentParticles(rain, CreateMaterialIfNull("Hidden/Environment/Rain", ref rainMaterial));
         RenderEnvironmentParticles(snow, CreateMaterialIfNull("Hidden/Environment/Snow", ref snowMaterial));
         // NEW =================================================  
     }
 ​
     void RenderEnvironmentParticles(EnvironmentParticlesSettings settings, Material material) {
         
         // NEW =================================================
         // if the amount is 0, dont render anything
         if (settings.amount <= 0)
             return;
         // NEW =================================================
 ​
         material.SetFloat("_GridSize", gridHandler.gridSize);
         
         // NEW =================================================
         material.SetFloat("_Amount", settings.amount);
         
         // send teh other variables which we'll use later
         material.SetColor("_Color", settings.color);
         material.SetColor("_ColorVariation", settings.colorVariation);
         material.SetFloat("_FallSpeed", settings.fallSpeed);
         material.SetVector("_FlutterFrequency", settings.flutterFrequency);
         material.SetVector("_FlutterSpeed", settings.flutterSpeed);
         material.SetVector("_FlutterMagnitude", settings.flutterMagnitude);
         material.SetVector("_CameraRange", settings.cameraRange);
         material.SetVector("_SizeRange", settings.sizeRange);
         // NEW =================================================
             
         Graphics.DrawMeshInstanced(meshToDraw, 0, material, renderMatrices, renderMatrices.Length, null, ShadowCastingMode.Off, true, 0, null, LightProbeUsage.Off);
     }
 ​
     public void RebuildPrecipitationMesh() {
         // [ UNCHANGED ]
     } 
 }
 ​
 #if UNITY_EDITOR

我们还改变了RenderEnvironmentParticles函数,增加一个设置对象,将数量值送往着色器,以及其它设置值。

我们可能希望基于数量变量来修改整体效果的透明度,但因为所有的雨滴会是渐变的,这样会看起来很奇怪。我们希望可以大雨滂沱,也可以小雨淅淅,为了实现这一点,我们可以在构建网格时,为每个顶点设置一个阈值,如果数量低于阈值,那么该顶点就不会渲染。阈值的计算基于网格中的顶点位置。

using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.Rendering;

[ExecuteInEditMode] public class PrecipitationManager : MonoBehaviour 
{
    [System.Serializable] public class EnvironmentParticlesSettings {
        // [ UNCHANGED ]
    }
    [Range(2, 256)] public int meshSubdivisions = 200;
    public EnvironmentParticlesSettings rain = new EnvironmentParticlesSettings(
        // [ UNCHANGED ] 
    );
    public EnvironmentParticlesSettings snow = new EnvironmentParticlesSettings(	
        // [ UNCHANGED ] 
    );
    GridHandler gridHandler;
    Matrix4x4[] renderMatrices = new Matrix4x4[3 * 3 * 3];
    Mesh meshToDraw;
    Material rainMaterial, snowMaterial;
    static Material CreateMaterialIfNull(string shaderName, ref Material reference) {
        // [ UNCHANGED ]
    }
    void OnEnable () {
        // [ UNCHANGED ]
    }
    void OnDisable() {
        // [ UNCHANGED ]
    }
    void OnPlayerGridChange(Vector3Int playerGrid) {
        // [ UNCHANGED ]
    }
    void Update() {
        // [ UNCHANGED ]
    }
    void RenderEnvironmentParticles(EnvironmentParticlesSettings settings, Material material) {
      // [ UNCHANGED ]    
    }

    public void RebuildPrecipitationMesh() {
        Mesh mesh = new Mesh ();
        List indicies = new List();
        List vertices = new List();
        List uvs = new List();
        
        float f = 100f / meshSubdivisions;
        int i  = 0;
        for (float x = 0.0f; x <= 100f; x += f) {
            for (float y = 0.0f; y <= 100f; y += f) {

                float x01 = x / 100.0f;
                float y01 = y / 100.0f;

                vertices.Add(new Vector3(x01 - .5f, 0, y01 - .5f));

                // NEW =================================================
                // calcualte the threshold for this vertex
                // to recreate the 'thinning out' effect
                float vertexIntensityThreshold = Mathf.Max(
                    (float)((x / f) % 4.0f) / 4.0f, 
                    (float)((y / f) % 4.0f) / 4.0f
                );

                // store the `vertexIntensityThreshold` value as the z component in the uv's
                uvs.Add(new Vector3(x01, y01, vertexIntensityThreshold));
                // NEW =================================================
                
                indicies.Add(i++);
            }    
        }
        
        mesh.SetVertices(vertices);
        mesh.SetUVs(0,uvs);
        mesh.SetIndices(indicies.ToArray(), MeshTopology.Points, 0);
        mesh.bounds = new Bounds(Vector3.zero, new Vector3(500, 500, 500));
        mesh.hideFlags = HideFlags.HideAndDontSave;
        meshToDraw = mesh;
    } 
}

#if UNITY_EDITOR
// [ UNCHANGED ]
#endif

我们可以在着色器代码中的几何体构建方法里利用阈值剔除顶点:

#include "UnityCG.cginc"

float _GridSize;

// NEW =================================================
float _Amount;
// NEW =================================================

struct MeshData {
    // [ UNCHANGED ]
};
MeshData vert(MeshData meshData) {
    // [ UNCHANGED ]
}
struct g2f {
    // [ UNCHANGED ]
};
void AddVertex (inout TriangleStream stream, float3 vertex, float2 uv, float colorVariation, float opacity) {      
    // [ UNCHANGED ]
}
void CreateQuad (inout TriangleStream stream, float3 bottomMiddle, float3 topMiddle, float3 perpDir, float colorVariation, float opacity) {    
    // [ UNCHANGED ]
}

#if defined(RAIN)
[maxvertexcount(8)] // rain draws 2 quads
#else
[maxvertexcount(4)] // snow draws one quad that's billboarded towards the camera
#endif
void geom(point MeshData IN[1], inout TriangleStream stream)
{    

    MeshData meshData = IN[0];
    
    UNITY_SETUP_INSTANCE_ID(meshData);

    float3 pos = meshData.vertex.xyz;
    pos.xz *= _GridSize;

    // NEW =================================================
    // mesh vertices cull rendering based on a pattern
    // and the particles `amount` to simulate 'thinning out'
    float vertexAmountThreshold = meshData.uv.z;
    if (vertexAmountThreshold > _Amount)
        return;
    // NEW =================================================
    
    pos.y += _GridSize * .5;

    float opacity = 1.0;

    float colorVariation = 0;
    float2 quadSize = float2(.05, .05);

    float3 quadUpDirection = float3(0,0,1);
    float3 topMiddle = pos + quadUpDirection * quadSize.y;
    float3 rightDirection = float3(.5 * quadSize.x, 0, 0);
    
    CreateQuad (stream, pos, topMiddle, rightDirection, colorVariation, opacity);
}

float4 frag(g2f IN) : SV_Target {
    // [ UNCHANGED ]
}

回到Unity中,如果我们设置数量,就会发现网格的密度发生变化。

为了更加自然,我们可以在阈值上添加噪声,到达阈值时修改每个四边形的透明度进行渐变。

在脚本中,我们添加一个纹理对象,然后通过材质传入着色器:

 using System.Collections.Generic;
 using UnityEngine;
 using UnityEditor;
 using UnityEngine.Rendering;
 ​
 [ExecuteInEditMode] public class PrecipitationManager : MonoBehaviour 
 {
     [System.Serializable] public class EnvironmentParticlesSettings {
         // [ UNCHANGED ]
     }
     
     // NEW =================================================
     public Texture2D mainTexture;
     public Texture2D noiseTexture;
     // NEW =================================================
         
     [Range(2, 256)] public int meshSubdivisions = 200;
     public EnvironmentParticlesSettings rain = new EnvironmentParticlesSettings(
         // [ UNCHANGED ] 
     );
     public EnvironmentParticlesSettings snow = new EnvironmentParticlesSettings(    
         // [ UNCHANGED ] 
     );
     GridHandler gridHandler;
     Matrix4x4[] renderMatrices = new Matrix4x4[3 * 3 * 3];
     Mesh meshToDraw;
     Material rainMaterial, snowMaterial;
     static Material CreateMaterialIfNull(string shaderName, ref Material reference) {
         // [ UNCHANGED ]
     }
     void OnEnable () {
         // [ UNCHANGED ]
     }
     void OnDisable() {
         // [ UNCHANGED ]
     }
     void OnPlayerGridChange(Vector3Int playerGrid) {
         // [ UNCHANGED ]
     }
     void Update() {
         // [ UNCHANGED ]    
     }
 ​
     void RenderEnvironmentParticles(EnvironmentParticlesSettings settings, Material material) {
 ​
         if (settings.amount <= 0)
             return;
 ​
         // NEW =================================================
         material.SetTexture("_MainTex", mainTexture);
         material.SetTexture("_NoiseTex", noiseTexture);  
         // NEW =================================================
 ​
         material.SetFloat("_GridSize", gridHandler.gridSize);
         material.SetFloat("_Amount", settings.amount);
         material.SetColor("_Color", settings.color);
         material.SetColor("_ColorVariation", settings.colorVariation);
         material.SetFloat("_FallSpeed", settings.fallSpeed);
         material.SetVector("_FlutterFrequency", settings.flutterFrequency);
         material.SetVector("_FlutterSpeed", settings.flutterSpeed);
         material.SetVector("_FlutterMagnitude", settings.flutterMagnitude);
         material.SetVector("_CameraRange", settings.cameraRange);
         material.SetVector("_SizeRange", settings.sizeRange);
 ​
         Graphics.DrawMeshInstanced(meshToDraw, 0, material, renderMatrices, renderMatrices.Length, null, ShadowCastingMode.Off, true, 0, null, LightProbeUsage.Off);
     }
 ​
     public void RebuildPrecipitationMesh() {
         // [ UNCHANGED ]
     } 
 }
 ​
 #if UNITY_EDITOR
 // [ UNCHANGED ]
 #endif

在shader头文件中,我们使用噪声纹理来变化定点与之,基于该阈值的数量来计算透明度:

#include "UnityCG.cginc"

// NEW =================================================
sampler2D _NoiseTex;
// NEW =================================================

float _GridSize;
float _Amount;

struct MeshData {
    // [ UNCHANGED ]
};
MeshData vert(MeshData meshData) {
    // [ UNCHANGED ]
}
struct g2f {
    // [ UNCHANGED ]
};
void AddVertex (inout TriangleStream stream, float3 vertex, float2 uv, float colorVariation, float opacity) {      
    // [ UNCHANGED ]
}
void CreateQuad (inout TriangleStream stream, float3 bottomMiddle, float3 topMiddle, float3 perpDir, float colorVariation, float opacity) {    
    // [ UNCHANGED ]
}

#if defined(RAIN)
[maxvertexcount(8)] // rain draws 2 quads
#else
[maxvertexcount(4)] // snow draws one quad that's billboarded towards the camera
#endif
void geom(point MeshData IN[1], inout TriangleStream stream)
{    

    MeshData meshData = IN[0];
    
    UNITY_SETUP_INSTANCE_ID(meshData);

    float3 pos = meshData.vertex.xyz;
    pos.xz *= _GridSize;

    // NEW =================================================
    // samples 2 seperate noise values so we get some variation
    float2 noise = float2(
        frac(tex2Dlod(_NoiseTex, float4(meshData.uv.xy    , 0, 0)).r + (pos.x + pos.z)), 
        frac(tex2Dlod(_NoiseTex, float4(meshData.uv.yx * 2, 0, 0)).r + (pos.x * pos.z))
    );
    // NEW =================================================


    float vertexAmountThreshold = meshData.uv.z;
    
    // NEW =================================================
    // add some noise to the vertex threshold
    vertexAmountThreshold *= noise.y;
    // NEW =================================================
    
    if (vertexAmountThreshold > _Amount)
        return;

    pos.y += _GridSize * .5;

    float opacity = 1.0;
    
    // NEW =================================================
    // fade out as the amount reaches the limit for this vertex threshold
    #define VERTEX_THRESHOLD_LEVELS 4
    float vertexAmountThresholdFade = min((_Amount - vertexAmountThreshold) * VERTEX_THRESHOLD_LEVELS, 1);
    opacity *= vertexAmountThresholdFade;
        
    if (opacity <= 0)
        return;
    // NEW =================================================

    // temporary values
    float colorVariation = 0;
    float2 quadSize = float2(.05, .05);

    float3 quadUpDirection = float3(0,0,1);
    float3 topMiddle = pos + quadUpDirection * quadSize.y;
    float3 rightDirection = float3(.5 * quadSize.x, 0, 0);
    
    CreateQuad (stream, pos, topMiddle, rightDirection, colorVariation, opacity);
}

float4 frag(g2f IN) : SV_Target {
    float4 color = float4(IN.uv.xy, 0, 1);
    
    // NEW =================================================
    // apply opacity
    color.a *= IN.uv.z;
    // NEW =================================================
    
    return color;
}

现在的话,雨雪网格的变化就会比较自然。

现在我们基于摄像机的距离和朝向来进行剔除。我们可以在着色器中进行:

 #include "UnityCG.cginc"
 ​
 sampler2D _NoiseTex;
 float _GridSize;
 float _Amount;
 ​
 // NEW =================================================
 float2 _CameraRange;
 // NEW =================================================
 ​
 struct MeshData {
     // [ UNCHANGED ]
 };
 MeshData vert(MeshData meshData) {
     // [ UNCHANGED ]
 }
 struct g2f {
     // [ UNCHANGED ]
 };
 void AddVertex (inout TriangleStream stream, float3 vertex, float2 uv, float colorVariation, float opacity) {      
     // [ UNCHANGED ]
 }
 void CreateQuad (inout TriangleStream stream, float3 bottomMiddle, float3 topMiddle, float3 perpDir, float colorVariation, float opacity) {    
     // [ UNCHANGED ]
 }
 ​
 #if defined(RAIN)
 [maxvertexcount(8)] // rain draws 2 quads
 #else
 [maxvertexcount(4)] // snow draws one quad that's billboarded towards the camera
 #endif
 void geom(point MeshData IN[1], inout TriangleStream stream)
 {    
     MeshData meshData = IN[0];
     UNITY_SETUP_INSTANCE_ID(meshData);
     float3 pos = meshData.vertex.xyz;
     pos.xz *= _GridSize;
     float2 noise = float2(
         frac(tex2Dlod(_NoiseTex, float4(meshData.uv.xy    , 0, 0)).r + (pos.x + pos.z)), 
         frac(tex2Dlod(_NoiseTex, float4(meshData.uv.yx * 2, 0, 0)).r + (pos.x * pos.z))
     );
     float vertexAmountThreshold = meshData.uv.z;
     vertexAmountThreshold *= noise.y;
 ​
     if (vertexAmountThreshold > _Amount)
         return;
 ​
     pos.y += _GridSize * .5;
 ​
     // NEW =================================================
     // calculate the world space position of the particles
     float3 worldPos = pos + float3(
         unity_ObjectToWorld[0].w, 
         unity_ObjectToWorld[1].w, 
         unity_ObjectToWorld[2].w
     );
 ​
     // the direction from the position to the camera
     float3 pos2Camera = worldPos - _WorldSpaceCameraPos;
     float distanceToCamera = length(pos2Camera);
     
     // normalize pos2Camera direction
     pos2Camera /= distanceToCamera;
 ​
     // calculate the camera's forward direction
     float3 camForward = normalize(mul((float3x3)unity_CameraToWorld, float3(0,0,1)));
 ​
     // if the angle between the direction to camera and it's forward are too large
     // then the camera is facign away, so don't draw
     if (dot(camForward, pos2Camera) < 0.5)
         return;
     // NEW =================================================
 ​
 ​
     float opacity = 1.0;
 ​
     // NEW =================================================
     // produces a value between 0 and 1 corresponding to where the distance to camera is within
     // the Camera Distance range (1 when at or below minimum, 0 when at or above maximum)
     // this way the particle fades out as it get's too far, and doesnt just pop out of existence
     float camDistanceInterpolation = 1.0 - min(max(distanceToCamera - _CameraRange.x, 0) / (_CameraRange.y - _CameraRange.x), 1);
     opacity *= camDistanceInterpolation;
     // NEW =================================================
 ​
     #define VERTEX_THRESHOLD_LEVELS 4
     float vertexAmountThresholdFade = min((_Amount - vertexAmountThreshold) * VERTEX_THRESHOLD_LEVELS, 1);
     opacity *= vertexAmountThresholdFade;
         
     if (opacity <= 0)
         return;
 ​
     float colorVariation = 0;
     float2 quadSize = float2(.05, .05);
 ​
     float3 quadUpDirection = float3(0,0,1);
     float3 topMiddle = pos + quadUpDirection * quadSize.y;
     float3 rightDirection = float3(.5 * quadSize.x, 0, 0);
     
     CreateQuad (stream, pos, topMiddle, rightDirection, colorVariation, opacity);
 }
 ​
 float4 frag(g2f IN) : SV_Target {
     // [ UNCHANGED ]
 }

现在移动摄像机的话就会发现网格会随距离渐变。

现在我们来实现雨雪下落效果。我们需要在着色器中为顶点的Y坐标制作动画。在这之前,我们需要确定雨雪会在哪里停止。在其到达网格y轴坐标时执行。在脚本中,我们将_MaxTravelDistance的着色器变量传入网格大小:

using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.Rendering;

[ExecuteInEditMode] public class PrecipitationManager : MonoBehaviour 
{
    [System.Serializable] public class EnvironmentParticlesSettings {
        // [ UNCHANGED ]
    }
    public Texture2D mainTexture;
    public Texture2D noiseTexture;
    [Range(2, 256)] public int meshSubdivisions = 200;
    public EnvironmentParticlesSettings rain = new EnvironmentParticlesSettings(
        // [ UNCHANGED ]
    );
    public EnvironmentParticlesSettings snow = new EnvironmentParticlesSettings(	
        // [ UNCHANGED ]
    );
    GridHandler gridHandler;
    Matrix4x4[] renderMatrices = new Matrix4x4[3 * 3 * 3];
    Mesh meshToDraw;
    Material rainMaterial, snowMaterial;
    static Material CreateMaterialIfNull(string shaderName, ref Material reference) {
        // [ UNCHANGED ]
    }
    void OnEnable () {
        // [ UNCHANGED ]
    }
    void OnDisable() {
        // [ UNCHANGED ]
    }
    void OnPlayerGridChange(Vector3Int playerGrid) {
        // [ UNCHANGED ]
    }
    void Update() {
        if (meshToDraw == null)
            RebuildPrecipitationMesh();

        // NEW =================================================
        float maxTravelDistance = gridHandler.gridSize;

        // render the rain and snow
        RenderEnvironmentParticles(rain, CreateMaterialIfNull("Hidden/Environment/Rain", ref rainMaterial), maxTravelDistance);
        RenderEnvironmentParticles(snow, CreateMaterialIfNull("Hidden/Environment/Snow", ref snowMaterial), maxTravelDistance);
        // NEW =================================================
    }

    void RenderEnvironmentParticles(EnvironmentParticlesSettings settings, Material material, float maxTravelDistance) {

        if (settings.amount <= 0)
            return;

        material.SetTexture("_MainTex", mainTexture);
        material.SetTexture("_NoiseTex", noiseTexture);  
        material.SetFloat("_GridSize", gridHandler.gridSize);
        material.SetFloat("_Amount", settings.amount);
        material.SetColor("_Color", settings.color);
        material.SetColor("_ColorVariation", settings.colorVariation);
        material.SetFloat("_FallSpeed", settings.fallSpeed);
        material.SetVector("_FlutterFrequency", settings.flutterFrequency);
        material.SetVector("_FlutterSpeed", settings.flutterSpeed);
        material.SetVector("_FlutterMagnitude", settings.flutterMagnitude);
        material.SetVector("_CameraRange", settings.cameraRange);
        material.SetVector("_SizeRange", settings.sizeRange);

        // NEW =================================================
        material.SetFloat("_MaxTravelDistance", maxTravelDistance);
        // NEW =================================================
            
        Graphics.DrawMeshInstanced(meshToDraw, 0, material, renderMatrices, renderMatrices.Length, null, ShadowCastingMode.Off, true, 0, null, LightProbeUsage.Off);
    }

    public void RebuildPrecipitationMesh() {
        // [ UNCHANGED ]
    } 
}

#if UNITY_EDITOR
// [ UNCHANGED ]
#endif

着色器中,我们随时间变换顶点的Y轴坐标,确保其会在到达_MaxTravelDistance后会重新循环。循环点和下落速度可以通过噪声值修改,因此雨雪的下落会看起来比较自然:

 #include "UnityCG.cginc"
 ​
 sampler2D _NoiseTex;
 float _GridSize;
 float _Amount;
 float2 _CameraRange;
 ​
 // NEW =================================================
 float _FallSpeed;
 float _MaxTravelDistance;
 // NEW =================================================
 ​
 struct MeshData {
     // [ UNCHANGED ]
 };
 MeshData vert(MeshData meshData) {
     // [ UNCHANGED ] 
 }
 struct g2f {
     // [ UNCHANGED ]
 };
 void AddVertex (inout TriangleStream stream, float3 vertex, float2 uv, float colorVariation, float opacity) {      
     // [ UNCHANGED ]
 }
 void CreateQuad (inout TriangleStream stream, float3 bottomMiddle, float3 topMiddle, float3 perpDir, float colorVariation, float opacity) {    
     // [ UNCHANGED ]
 }
 ​
 #if defined(RAIN)
 [maxvertexcount(8)] // rain draws 2 quads
 #else
 [maxvertexcount(4)] // snow draws one quad that's billboarded towards the camera
 #endif
 void geom(point MeshData IN[1], inout TriangleStream stream)
 {    
     MeshData meshData = IN[0];
     UNITY_SETUP_INSTANCE_ID(meshData);
     float3 pos = meshData.vertex.xyz;
     pos.xz *= _GridSize;
     float2 noise = float2(
         frac(tex2Dlod(_NoiseTex, float4(meshData.uv.xy    , 0, 0)).r + (pos.x + pos.z)), 
         frac(tex2Dlod(_NoiseTex, float4(meshData.uv.yx * 2, 0, 0)).r + (pos.x * pos.z))
     );
     float vertexAmountThreshold = meshData.uv.z;
     vertexAmountThreshold *= noise.y;
     if (vertexAmountThreshold > _Amount)
         return;
     
     // NEW =================================================
     // "falling down" movement
     // add 10000 to the time variable so it starts out `prebaked`
     // modify the movespeed by a random factor as well
     pos.y -= (_Time.y + 10000) * (_FallSpeed + (_FallSpeed * noise.y));
 ​
     // make sure the particles "loops" around back to the top once it reaches the
     // max travel distance (+ some noise for randomness)
     pos.y = fmod(pos.y, -_MaxTravelDistance) + noise.x;
     // NEW =================================================
     
     
     pos.y += _GridSize * .5;
     float3 worldPos = pos + float3(
         unity_ObjectToWorld[0].w, 
         unity_ObjectToWorld[1].w, 
         unity_ObjectToWorld[2].w
     );
 ​
     float3 pos2Camera = worldPos - _WorldSpaceCameraPos;
     float distanceToCamera = length(pos2Camera);
     pos2Camera /= distanceToCamera;
     float3 camForward = normalize(mul((float3x3)unity_CameraToWorld, float3(0,0,1)));
     if (dot(camForward, pos2Camera) < 0.5)
         return;
 ​
     float opacity = 1.0;
     float camDistanceInterpolation = 1.0 - min(max(distanceToCamera - _CameraRange.x, 0) / (_CameraRange.y - _CameraRange.x), 1);
     opacity *= camDistanceInterpolation;
     #define VERTEX_THRESHOLD_LEVELS 4
     float vertexAmountThresholdFade = min((_Amount - vertexAmountThreshold) * VERTEX_THRESHOLD_LEVELS, 1);
     opacity *= vertexAmountThresholdFade;
     if (opacity <= 0)
         return;
 ​
     float colorVariation = 0;
     float2 quadSize = float2(.05, .05);
 ​
     // change the quadUpDirection so the quad is upright for now
     float3 quadUpDirection = float3(0, 1, 0);
     float3 topMiddle = pos + quadUpDirection * quadSize.y;
     float3 rightDirection = float3(.5 * quadSize.x, 0, 0);
     
     CreateQuad (stream, pos, topMiddle, rightDirection, colorVariation, opacity);
 }
 ​
 float4 frag(g2f IN) : SV_Target {
     // [ UNCHANGED ]
 }

对于雪花,我们需要在X、Z轴进行偏移来体现其的轻柔。我们在几何着色器中使用_Flutter变量来完成:

#include "UnityCG.cginc"

sampler2D _NoiseTex;
float _GridSize;
float _Amount;
float2 _CameraRange;
float _FallSpeed;
float _MaxTravelDistance;

// NEW =================================================
float2 _FlutterFrequency;
float2 _FlutterSpeed;
float2 _FlutterMagnitude;
// NEW =================================================

struct MeshData {
    // [ UNCHANGED ]
};
MeshData vert(MeshData meshData) {
    // [ UNCHANGED ]
}
struct g2f {
    // [ UNCHANGED ]
};
void AddVertex (inout TriangleStream stream, float3 vertex, float2 uv, float colorVariation, float opacity) {      
    // [ UNCHANGED ]
}
void CreateQuad (inout TriangleStream stream, float3 bottomMiddle, float3 topMiddle, float3 perpDir, float colorVariation, float opacity) {    
    // [ UNCHANGED ]
}

#if defined(RAIN)
[maxvertexcount(8)] // rain draws 2 quads
#else
[maxvertexcount(4)] // snow draws one quad that's billboarded towards the camera
#endif
void geom(point MeshData IN[1], inout TriangleStream stream)
{    
    MeshData meshData = IN[0];
    UNITY_SETUP_INSTANCE_ID(meshData);
    float3 pos = meshData.vertex.xyz;
    pos.xz *= _GridSize;
    float2 noise = float2(
        frac(tex2Dlod(_NoiseTex, float4(meshData.uv.xy    , 0, 0)).r + (pos.x + pos.z)), 
        frac(tex2Dlod(_NoiseTex, float4(meshData.uv.yx * 2, 0, 0)).r + (pos.x * pos.z))
    );
    
    float vertexAmountThreshold = meshData.uv.z;
    vertexAmountThreshold *= noise.y;

    if (vertexAmountThreshold > _Amount)
        return;
    
    pos.y -= (_Time.y + 10000) * (_FallSpeed + (_FallSpeed * noise.y));

    // NEW =================================================
    // Add random noise while travelling based on time, some randomness, and "distance travelled"
    float2 inside = pos.y * noise.yx * _FlutterFrequency + ((_FlutterSpeed + (_FlutterSpeed * noise)) * _Time.y);
    float2 flutter = float2(sin(inside.x), cos(inside.y)) * _FlutterMagnitude;
    pos.xz += flutter;
    // NEW =================================================

    pos.y = fmod(pos.y, -_MaxTravelDistance) + noise.x;
    pos.y += _GridSize * .5;
    float3 worldPos = pos + float3(
        unity_ObjectToWorld[0].w, 
        unity_ObjectToWorld[1].w, 
        unity_ObjectToWorld[2].w
    );
    float3 pos2Camera = worldPos - _WorldSpaceCameraPos;
    float distanceToCamera = length(pos2Camera);
    pos2Camera /= distanceToCamera;
    float3 camForward = normalize(mul((float3x3)unity_CameraToWorld, float3(0,0,1)));
    if (dot(camForward, pos2Camera) < 0.5)
        return;

    float opacity = 1.0;
    float camDistanceInterpolation = 1.0 - min(max(distanceToCamera - _CameraRange.x, 0) / (_CameraRange.y - _CameraRange.x), 1);
    opacity *= camDistanceInterpolation;
    #define VERTEX_THRESHOLD_LEVELS 4
    float vertexAmountThresholdFade = min((_Amount - vertexAmountThreshold) * VERTEX_THRESHOLD_LEVELS, 1);
    opacity *= vertexAmountThresholdFade;
        
    if (opacity <= 0)
        return;

    float colorVariation = 0;
    float2 quadSize = float2(.05, .05);

    float3 quadUpDirection = float3(0, 1, 0);
    float3 topMiddle = pos + quadUpDirection * quadSize.y;
    float3 rightDirection = float3(.5 * quadSize.x, 0, 0);
    
    CreateQuad (stream, pos, topMiddle, rightDirection, colorVariation, opacity);
}

float4 frag(g2f IN) : SV_Target {
    // [ UNCHANGED ]
}

之后我们应用纹理,并修改一些细节。

你可能感兴趣的:(unity天气系统)