Unity Shader:昼夜交替和天气系统

昼夜交替,主要涉及的是天空盒的平滑交替变化,同时其的颜色会影响场景中模型的纹理的颜色,这里先给出我最初的思路:

  • 首先,准备好多个六面天空盒纹理,然后随时间变化场景中要渲染天空盒的每一个面的纹理,两纹理间进行采样像素的平滑插值,使用时间作为影响因数。
  • 接着为场景中的平行光添加一个旋转函数来模拟日升日落。
  • 每个天空盒纹理附带一个基础的环境光颜色,使用该环境光颜色,从暗到亮到暗来模拟日升日落对场景的影响。
  • 为了表现时间在变化,可以添加太阳、云和月亮的效果。

天气系统的话,主要就是晴天和雨天(雪天暂不考虑),晴天不需要做太多的变化,如果硬要做的话就是在正午的时候添加晕光效果。雨天主要是降低场景的亮度,同时添加粒子模拟雨滴。

为此,我们首先尝试自己实现一个天空盒材质。

目前网上能够找到的天空盒纹理素材最常见的是6面纹理,我们先根据这六面纹理实现一个天空盒材质

我将相关结构体和重复调用的着色器在头文件中声明:

#include "UnityCG.cginc"

struct vertexInput
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
};

struct vertexOutput
{
    float4 vertex : SV_POSITION
    float2 uv : TEXCOORD0;
};


//skybox vertex shader
vertexOutput skyboxVert(vertexInput i)
{
    vertexOutput o;
    o.vertex = UnityObjectToClipPos(i.vertex);
    o.uv = i.uv;
    return o;
}

//skybox fragment Shader
half4 skyboxFrag(vertexOutput i, sampler2D smp, half4 smpDecode)
{
    half4 tex = tex2D(smp, i.uv);
    half3 col = DecodeHDR(tex, smpDecode);
    return half4(col, 1.0);
}

注意,最后一个片段着色器我考虑了HDR颜色范围。

之后,在主着色器中我声明了六张纹理属性:

    Properties
    {
        [NoScaleOffset]_FrontTex("Front[+Z] ", 2D) = "grey"{}
        [NoScaleOffset]_BackTex("Back[-Z] ", 2D) = "grey"{}
        [NoScaleOffset]_LeftTex("Left[+X] ", 2D) = "grey"{}
        [NoScaleOffset]_RightTex("Right[-X] ", 2D) = "grey"{}
        [NoScaleOffset]_UpTex("Up[+Y] ", 2D) = "grey"{}
        [NoScaleOffset]_DownTex("Down[-Y] ", 2D) = "grey"{}
    }

[NoScaleOffset]表明不使用tile和offset来变换纹理。

之后我们开启背景渲染队列和渲染类型标签,同时关闭面剔除和深度写入:

        Tags { "Queue"="Background" "RenderType"="Background" "PreviewType"="Skybox" }
        Cull Off
        ZWrite Off

最后,我们定义了六个pass来渲染每一面:

        Pass
        {
            CGPROGRAM
            #pragma vertex skyboxVert
            #pragma frag frag
            #pragma target 2.0
            #include "DayNightShader.cginc"
            sampler2D _FrontTex;
            half4 _FrontTex_HDR;
            half4 frag(vertexOutput i):SV_Target{return skyboxFrag(i, _FrontTex, _FrontTex_HDR);}
            ENDCG
        }

        Pass
        {
            CGPROGRAM
            #pragma vertex skyboxVert
            #pragma frag frag
            #pragma target 2.0
            #include "DayNightShader.cginc"
            sampler2D _BackTex;
            half4 _BackTex_HDR;
            half4 frag(vertexOutput i):SV_Target{return skyboxFrag(i, _BackTex, _BackTex_HDR);}
            ENDCG
        }

        Pass
        {
            CGPROGRAM
            #pragma vertex skyboxVert
            #pragma frag frag
            #pragma target 2.0
            #include "DayNightShader.cginc"
            sampler2D _LeftTex;
            half4 _LeftTex_HDR;
            half4 frag(vertexOutput i):SV_Target{return skyboxFrag(i, _LeftTex, _LeftTex_HDR);}
            ENDCG
        }

        Pass
        {
            CGPROGRAM
            #pragma vertex skyboxVert
            #pragma frag frag
            #pragma target 2.0
            #include "DayNightShader.cginc"
            sampler2D _RightTex;
            half4 _RightTex_HDR;
            half4 frag(vertexOutput i):SV_Target{return skyboxFrag(i, _RightTex, _RightTex_HDR);}
            ENDCG
        }

        Pass
        {
            CGPROGRAM
            #pragma vertex skyboxVert
            #pragma frag frag
            #pragma target 2.0
            #include "DayNightShader.cginc"
            sampler2D _UpTex;
            half4 _UpTex_HDR;
            half4 frag(vertexOutput i):SV_Target{return skyboxFrag(i, _UpTex, _UpTex_HDR);}
            ENDCG
        }

        Pass
        {
            CGPROGRAM
            #pragma vertex skyboxVert
            #pragma frag frag
            #pragma target 2.0
            #include "DayNightShader.cginc"
            sampler2D _DownTex;
            half4 _DownTex_HDR;
            half4 frag(vertexOutput i):SV_Target{return skyboxFrag(i, _DownTex, _DownTex_HDR);}
            ENDCG
        }

这样,一个简单的天空盒材质就完成了,接下来我们来尝试完成昼夜系统。我已经在UnityAsset中找到了一套天空盒纹理:


为了不定义太多的纹理属性,因为算上我要使用的天空盒,总共会定义42张纹理属性,这里我们可以取而代之定义6个纹理数组:

    Properties
    {
        [NoScaleOffset]_FrontTex("Front[+Z] ", 2DArray) = "grey"{}
        [NoScaleOffset]_BackTex("Back[-Z] ", 2DArray) = "grey"{}
        [NoScaleOffset]_LeftTex("Left[+X] ", 2DArray) = "grey"{}
        [NoScaleOffset]_RightTex("Right[-X] ", 2DArray) = "grey"{}
        [NoScaleOffset]_UpTex("Up[+Y] ", 2DArray) = "grey"{}
        [NoScaleOffset]_DownTex("Down[-Y] ", 2DArray) = "grey"{}
    }

我们先尝试直接根据索引变换纹理,声明一个切片范围属性

        _SliceRange ("Slices", Range(0,6)) = 0

记得更新输出结构体,添加索引属性,在顶点着色器中修改uv.z:

struct vertexOutput
{
    float4 vertex : SV_POSITION;
    float2 uv : TEXCOORD0;
    float arrayIndex : TEXCOORD1;
};

vertexOutput skyboxVert(vertexInput i)
{
    vertexOutput o;
    o.vertex = UnityObjectToClipPos(i.vertex);
    o.uv = i.uv;
    o.arrayIndex = _SliceRange;
    return o;
}

然后修改主shader中的pass:

        Pass
        {
            CGPROGRAM
            #pragma vertex skyboxVert
            #pragma fragment frag
            #pragma target 2.0
            #include "DayNightShader.cginc"
            half4 _FrontTex_HDR;
            UNITY_DECLARE_TEX2DARRAY(_FrontTex);
            half4 frag(vertexOutput i):SV_Target
            {
                half4 tex = UNITY_SAMPLE_TEX2DARRAY(_FrontTex, float3(i.uv, i.arrayIndex));
                half3 col = DecodeHDR(tex, _FrontTex_HDR);
                return half4(col, 1.0);
            }
            ENDCG
        }

注意主要的代码在于UNITY_DECLARE_TEX2DARRAYUNITY_SAMPLE_TEX2DARRAY,它们分别声明一个纹理数组采样器,以及根据3维UV坐标采样纹理,第三个维度是索引。

之后我们就可以在材质界面使用TextureArray了:



但,Unity并不直接支持直接生成Texture 2D Array类型的纹理,我们需要自己生成。

感谢github上的某热心网友,让这件事变得简单了许多,这里附上链接:
https://github.com/XJINE/Unity_AssetCreationHelper
https://github.com/XJINE/Unity_Texture2DArrayGenerator

主要是利用Texture2DArray类进行的生成。

这里已经生成好了6个纹理数组:


然后应用到材质上:



这时候拖动Slices就会发现天空盒在不断变化。

接下来我们尝试在两张纹理间对颜色进行插值变化,这里使用lerp进行线性插值:

        Pass
        {
            CGPROGRAM
            #pragma vertex skyboxVert
            #pragma fragment frag
            #pragma target 2.0
            //#pragma require 2darray
            #include "DayNightShader.cginc"
            half4 _FrontTex_HDR;
            UNITY_DECLARE_TEX2DARRAY(_FrontTex);
            half4 frag(vertexOutput i):SV_Target
            {
                half4 previousTex = UNITY_SAMPLE_TEX2DARRAY(_FrontTex, float3(i.uv, floor(i.arrayIndex)));
                half4 nextTex = UNITY_SAMPLE_TEX2DARRAY(_FrontTex, float3(i.uv, ceil(i.arrayIndex)));
                half4 texColor = half4(lerp(previousTex, nextTex, frac(i.arrayIndex)).xyz, 1.0);
                half3 col = DecodeHDR(texColor, _FrontTex_HDR);
                return half4(col, 1.0);
            }
            ENDCG
        }

之后就可以拖动Slices滑条来得到平滑的天空盒变化。


然后我们让该属性随时间自动变化,模拟昼夜交替。

我们在外部写一个脚本来控制该属性。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DayAndNightTime : MonoBehaviour
{
    public Material material;
    float count = 0.0f;
    public float speed = 1.0f;

    private void Update()
    {
        material.SetFloat("_SliceRange", count);
        count += Time.deltaTime * speed;
    }
}

不过有一个问题,索引的最大值为6.0f,因此当超过该值的时候应该置为0:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DayAndNightTime : MonoBehaviour
{
    public Material material;
    float count = 0.0f;
    public float speed = 1.0f;

    private void Update()
    {
        material.SetFloat("_SliceRange", count);
        count += Time.deltaTime * speed;
        if (count >= 6.0f)
        {
            count = 0.0f;
        }
    }
}

不过这在视觉上就会产生一个问题,即半夜到凌晨是突然转换的,因此将值设为7比较好:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DayAndNightTime : MonoBehaviour
{
    public Material material;
    float count = 0.0f;
    public float speed = 1.0f;

    private void Update()
    {
        material.SetFloat("_SliceRange", count);
        count += Time.deltaTime * speed;
        if (count >= 7.0f)
        {
            count = 0.0f;
        }
    }
}

然后在shader中,当索引值大于6.0时,我们在半夜和凌晨间插值:

                float nextIndex = ceil(i.arrayIndex);
                if(nextIndex == 7.0f)
                {
                    nextIndex = 0.0f;
                }
                half4 nextTex = UNITY_SAMPLE_TEX2DARRAY(_FrontTex, float3(i.uv, nextIndex));

这样就可以得到随时间变化的昼夜交替:


同时我们将平行光随时间变化来模拟太阳日升日落的变化,我们从这个角度开始:



然后让其随时间沿x轴旋转,来表示太阳上升落下,同时还要绕y轴旋转来表示太阳东升西落:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DirLightRotation : MonoBehaviour
{
    public float UpDownSpeed = 3.0f;
    public float EastWestSpeed = 2.0f;
    void Update()
    {
        transform.Rotate(new UnityEngine.Vector3(Time.deltaTime * UpDownSpeed * 10.0f, 0,  0), Space.Self);
        transform.Rotate(new UnityEngine.Vector3(0, Time.deltaTime * EastWestSpeed * 10.0f, 0), Space.Self);
    }
}

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

我们可以使用几何着色器来批量创建粒子,可以减少从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;
    }
}

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

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

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



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



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

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


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

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<Vector3Int> 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,并将脚本赋予它,将摄像机赋予该脚本组件。


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

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>();
        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<int> indicies = new List<int>();
        List<Vector3> vertices = new List<Vector3>();
        List<Vector3> uvs = new List<Vector3>();
            
        // 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<g2f> 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<g2f> 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<g2f> 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来绘制实例。

效果如下:


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

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

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<int> indicies = new List<int>();
        List<Vector3> vertices = new List<Vector3>();
        List<Vector3> uvs = new List<Vector3>();
        
        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<g2f> stream, float3 vertex, float2 uv, float colorVariation, float opacity) {      
    // [ UNCHANGED ]
}
void CreateQuad (inout TriangleStream<g2f> 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<g2f> 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<g2f> stream, float3 vertex, float2 uv, float colorVariation, float opacity) {      
    // [ UNCHANGED ]
}
void CreateQuad (inout TriangleStream<g2f> 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<g2f> 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<g2f> stream, float3 vertex, float2 uv, float colorVariation, float opacity) {      
    // [ UNCHANGED ]
}
void CreateQuad (inout TriangleStream<g2f> 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<g2f> 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<g2f> stream, float3 vertex, float2 uv, float colorVariation, float opacity) {      
    // [ UNCHANGED ]
}
void CreateQuad (inout TriangleStream<g2f> 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<g2f> 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<g2f> stream, float3 vertex, float2 uv, float colorVariation, float opacity) {      
    // [ UNCHANGED ]
}
void CreateQuad (inout TriangleStream<g2f> 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<g2f> 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 ]
}

接下来我们为四边形网格应用纹理,让其基于位置/时间来变换颜色,同时考虑网格的大小:

#include "UnityCG.cginc"

// NEW =================================================
sampler2D _MainTex;
// NEW =================================================

sampler2D _NoiseTex;
float _GridSize;
float _Amount;
float2 _CameraRange;
float _FallSpeed;
float _MaxTravelDistance;
float2 _FlutterFrequency;
float2 _FlutterSpeed;
float2 _FlutterMagnitude;

// NEW =================================================
float4 _Color;
float4 _ColorVariation;
float2 _SizeRange; 
// NEW =================================================

struct MeshData {
    // [ UNCHANGED ]
};
MeshData vert(MeshData meshData) {
    // [ UNCHANGED ]
}
struct g2f {
    // [ UNCHANGED ]
};
void AddVertex (inout TriangleStream<g2f> stream, float3 vertex, float2 uv, float colorVariation, float opacity) {      
    // [ UNCHANGED ]
}
void CreateQuad (inout TriangleStream<g2f> 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<g2f> 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));
    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;
    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;

    // NEW =================================================
    // calculate the color variation based on the position, time, and some randomness
    // and multiply it by the amount the user specified (in the color variation alpha channel)
    // (from speed tree)
    float colorVariation = (sin(noise.x * (pos.x + pos.y * noise.y + pos.z + _Time.y * 2)) * .5 + .5) * _ColorVariation.a;
    
    // choose a size multiplier randomly
    float2 quadSize = lerp(_SizeRange.x, _SizeRange.y, noise.x);
    // NEW =================================================

    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 {
    // NEW =================================================
    // samples the texture and modify its color
    float4 color = tex2D(_MainTex, IN.uv.xy) * _Color;

    // add hue variation (taken from speed tree)
    float colorVariationAmount = IN.uv.w;
    float3 shiftedColor = lerp(color.rgb, _ColorVariation.rgb, colorVariationAmount);
    float maxBase = max(color.r, max(color.g, color.b));
    float newMaxBase = max(shiftedColor.r, max(shiftedColor.g, shiftedColor.b));
    // preserve vibrance
    color.rgb = saturate(shiftedColor * ((maxBase/newMaxBase) * 0.5 + 0.5));
    // NEW =================================================
    
    color.a *= IN.uv.z;

    return color;
}

由于雪花被渲染为相对较小的形状规则的四边形,我们可以使用公告板技术来让其一直面对摄像机。这样的话移动摄像机时它不会消失。为此,我们在几何体方法计算:

#include "UnityCG.cginc"

sampler2D _MainTex;
sampler2D _NoiseTex;
float _GridSize;
float _Amount;
float2 _CameraRange;
float _FallSpeed;
float _MaxTravelDistance;
float2 _FlutterFrequency;
float2 _FlutterSpeed;
float2 _FlutterMagnitude;
float4 _Color;
float4 _ColorVariation;
float2 _SizeRange; 

struct MeshData {
    // [ UNCHANGED ]
};
MeshData vert(MeshData meshData) {
    // [ UNCHANGED ]
}
struct g2f{
    // [ UNCHANGED ]
};
void AddVertex (inout TriangleStream<g2f> stream, float3 vertex, float2 uv, float colorVariation, float opacity){      
    // [ UNCHANGED ]
}
void CreateQuad (inout TriangleStream<g2f> 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<g2f> 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));
    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;
    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 = (sin(noise.x * (pos.x + pos.y * noise.y + pos.z + _Time.y * 2)) * .5 + .5) * _ColorVariation.a;
    float2 quadSize = lerp(_SizeRange.x, _SizeRange.y, noise.x);

// NEW =================================================
#if defined (RAIN)
    // what we had before
    float3 quadUpDirection = float3(0, 1, 0);
    float3 topMiddle = pos + quadUpDirection * quadSize.y;
    float3 rightDirection = float3(.5 * quadSize.x, 0, 0);
    
#else
    // snow is billboarded, that means the quad always faces the camera
    float3 quadUpDirection = UNITY_MATRIX_IT_MV[1].xyz;
    float3 topMiddle = pos + quadUpDirection * quadSize.y;
    float3 rightDirection = UNITY_MATRIX_IT_MV[0].xyz * .5 * quadSize.x;
#endif
// NEW =================================================

    CreateQuad (stream, pos, topMiddle, rightDirection, colorVariation, opacity);
}

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

现在的话雪花会一直朝向我们。

雨滴类似,不过在高度上四边形会进行拉伸,因为雨滴粒子会是一个不统一的矩形,公告板会有些问题。

为此我们渲染两个相互垂直的四边形:



接着修改头文件:

#include "UnityCG.cginc"

// NEW =================================================
#if defined (RAIN)
// creates a rotation matrix around the axis of 90 degrees
// (only needded for the second rain quad)
float4x4 rotationMatrix90(float3 axis) {    
    float ocxy = axis.x * axis.y;
    float oczx = axis.z * axis.x;
    float ocyz = axis.y * axis.z;
    return float4x4(
        axis.x * axis.x, ocxy - axis.z, oczx + axis.y, 0.0,
        ocxy + axis.z, axis.y * axis.y, ocyz - axis.x, 0.0,
        oczx - axis.y, ocyz + axis.x, axis.z * axis.z, 0.0,
        0.0, 0.0, 0.0, 1.0
    );
}
#endif
// NEW =================================================

sampler2D _MainTex;
sampler2D _NoiseTex;
float _GridSize;
float _Amount;
float2 _CameraRange;
float _FallSpeed;
float _MaxTravelDistance;
float2 _FlutterFrequency;
float2 _FlutterSpeed;
float2 _FlutterMagnitude;
float4 _Color;
float4 _ColorVariation;
float2 _SizeRange; 

struct MeshData {
    // [ UNCHANGED ]
};
MeshData vert(MeshData meshData) {
    // [ UNCHANGED ]
}
struct g2f {
    // [ UNCHANGED ]
};
void AddVertex (inout TriangleStream<g2f> stream, float3 vertex, float2 uv, float colorVariation, float opacity){      
    // [ UNCHANGED ]
}
void CreateQuad (inout TriangleStream<g2f> 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<g2f> 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));
    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;
    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 = (sin(noise.x * (pos.x + pos.y * noise.y + pos.z + _Time.y * 2)) * .5 + .5) * _ColorVariation.a;
    float2 quadSize = lerp(_SizeRange.x, _SizeRange.y, noise.x);

#if defined (RAIN)

    // NEW =================================================
    // rain is skinny along the x axis
    quadSize.x *= .01;
    // NEW =================================================

    float3 quadUpDirection = float3(0, 1, 0);
    float3 topMiddle = pos + quadUpDirection * quadSize.y;
    float3 rightDirection = float3(.5 * quadSize.x, 0, 0);
#else
    float3 quadUpDirection = UNITY_MATRIX_IT_MV[1].xyz;
    float3 topMiddle = pos + quadUpDirection * quadSize.y;
    float3 rightDirection = UNITY_MATRIX_IT_MV[0].xyz * .5 * quadSize.x;
#endif

    CreateQuad (stream, pos, topMiddle, rightDirection, colorVariation, opacity);

// NEW =================================================
#if defined (RAIN)
    // rain draws 2 quads perpendicular to eachotehr
    rightDirection = mul((float3x3)rotationMatrix90(quadUpDirection), rightDirection);
    CreateQuad (stream, pos, topMiddle, rightDirection, colorVariation, opacity);
#endif
// NEW =================================================
}

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

至此雨滴完善了。

接下来我们可以添加风,我们会在XZ平面上定义一个风向,会弯曲对着风向的粒子。

为了在脚本中创建包含弯曲角的旋转矩阵,同时包含风的方向:

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;

    // NEW =================================================
    [Range(0,1)] public float windStrength;
    [Range(-180,180)] public float windYRotation;
    // 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() {
        if (meshToDraw == null)
            RebuildPrecipitationMesh();

        // NEW =================================================
        // the higher the windstrength, the more the precipitation
        // "leans" in the direction of the wind (with a max lean angle of 45 degrees)
        float windStrengthAngle = Mathf.Lerp(0, 45, windStrength);

        Vector3 windRotationEulerAngles = new Vector3(
            -windStrengthAngle,
            windYRotation,
            0
        );

        // we need to supply the shader with the rotation matrix so it can "fall" in the correct direction
        Matrix4x4 windRotationMatrix = Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(windRotationEulerAngles), Vector3.one);
        // NEW =================================================
            
        float maxTravelDistance = gridHandler.gridSize;

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

    void RenderEnvironmentParticles(EnvironmentParticlesSettings settings, Material material, float maxTravelDistance, Matrix4x4 windRotationMatrix) {
        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);
        material.SetFloat("_MaxTravelDistance", maxTravelDistance);

        // NEW =================================================
        material.SetMatrix("_WindRotationMatrix", windRotationMatrix);
        // 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

shader头文件中我们使用_WindRotationMatrix来旋转顶点位置:

#include "UnityCG.cginc"

#if defined (RAIN)
float4x4 rotationMatrix90(float3 axis) {    
    // [ UNCHANGED ]
}
#endif

sampler2D _MainTex;
sampler2D _NoiseTex;
float _GridSize;
float _Amount;
float2 _CameraRange;
float _FallSpeed;
float _MaxTravelDistance;
float2 _FlutterFrequency;
float2 _FlutterSpeed;
float2 _FlutterMagnitude;
float4 _Color;
float4 _ColorVariation;
float2 _SizeRange; 

// NEW =================================================
float4x4 _WindRotationMatrix;
// NEW =================================================

struct MeshData {
    // [ UNCHANGED ]
};
MeshData vert(MeshData meshData) {
    // [ UNCHANGED ]
}
struct g2f{
    // [ UNCHANGED ]
};
void AddVertex (inout TriangleStream<g2f> stream, float3 vertex, float2 uv, float colorVariation, float opacity) {      
    // [ UNCHANGED ]
}
void CreateQuad (inout TriangleStream<g2f> 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<g2f> 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 =================================================
    // cast the wind rotation matrix, since we only need it to 
    // multiply 3 component vectors
    float3x3 windRotation = (float3x3)_WindRotationMatrix;
    // NEW =================================================

    pos.y -= (_Time.y + 10000) * (_FallSpeed + (_FallSpeed * noise.y));
    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;
    pos.y = fmod(pos.y, -_MaxTravelDistance) + noise.x;

    // NEW =================================================
    // rotate the position so the "travel direction" is where it would be
    // relative to the wind
    pos = mul(windRotation, pos);
    // 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;
        
    // turn opacity to 1 to debug rotation:
    opacity = 1;

    if (opacity <= 0)
        return;
    float colorVariation = (sin(noise.x * (pos.x + pos.y * noise.y + pos.z + _Time.y * 2)) * .5 + .5) * _ColorVariation.a;
    float2 quadSize = lerp(_SizeRange.x, _SizeRange.y, noise.x);

#if defined (RAIN)
    quadSize.x *= .01;
    float3 quadUpDirection = float3(0, 1, 0);
    float3 topMiddle = pos + quadUpDirection * quadSize.y;
    float3 rightDirection = float3(.5 * quadSize.x, 0, 0);
#else
    float3 quadUpDirection = UNITY_MATRIX_IT_MV[1].xyz;
    float3 topMiddle = pos + quadUpDirection * quadSize.y;
    float3 rightDirection = UNITY_MATRIX_IT_MV[0].xyz * .5 * quadSize.x;
#endif

    CreateQuad (stream, pos, topMiddle, rightDirection, colorVariation, opacity);

#if defined (RAIN)
    rightDirection = mul((float3x3)rotationMatrix90(quadUpDirection), rightDirection);
    CreateQuad (stream, pos, topMiddle, rightDirection, colorVariation, opacity);
#endif

}

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

我们会发现旋转太统一了,我们可以计算旋转储是位置的偏移来解决,:

#include "UnityCG.cginc"

#if defined (RAIN)
float4x4 rotationMatrix90(float3 axis) {    
    // [ UNCHANGED ]
}
#endif

sampler2D _MainTex;
sampler2D _NoiseTex;
float _GridSize;
float _Amount;
float2 _CameraRange;
float _FallSpeed;
float _MaxTravelDistance;
float2 _FlutterFrequency;
float2 _FlutterSpeed;
float2 _FlutterMagnitude;
float4 _Color;
float4 _ColorVariation;
float2 _SizeRange; 
float4x4 _WindRotationMatrix;

struct MeshData {
    // [ UNCHANGED ]
};
MeshData vert(MeshData meshData) {
    // [ UNCHANGED ] 
}
struct g2f {
    // [ UNCHANGED ]
};
void AddVertex (inout TriangleStream<g2f> stream, float3 vertex, float2 uv, float colorVariation, float opacity){      
    // [ UNCHANGED ]
}
void CreateQuad (inout TriangleStream<g2f> 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<g2f> 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;
    
    float3x3 windRotation = (float3x3)_WindRotationMatrix;

    // NEW =================================================
    // cache the offset we get from rotatting the particle with the wind
    float3 rotatedVertexOffset = mul(windRotation, pos) - pos;
    // NEW =================================================
    
    pos.y -= (_Time.y + 10000) * (_FallSpeed + (_FallSpeed * noise.y));
    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;
    pos.y = fmod(pos.y, -_MaxTravelDistance) + noise.x;

    pos = mul(windRotation, pos);
    
    // NEW =================================================
    // this rotates the entire mesh quad, so we counteract that tilt
    pos -= rotatedVertexOffset;
    // 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;
    opacity = 1;
    if (opacity <= 0)
        return;
    float colorVariation = (sin(noise.x * (pos.x + pos.y * noise.y + pos.z + _Time.y * 2)) * .5 + .5) * _ColorVariation.a;
    float2 quadSize = lerp(_SizeRange.x, _SizeRange.y, noise.x);
#if defined (RAIN)
    quadSize.x *= .01;
    float3 quadUpDirection = float3(0, 1, 0);
    float3 topMiddle = pos + quadUpDirection * quadSize.y;
    float3 rightDirection = float3(.5 * quadSize.x, 0, 0);
#else
    float3 quadUpDirection = UNITY_MATRIX_IT_MV[1].xyz;
    float3 topMiddle = pos + quadUpDirection * quadSize.y;
    float3 rightDirection = UNITY_MATRIX_IT_MV[0].xyz * .5 * quadSize.x;
#endif
    CreateQuad (stream, pos, topMiddle, rightDirection, colorVariation, opacity);
#if defined (RAIN)
    rightDirection = mul((float3x3)rotationMatrix90(quadUpDirection), rightDirection);
    CreateQuad (stream, pos, topMiddle, rightDirection, colorVariation, opacity);
#endif

}

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

仔细观察的话,会在底部发现雪花会突然变细,这是因为雪花直在_MaxTravelDistance内下落。雪花直接下落这么做可以,但如果有一定角度的话, 我们需要对使用基于风强角和邻近网格边构成三角形的斜边进行移动:


雨滴会发现另一个纹理,即使应用了风,它还是指向上方,我们可以旋转雨滴四边形的上方向:

#include "UnityCG.cginc"

#if defined (RAIN)
float4x4 rotationMatrix90(float3 axis) {    
   // [ UNCHANGED ]
}
#endif

sampler2D _MainTex;
sampler2D _NoiseTex;
float _GridSize;
float _Amount;
float2 _CameraRange;
float _FallSpeed;
float _MaxTravelDistance;
float2 _FlutterFrequency;
float2 _FlutterSpeed;
float2 _FlutterMagnitude;
float4 _Color;
float4 _ColorVariation;
float2 _SizeRange; 
float4x4 _WindRotationMatrix;

struct MeshData {
    // [ UNCHANGED ]
};
MeshData vert(MeshData meshData) {
    // [ UNCHANGED ]
}
struct g2f {
    // [ UNCHANGED ]
};
void AddVertex (inout TriangleStream<g2f> stream, float3 vertex, float2 uv, float colorVariation, float opacity) {      
    // [ UNCHANGED ]
}
void CreateQuad (inout TriangleStream<g2f> 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<g2f> 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;
    float3x3 windRotation = (float3x3)_WindRotationMatrix;
    float3 rotatedVertexOffset = mul(windRotation, pos) - pos;
    pos.y -= (_Time.y + 10000) * (_FallSpeed + (_FallSpeed * noise.y));
    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;
    pos.y = fmod(pos.y, -_MaxTravelDistance) + noise.x;
    pos = mul(windRotation, pos);
    pos -= rotatedVertexOffset;
    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;
        
    // turn opacity to 1 to debug rotation:
    opacity = 1;

    if (opacity <= 0)
        return;

    float colorVariation = (sin(noise.x * (pos.x + pos.y * noise.y + pos.z + _Time.y * 2)) * .5 + .5) * _ColorVariation.a;
    float2 quadSize = lerp(_SizeRange.x, _SizeRange.y, noise.x);

#if defined (RAIN)
    quadSize.x *= .01;
    
    // NEW =================================================
    // rain is stretched in the direction of the wind
    float3 quadUpDirection = mul(windRotation, float3(0,1,0));
    // NEW =================================================
    
    float3 topMiddle = pos + quadUpDirection * quadSize.y;
    float3 rightDirection = float3(.5 * quadSize.x, 0, 0);
    
#else
    float3 quadUpDirection = UNITY_MATRIX_IT_MV[1].xyz;
    float3 topMiddle = pos + quadUpDirection * quadSize.y;
    float3 rightDirection = UNITY_MATRIX_IT_MV[0].xyz * .5 * quadSize.x;
#endif
    CreateQuad (stream, pos, topMiddle, rightDirection, colorVariation, opacity);
#if defined (RAIN)
    rightDirection = mul((float3x3)rotationMatrix90(quadUpDirection), rightDirection);
    CreateQuad (stream, pos, topMiddle, rightDirection, colorVariation, opacity);
#endif

}

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

至此,简单的天气系统完成。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容