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;
}
}
将脚本附在摄像机上就可以随意移动。
接下来实现网格系统。为了节省性能消耗,我们可以只渲染摄像机一定范围内的效果。这样的话随摄像机移动,效果也会随之移动渲染。但这会有问题,当摄像机移动时,我们可能会在某些位置看到雨雪分界线。
我们可以在世界空间完成一个网格,其中只有网格坐标内的区域会渲染雨滴:
不过在玩家到达边界时会发生问题:
此时玩家会看到明显的雨雪分界线。
为了修复这一点我们可以向每个方向扩大一个网格,在下面的网格渲染效果,这样的话就不会得到边界效果:
这样在世界空间中,我们会为摄像机的活动范围设置一个的网格立方体。我们可以定义一个脚本来显示这一网格范围,帮助我们观察:
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
,并将脚本赋予它,将摄像机赋予该脚本组件。
接下来我们创建要渲染的网格:
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
来绘制实例。
效果如下:
我们不需要每时每刻渲染所有的四边形,因此我们可以基于粒子的数量、距摄像机的距离和是否在摄像机后来设置剔除。
首先更新脚本,包含一个内置设置类:
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 ]
}
之后我们应用纹理,并修改一些细节。