Unity之几何着色器--草随风摇曳
1. 简介12
1.1 主要工作:
- 通过HeightMap生成地形网格
- 通过分块的思想生成草的初始定位顶点
- 通过几何着色器配合伪随机数生成草的网格
- 通过伪随机函数来对风进行模拟
- 通过Blinn Phong 光照模型进行光照渲染
2. 方法
2.1 地形的生成
2.1.1 获取HeightMap
(1)说明
- HeightMap格式为png、jpg等。
- 在Unity中还需要对高层纹理图片进行可读设置
- unity中网格顶线有上限:65535。因此把地形大小设置为250*250。
2.1.2 实现思路
在C#脚本中实现:
感觉这里这部分可以通过Shader实现,后续进行研究
- 定义网格顶点集合。
- 定义网格三角面片集合。
- 对Texture2D纹理进行采样,取出图片中的灰度值,通过参数进行高度值还原。
- 定义顶点的位置 pos =(x,height,z)。
- 把三角面前以顶点形式存入三角面片集合,一次存入6个顶点,也就是2个三角面片,正好组成一个矩形面片。
- 根据生成顶点的数量定义uv集合。
- 定义地形网格对象,把顶点集合、三角面片集合转换成数组形式,并带上uv数组,把三者赋予网格对象的属性。
- 在场景中生成
Terrian
对象,并添加MeshFilter
和MeshRender
,同时赋予Mesh
和Material
。
private void GenerateTerrain()
{
//要生成一个平面,我们需要自定义其顶点和网格数据
List<Vector3> vertexs = new List<Vector3>();
List<int> tris = new List<int>();
for (int i = 0; i < terrainSize; i++) {
for (int j = 0; j < terrainSize; j++) {
vertexs.Add(new Vector3(i,heightMap.GetPixel(i,j).r * terrainHeight,j));
if (i == 0 || j == 0) {
continue;
}
tris.Add(terrainSize * i + j);
tris.Add(terrainSize * i + j - 1);
tris.Add(terrainSize * (i-1) + j-1);
tris.Add(terrainSize * (i-1) + j-1);
tris.Add(terrainSize * (i-1) + j);
tris.Add(terrainSize * i + j);
}
}
Vector2[] uvs = new Vector2[vertexs.Count];
for (int i = 0; i < uvs.Length; i++) {
uvs[i] = new Vector2(vertexs[i].x,vertexs[i].z);
}
GameObject plane = new GameObject("Terrian");
plane.AddComponent<MeshFilter>();
MeshRenderer renderer = plane.AddComponent<MeshRenderer>();
renderer.sharedMaterial = terrainMat;
Mesh groundMesh = new Mesh();
groundMesh.vertices = vertexs.ToArray();
groundMesh.uv = uvs;
groundMesh.triangles = tris.ToArray();
groundMesh.RecalculateNormals();
plane.GetComponent<MeshFilter>().mesh = groundMesh;
verts.Clear();
}
2.2 草地的生成
2.2.1 实现思路
把草放到一个Mesh容器中,每个Mesh中存储草根部顶点位置信息,之后通过几何着色器生成每株草的顶点网格。由于Mesh的顶线存在上限(65535),因此如果再草根顶点数量超过了65000(人为规定)后需要创建新的Mesh来存储草根顶点。
C#脚本
(1)草的初始位置
区域划分 首先把地形区域划分为若干个Patch
小块,遍历地形区域中的Patch
。在每个Patch中随机生成一定数量的草根顶点。
草根位置 对于草根顶点的位置,其高度(y)需要结合地形的高度,因此还需要从高度图中采样像素获取灰度值,从而得出高度。之后把草根顶点存入定义好的草根顶点集合中。
private void GenerateGrassArea(int patchCount, int countPerPatch)
{
List<int> indices = new List<int>();
//Unity网格顶点上限65535
for (int i = 0; i < 65000; i++)
{
indices.Add(i);
}
//设置循环起始位置
var startPosition = new Vector3(0, 0, 0);
//计算每次循环后位置的偏移量,即“步幅”
var patchSize = new Vector3(terrainSize / patchCount, 0, terrainSize / patchCount);
for (int x = 0; x <= patchCount; x++)
{
for (int y = 0; y <= patchCount; y++)
{
//调用另一个函数来在startPosition的周围生成更多的随机分布的点,这些点即为上文提到的“草根集”
this.GenerateGrass(startPosition, patchSize, countPerPatch);
startPosition.x += patchSize.x;
}
startPosition.x = 0;
startPosition.z += patchSize.z;
}
Mesh mesh;
GameObject grassLayer;
MeshFilter meshFilter;
MeshRenderer renderer;
int a = 0;
while (verts.Count > 65000) {
mesh = new Mesh();
mesh.vertices = verts.GetRange(0,65000).ToArray();
mesh.SetIndices(indices.ToArray(),MeshTopology.Points,0);
grassLayer = new GameObject("grasslayer" + a++);
meshFilter = grassLayer.AddComponent<MeshFilter>();
renderer = grassLayer.AddComponent<MeshRenderer>();
renderer.sharedMaterial = grassMat;
meshFilter.mesh = mesh;
verts.RemoveRange(0,65000);
}
grassLayer = new GameObject("grassLayer" + a);
mesh = new Mesh();
mesh.vertices = verts.ToArray();
// 通过点来创建网格
mesh.SetIndices(indices.GetRange(0, verts.Count).ToArray(), MeshTopology.Points, 0);
meshFilter = grassLayer.AddComponent<MeshFilter>();
meshFilter.mesh = mesh;
renderer = grassLayer.AddComponent<MeshRenderer>();
renderer.sharedMaterial = grassMat;
}
(2)草的生成
对于获取的草根顶点集合,规定每批次最大生成量为65000,因此,先从草根集合中选出0-65000个顶点进行数组化操作,并使用Mesh
的SetIndices
函数以点的形式对网格进行初始化。创建草的GameObject
添加MeshFilter
和MeshRenderer
,并赋予相应的Mesh
和Material
。每批次草根网格创建后都需要对草根进行上一批次顶点的清空操作!
private void GenerateGrass(Vector3 pos, Vector3 patchSize, int countPerPatch)
{
for (int i = 0; i < countPerPatch; i++) {
var randomX = Random.value * patchSize.x;
var randomZ = Random.value * patchSize.z;
int indexX = (int)(pos.x + randomX);
int indexZ = (int)(pos.z + randomZ);
//防止种草种出地形
if (indexX >= terrainSize)
{
indexX = (int)terrainSize - 1;
}
if (indexZ >= terrainSize)
{
indexZ = (int)terrainSize - 1;
}
//添加此次循环生成的点的位置
Vector3 currentPos = new Vector3(pos.x + randomX, heightMap.GetPixel(indexX, indexZ).grayscale * terrainHeight, pos.z + randomZ);
verts.Add(currentPos);
}
}
(3)草的渲染
Shader脚本
1. 顶点着色器:简单起见直接传递数据到几何着色器2. 几何着色器
- 规定每株草需要生成的顶点数量,初始化顶点数组。
struct g2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
g2f createGSOut()
{
g2f output;
output.pos = float4(0, 0, 0, 0);
output.normal = float3(0, 0, 0);
output.uv = float2(0, 0);
return output;
}
//我们将使用12个顶点来作为每根草的网格顶点
const int vertexCount = 12;
//初始化g2f数组
g2f v[vertexCount] = {
createGSOut(), createGSOut(), createGSOut(), createGSOut(),
createGSOut(), createGSOut(), createGSOut(), createGSOut(),
createGSOut(), createGSOut(), createGSOut(), createGSOut(),
};
- 通过伪随机数生成草网格顶点的位置4
首先获取伪随机数因子,之后规定每株草的宽度和高度。之后,对草的uv偏移量进行初始化,还有草高度的偏移量进行初始化
float random = frac(sin(UNITY_HALF_PI * root.x * 5000.0f) * 100000.0f);
//给每根草的长宽加上这个随机值,我们希望草的宽度不要太宽
// 这两项被定义为全局变量
_Width = _Width + (random / 50);
_Height = _Height + (random / 5);
// uv主要在v轴上进行划分,因为u轴上就两点0,1不需要再划分
float current_uv = 0.0f;
float offset_uv = 1.0 / (vertexCount / 2 - 1);
// 两侧顶点数量一样,故而用一侧的顶点数量在高度上进行平均划分
float currentVertexHeight = 0.0f;
float currentHeightOffset = 1.0 / (vertexCount / 2 -1 );
// 初始化风力因子
float windCoEff = 0.0f;
- 遍历草的每个顶点
显而易见,草的偶数顶点,uv坐标中u为0;奇数索引,uv坐标中u为1。因此根据顶点索引的奇偶来对草顶点进行设置。根据偏移量设置每个顶点的uv坐标和位置值。同时,草越高的顶点所受风力影响越大,因此,也对每个顶点的风力参数进行设置。由此,我们便获得了全部的草顶点。
for (int i = 0; i < vertexCount; i++)
{
// 简化了法线,直接手动设置
v[i].normal = float3(0, 0, 1);
if (fmod(i, 2) == 0)
{
v[i].pos = float4(root.x - _Width, root.y + currentVertexHeight, root.z, 1);
v[i].uv = fixed2(0, current_uv);
}
else
{
v[i].pos = float4(root.x + _Width, root.y + currentVertexHeight, root.z, 1);
v[i].uv = fixed2(1, current_uv);
// 对uv坐标进行偏移
current_uv += offset_uv;
// 对y轴方向,即草的高度进行偏移
currentVertexHeight += currentHeightOffset * _Height;
// 顶点越高收到风的影响越大,使用uv的偏移量数值来模拟风力影响参数的增大
windCoEff += offset_uv;
}
- 构建三角形
//在三角形输出流中将顶点加入其中,自动构建三角形
for (int p = 0; p < (vertexCount - 2); p++)
{
triStream.Append(v[p]);
triStream.Append(v[p + 1]);
triStream.Append(v[p + 2]);
}
- 让草的分布更加随机
目前草都是朝一个方向进行生成,如果转动摄像机会法线到了一定角度由于“面片草”的效果,会出现缝隙的视觉效果,就好像一条小路一样。于是,需要对草进行随机旋转。因此,也是先通过正弦函数生成伪随机数作为旋转参数。之后使用旋转矩阵进行旋转5。
/*
* 通过正弦噪声做随机角度旋转变换
*/
fixed randomAngle = frac(sin(root.x* 5000) * 100000.0) * UNITY_HALF_PI;
float4x4 transformToOriginal = float4x4(
1, 0, 0, -root.x,
0, 1, 0, -root.y,
0, 0, 1, -root.z,
0, 0, 0, 1
);
float4x4 rotatationOnOriginal = float4x4(
cos(randomAngle), 0, sin(randomAngle), 0,
0, 1, 0, 0,
-sin(randomAngle), 0, cos(randomAngle), 0,
0, 0, 0, 1
);
float4x4 transformToPos = float4x4(
1, 0, 0, root.x,
0, 1, 0, root.y,
0, 0, 1, root.z,
0, 0, 0, 1
);
// 计算旋转
float4x4 M = mul(mul(transformToPos, rotatationOnOriginal), transformToOriginal);
v[i].pos = mul(M, v[i].pos);
- 风动效果
由于定义了windCoEff
参数,根据这个参数的含义我们知道,顶点的位置越高,这个参数值越大,因此它是用来控制不同高度草顶点位移程度的因子。配合风力方向于伪随机参数,我们就能模拟草因为风而摆动的效果。
/*
* 通过正弦噪声做风吹动草的效果
*/
float randomDir = float2(sin(random * 15), sin(random * 10));
// 水平方向上草顶点的偏移
v[i].pos.xz += (sin((root.x * 10 + root.z / 5) * random) * windCoEff + randomDir * sin(random * 15))* windCoEff;
// 定义风向
float2 windDir = float2(1, 1);
// 为了等动态发生波浪效果加上时间的影响
float2 wind = windDir * sin(_Time.x * UNITY_PI * _WindSpeed *(root.x*windDir.x + root.z * windDir.y) / 100);
// 更新水平方向上的顶点位置
v[i].pos.xz += wind * _WindForce * windCoEff;
// 计算草被压低的程度
v[i].pos.y -= length(wind * _WindForce * windCoEff);
- 片段着色器
使用基础光照模型进行光照渲染,其中加入一个HDR,可以调节草的色调
fixed4 frag(g2f i):SV_Target
{
// sample the texture
fixed3 col = tex2D(_MainTex, i.uv);
fixed4 alpha = tex2D(_AlphaTex, i.uv);
fixed3 light;
half3 worldNormal = UnityObjectToWorldNormal(i.normal);
//ambient
fixed3 ambient = 0.9;
//diffuse
fixed3 diffuseLight = saturate(dot(worldNormal, UnityWorldSpaceLightDir(i.pos))) * _LightColor0;
//specular Blinn-Phong
fixed3 halfVector = normalize(UnityWorldSpaceLightDir(i.pos) + WorldSpaceViewDir(i.pos));
fixed3 specularLight = pow(saturate(dot(worldNormal, halfVector)), 15) * _LightColor0;
light = ambient + diffuseLight + specularLight;
return fixed4(col * light * _ExtraColor, alpha.r);
}
ENDCG
3. 参考
本文地址:https://blog.csdn.net/weixin_38708854/article/details/110172558
上一篇: 2020-11-26课堂练习
下一篇: 鸿蒙媒体子系统解读-编码录像流程解读