一、SnowTrack(雪跟蹤)
前言
這篇小文簡單介紹一下如何在Unity中利用shader很簡單的實現雪地效果。
01 雪地痕跡的效果
實現雪地印痕的思路其實也很簡單嗎,既記錄玩家移動過程中的位置,之后再根據這些數據修改雪地的mesh即可。
02 工程實現
所以,很簡單的,我們在unity中只需要一個玩家頭頂上的正交相機和一個rendertexture就可以記錄玩家的移動過程中的位置了。
之后再shader文件中先用vs根據rendertexture的數據修改雪地mesh的相關頂點位置,同時為了更方便地實現光照效果,接下來使用surface shader,實現光照。
全部代碼如下所示:
Properties {
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_SnowTrackTex ("SnowTrackTex", 2D) = "white" {}
_NormalMap ("NormalMap", 2D) = "bump" {}
_SnowTrackFactor("SnowTrackFactor", float) = 0
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard addshadow fullforwardshadows vertex:vert
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0
sampler2D _MainTex;
sampler2D _SnowTrackTex;
sampler2D _NormalMap;
float _SnowTrackFactor;
struct Input {
float2 uv_MainTex;
};
void vert(inout appdata_full vertex)
{
vertex.vertex.y -= tex2Dlod(_SnowTrackTex, float4(vertex.texcoord.xy, 0, 0)).r * _SnowTrackFactor;
}
void surf (Input IN, inout SurfaceOutputStandard o) {
fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
03 效果圖
04 demo地址
二、Stanford-Bunny-Fur-With-Unity(斯坦福兔子)
前言
這篇小文簡單介紹一下如何在Unity中利用shader很簡單的實現毛皮效果。
01 斯坦福兔子和它的毛
我相信對圖形學感興趣的一定經常會見到這個上鏡率超高的兔子。
關於它的典故各位可以看看斯坦福兔子模型的來源和故事有哪些?
02 工程實現
接下來就開始我們對兔子的改造行動吧。
-
是否需要皮毛的網格數據呢?
答案:是 -
皮毛的網格要根據什么來生成呢?
要生在兔子身上,所以兔子的原始網格信息提供了皮毛的網格信息。 -
那么具體要怎么做?
很簡單,Geometry Shader就是干這個的。而我們只需要根據兔子的網格信息,以每一個triangle為一個單位,在這個triangle上生成一個向外指的"金字塔" 就可以了。
也就是說,在原有triangle的基礎上有新生成了3個指向外向triangle,形成毛皮的效果。
代碼如下:Shader "Unlit/aiting_Shader-s" { Properties { _MainTex ("Texture", 2D) = "white" {} _FurFactor("FurFactor", Range(0.01, 0.05)) = 0.02 } SubShader { Tags{ "RenderType" = "Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma geometry geom #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; }; struct v2g { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct g2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; fixed4 col : COLOR; }; sampler2D _MainTex; float4 _MainTex_ST; float _FurFactor; v2g vert(appdata_base v) { v2g o; o.vertex = v.vertex; o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); o.normal = v.normal; return o; } [maxvertexcount(9)] void geom(triangle v2g IN[3], inout TriangleStream<g2f> tristream) { g2f o; float3 edgeA = IN[1].vertex - IN[0].vertex; float3 edgeB = IN[2].vertex - IN[0].vertex; float3 normalFace = normalize(cross(edgeA, edgeB)); float3 centerPos = (IN[0].vertex + IN[1].vertex + IN[2].vertex) / 3; float2 centerTex = (IN[0].uv + IN[1].uv + IN[2].uv) / 3; centerPos += float4(normalFace, 0) * _FurFactor; for (uint i = 0; i < 3; i++) { o.vertex = UnityObjectToClipPos(IN[i].vertex); o.uv = IN[i].uv; o.col = fixed4(0., 0., 0., 1.); tristream.Append(o); uint index = (i + 1) % 3; o.vertex = UnityObjectToClipPos(IN[index].vertex); o.uv = IN[index].uv; o.col = fixed4(0., 0., 0., 1.); tristream.Append(o); o.vertex = UnityObjectToClipPos(float4(centerPos, 1)); o.uv = centerTex; o.col = fixed4(1.0, 1.0, 1.0, 1.); tristream.Append(o); tristream.RestartStrip(); } } fixed4 frag(g2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv) * i.col; return col; } ENDCG } } }
03 效果圖
因此總共會生成9個頂點,三個新三角形共同組成一根毛。
04 demo地址
https://github.com/chenjd/Stanford-Bunny-Fur-With-Unity
三、Explosion and sand effect(爆炸和砂效果)
前言
這篇文章繼續沿用了同樣來自斯坦福的另一個模型Armadillo,同樣也使用了geometry shader來實現效果的表現。
01 凶惡的怪物和爆炸
當然,用之前的斯坦福兔子的模型做爆炸的效果也是可以的,但是考慮到要讓一個那么可愛的模型變成沙礫總覺得不太好,所以長相自帶怪物屬性的模型Armadillo就成了一個不錯的選擇。
不過另一個讓我選擇Armadillo的原因其實是因為它的面數和頂點數相對來說更多,可以看到它有106289個頂點和212574個多邊形組成,所以用來做爆炸成為沙礫的效果要更好。
02 工程實現
現在讓我們把Armadillo的obj文件導入到Unity內,可以看到這個怪物已經站立在我們的場景內了。接下來我們就要利用geometry shader來實現我們想要的爆炸沙粒化的效果了。
之前提到Geometry Shader的時候,往往是利用它來生成更多新的頂點和多邊形來實現我們期望的效果,例如利用它在GPU上生成草體,實現真實草的實時渲染。
但是Geometry Shader不僅可以生成新的圖元,同時它還可以減少頂點和多邊形的輸出,以實現一些有趣的效果,比如這篇小文章的例子,利用Geometry Shader來實現怪獸的爆炸和沙粒化效果。
而我們要做的也很簡單,就是在Geometry Shader內將輸入的由3個頂點組成的三角形圖元修改為只有一個頂點組成的點圖元。而輸出的這個點的坐標我們可以很簡單的使用三角形的中心點坐標。
這樣,組成怪獸的網格就由三角形圖元變成了點圖元,而且頂點數量也隨之減少,至於怪物本身也變成了下面這個樣子。
但是這個時候的模型是靜止的,因此也看不出爆炸甚至是沙礫的效果。所以接下來我們就要讓怪物的模型隨着時間運動起來。
而一個大家都知道的運動學公式就可以用來實現這個效果:
其中的S就是頂點的最新位置,v0和a的值可以作為一個uniform變量傳入shader,運動方向可以是沿着三角形的法線方向,而t的來源則是Unity內置的變量_Time的y分量。
這樣,需要的幾個變量我們就有了:之后只要帶入運動學公式就好了。
代碼如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour
{
public Material ExplosionMaterial;
private bool isClicked;
void Update()
{
if (this.isClicked || this.ExplosionMaterial == null)
{
return;
}
if (Input.GetMouseButton(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit Hit;
if (Physics.Raycast(ray, out Hit))
{
MeshRenderer[] renderers = Hit.collider.GetComponentsInChildren<MeshRenderer>();
this.ExplosionMaterial.SetFloat("_StartTime", Time.timeSinceLevelLoad);
for (int i = 0; i < renderers.Length; i++)
{
renderers[i].material = this.ExplosionMaterial;
}
this.isClicked = true;
}
}
}
}
shader代碼:
Shader "Unlit/aiting_shader-m"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Speed("Speed", Float) = 10
_AccelerationValue("AccelerationValue", Float) = 10
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma geometry geom
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2g
{
float2 uv : TEXCOORD0;
float4 vertex : POSITION;
};
struct g2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float _Speed;
float _AccelerationValue;
float _StartTime;
v2g vert (appdata v)
{
v2g o;
o.vertex = v.vertex;
o.uv = v.uv;
return o;
}
[maxvertexcount(1)]
void geom(triangle v2g IN[3], inout PointStream<g2f> pointStream)
{
g2f o;
float3 v1 = IN[1].vertex - IN[0].vertex;
float3 v2 = IN[2].vertex - IN[0].vertex;
float3 norm = normalize(cross(v1, v2));
float3 tempPos = (IN[0].vertex + IN[1].vertex + IN[2].vertex) / 3;
float realTime = _Time.y - _StartTime;
tempPos += norm * (_Speed * realTime + .5 * _AccelerationValue * pow(realTime, 2));
o.vertex = UnityObjectToClipPos(tempPos);
o.uv = (IN[0].uv + IN[1].uv + IN[2].uv) / 3;
pointStream.Append(o);
}
fixed4 frag (g2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
03 效果圖
04 demo地址
https://github.com/chenjd/Unity-Miscellaneous-Shaders
Unity-Boids-Behavior-on-GPGPU(海鷗群/魚群)
前言
在今年六月的Unity Europe 2017大會上unity的CTO Joachim Ante演示了未來unity新的編程特性--C# Job系統,它提供了編寫多線程代碼的一種既簡單又安全的方法。Joachim通過一個大規模群落行為仿真的演示,向我們展現了最新的Job系統是如何充分利用CPU多核架構的優勢來提升性能的。
但是吸引我的並非是C# Job如何利用多線程實現性能的提升,相反,吸引我的是如何在現在還沒有C# Job系統的Unity中實現類似的效果。
在Ante的session中,他的演示主要是利用多核CPU提高計算效率來實現大群體行為。那么我就來演示一下,如何利用GPU來實現類似的目標吧。利用GPU做一些非渲染的計算也被稱為GPGPU——General-purpose computing on graphics processing units,圖形處理器通用計算。
01 CPU的限制
為何Joachim 要用這種大規模群落行為的仿真來宣傳Unity的新系統呢?
其實相對來說復雜的並非邏輯,這里的關鍵詞是“大規模”——在他的演示中,實現了20,000個boid的群體效果,而更牛逼的是幀率保持在了40fps上下。
事實上自然界中的這種群體行為並不罕見,例如大規模的鳥群,大規模的魚群。
在搜集資料的時候,我還發現了一位優秀的水下攝影師、加利福尼亞海灣海洋計划總監octavio aburto的個人網站上的一些讓人驚嘆的作品。
圖片來自OctavioAburto
而要在計算機上模擬出這種自然界的現象,乍看上去似乎十分復雜,但實際上卻並非如此。
查閱資料,可以發現早在1986年就由Craig Reynolds提出了一個邏輯簡單,而效果很贊的群體仿真模型——而作為這個群體內的個體的專有名詞boid(bird-oid object,類鳥物)也是他提出的。
簡單來說,一個群體內的個體包括3種基本的行為:
-
Separation:顧名思義,該個體用來規避周圍個體的行為。
-
Alignment:作為一個群體,要有一個大致統一的前進方向。因此作為群體中的某個個體,可以根據自己周圍的同伴的前進方向獲取一個前進方向。
- Cohesion:同樣,作為一個群體肯定要有一個向心力。否則隊伍四散奔走就不好玩了,因此每個個體就可以根據自己周圍同伴的位置信息獲取一個向中心聚攏的方向。
以上三種行為需要同時加以考慮,才有可能模擬出一個接近真實的效果。
可以看出,這里的邏輯並不復雜,但是麻煩的問題在於實現這套邏輯的前提是每個個體boid都需要獲取自己周圍的同伴信息。
因此最簡單也最通用的方式就是每個boid都要和群落中的所有boid比較位置信息,獲取二者之間的距離,如果小於閾值則判定是自己周圍的同伴。而這種比較的時間復雜度顯然是O( n^2)。因此,當群體是由幾百個個體組成時,直接在cpu上計算時的表現還是可以接受的。但是數量一旦繼續上升,效果就很難保證了。
當然,在Unity中我們還可以利用它的物理組件來獲取一個boid個體周圍的同伴信息
這個方法會返回和自己重疊的對象列表,由於unity使用了空間划分的機制,所以這種方式的性能要好於直接比較n個boid之間的距離。
但是即便如此,cpu的計算能力仍然是一個瓶頸。隨着群體個體數量的上升,性能也會快速的下降。
02 GPU的優勢
既然限制的瓶頸在於CPU面對大規模個體時的計算能力的不足,那么一個自然的想法就是將這部分計算轉移到更擅長大規模計算的GPU上來進行.
CPU的結構復雜,主要完成邏輯控制和緩存功能,運算單元較少。與CPU相比,GPU的設計目的是盡可能的快速完成圖像處理,通過簡化邏輯控制並增加運算單元實現了高性能的並行計算。
利用GPU的超強計算能力來實現一些渲染之外的功能並非一個新的概念,早在十年前nvidia就為GPU引入了一個易用的編程接口,即CUDA統一計算架構,之后微軟推出了DirectCompute——它隨DirectX 11一同發布。
和常見的vertex shader和fragment shader類似,要在GPU運行我們自己設定的邏輯也需要通過shader,不過和傳統的shader的不同之處在於,compute shader並非傳統的渲染流水線中的一個階段,相反它主要用來計算原本由CPU處理的通用計算任務,這些通用計算常常與圖形處理沒有任何關系,因此這種方式也被稱為GPGPU——General-purpose computing on graphics processing units,圖形處理器通用計算。
利用這些功能,之前由CPU來實現的計算就可以轉移到計算能力更強大的GPU上來進行了,比如物理計算、AI等等。
而Unity的Compute Shader十分接近DirectCompute,最初Unity引入Compute Shader時僅僅支持DirectX 11,不過目前的版本已經支持別的圖形API了。詳情可以參考:Unity - Manual: Compute shaders。
在Unity中我們可以很方便的創建一個Compute Shader,
這里我先簡單的介紹一下這個Compute Shader中的相關概念,首先在這里我們指明了這個shader的入口函數。之后,聲明了在compute shader中操作的數據。
這里使用的是RWTexture2D,而我們更常用的是RWStructuredBuffer(RW在這里表示可讀寫)。
之后是很關鍵的一行:[numthreads(8,8,1)]
這里首先要說一下Compute Shader執行的線程模型。DirectCompute將並行計算的問題分解成了多個線程組,每個線程組內又包含了多個線程。
[numthreads(8,8,1)]的意思是在這個線程組中分配了8x8x1=64個線程,當然我們也可以直接使用
因為三維線程模型主要是為了方便某些使用情景,和性能關系不大,硬件在執行時仍然是把所有線程當做一維的。
至此,我們已經在shader中確定了每個線程組內包括幾個線程,但是我們還沒有分配線程組,也沒有開始執行這個shader。
和一般的shader不同,compute shader和圖形無關,因此在使用compute shader時不會涉及到mesh、material這些內容。相反,compute shader的設置和執行要在c#腳本中進行。
在c#腳本中准備、傳送數據,分配線程組並執行compute shader,最后數據再從GPU傳遞回CPU。
不過,這里有一個問題需要說明。雖然現在將計算轉移到GPU后計算能力已經不再是瓶頸,但是數據的轉移此時變成了首要的限制因素。而且在Dispatch之后直接調用GetData可能會造成CPU的阻塞。因為CPU此時需要等待GPU計算完畢並將數據傳遞回CPU,所以希望日后Unity能夠提供一個異步版本的GetData。
最后將行為模擬的邏輯從CPU轉移到GPU之后,模擬10,000個boid組成的大群組在我的筆記本上已經能跑在30FPS上下了。
03 工程實現
C#代碼如下
GPUBoid腳本 :
using UnityEngine;
public struct GPUBoid
{
public Vector3 pos, rot, flockPos;
public float speed, nearbyDis, boidsCount;
}
GPUFlock腳本:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;
public class GPUFlock : MonoBehaviour
{
#region 字段
public ComputeShader CShader;
public GameObject boidPrefab;
public int boidsCount;
public float spawnRadius;
public GameObject[] boidsGo;
public GPUBoid[] boidsData;
public float flockSpeed;
public float nearbyDis;
private Vector3 targetPos = Vector3.zero;
private int kernelHandle;
#endregion
#region 方法
void Start()
{
this.boidsGo = new GameObject[this.boidsCount];
this.boidsData = new GPUBoid[this.boidsCount];
this.kernelHandle = CShader.FindKernel("CSMain");
for (int i = 0; i < this.boidsCount; i++)
{
this.boidsData[i] = this.CreatBoidData();
this.boidsGo[i] = Instantiate(boidPrefab, this.boidsData[i].pos, Quaternion.Euler(this.boidsData[i].rot)) as GameObject;
this.boidsData[i].rot = this.boidsGo[i].transform.forward;
}
}
GPUBoid CreatBoidData()
{
GPUBoid boidData = new GPUBoid();
Vector3 pos = transform.position + Random.insideUnitSphere * spawnRadius;
Quaternion rot = Quaternion.Slerp(transform.rotation, Random.rotation, 0.3f);
boidData.pos = pos;
boidData.flockPos = transform.position;
boidData.boidsCount = this.boidsCount;
boidData.nearbyDis = this.nearbyDis;
boidData.speed = this.flockSpeed + Random.Range(-0.5f, 0.5f);
return boidData;
}
void Update()
{
this.targetPos += new Vector3(2f, 5f, 3f);
this.transform.localPosition += new Vector3(
(Mathf.Sin(Mathf.Deg2Rad * this.targetPos.x) * -0.2f),
(Mathf.Sin(Mathf.Deg2Rad * this.targetPos.y) * 0.2f),
(Mathf.Sin(Mathf.Deg2Rad * this.targetPos.z) * 0.2f)
);
ComputeBuffer buffer = new ComputeBuffer(boidsCount, 56);
for (int i = 0; i < this.boidsData.Length; i++)
{
this.boidsData[i].flockPos = this.transform.position;
}
buffer.SetData(this.boidsData);
CShader.SetBuffer(this.kernelHandle, "boidBuffer", buffer);
CShader.SetFloat("deltaTime", Time.deltaTime);
CShader.Dispatch(this.kernelHandle, this.boidsCount, 1, 1);
buffer.GetData(this.boidsData);
buffer.Release();
for (int i = 0; i < this.boidsData.Length; i++)
{
this.boidsGo[i].transform.localPosition = this.boidsData[i].pos;
if (!this.boidsData[i].rot.Equals(Vector3.zero))
{
this.boidsGo[i].transform.rotation = Quaternion.LookRotation(this.boidsData[i].rot);
}
}
}
#endregion
}
RotateForDemo腳本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RotateForDemo : MonoBehaviour {
void Update () {
transform.localRotation = Quaternion.AngleAxis(10 * Time.deltaTime, Vector3.up) * transform.localRotation;
}
}
Shader代碼如下:
// 用來在gpu上實現集群效果
//
#pragma kernel CSMain
//封裝計算單個boid時所需要的數據
struct Boid
{
float3 pos;
float3 rot;
float3 flockPos;
float speed;
float nearbyDis;
float boidsCount;
};
RWStructuredBuffer<Boid> boidBuffer;
float deltaTime;
[numthreads(128, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
Boid boid = boidBuffer[id.x];
float3 pos = boid.pos;
float3 rot = boid.rot;
//separation
float3 separation = float3(0.0, 0.0, 0.0);
//alignment
float3 alignment = float3(0.0, 0.0, 0.0);
//cohesion
float3 cohesion = boid.flockPos;
float3 tempCohesion = float3(0.0, 0.0, 0.0);
float tempSpeed = 0;
uint nearbyCount = 0;
[loop]
for (int i = 0; i < int(boid.boidsCount); i++)
{
if (i != int(id.x))
{
Boid tempBoid = boidBuffer[i];
if (length(boid.pos - tempBoid.pos) < boid.nearbyDis)
{
separation += boid.pos - tempBoid.pos;
alignment += tempBoid.rot;
tempCohesion += tempBoid.pos;
nearbyCount++;
}
}
}
if (nearbyCount > 0)
{
alignment *= 1 / nearbyCount;
tempCohesion *= 1 / nearbyCount;
}
cohesion += tempCohesion;
float3 direction = alignment + separation + normalize(cohesion - boid.pos);
boid.rot = lerp(boid.rot, normalize(direction), deltaTime * 4);
boid.pos += boid.rot * boid.speed * deltaTime;
boidBuffer[id.x] = boid;
}
效果圖
demo地址
https://github.com/chenjd/Unity-Boids-Behavior-on-GPGPU
本項目是學習總結而來地址如下:
陳嘉棟