Unity之几何着色器--草随风摇曳

1. 简介12

1.1 主要工作:

  1. 通过HeightMap生成地形网格
  2. 通过分块的思想生成草的初始定位顶点
  3. 通过几何着色器配合伪随机数生成草的网格
  4. 通过伪随机函数来对风进行模拟
  5. 通过Blinn Phong 光照模型进行光照渲染

2. 方法

2.1 地形的生成

2.1.1 获取HeightMap

USGS获取世界任何一个区域的HeightMap 3

(1)说明

  1. HeightMap格式为png、jpg等。
  2. 在Unity中还需要对高层纹理图片进行可读设置
    在这里插入图片描述
  3. unity中网格顶线有上限:65535。因此把地形大小设置为250*250。

2.1.2 实现思路

在C#脚本中实现:

感觉这里这部分可以通过Shader实现,后续进行研究

  1. 定义网格顶点集合。
  2. 定义网格三角面片集合。
  3. 对Texture2D纹理进行采样,取出图片中的灰度值,通过参数进行高度值还原。
  4. 定义顶点的位置 pos =(x,height,z)。
  5. 把三角面前以顶点形式存入三角面片集合,一次存入6个顶点,也就是2个三角面片,正好组成一个矩形面片。
  6. 根据生成顶点的数量定义uv集合。
  7. 定义地形网格对象,把顶点集合、三角面片集合转换成数组形式,并带上uv数组,把三者赋予网格对象的属性。
  8. 在场景中生成Terrian对象,并添加MeshFilterMeshRender,同时赋予MeshMaterial
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个顶点进行数组化操作,并使用MeshSetIndices函数以点的形式对网格进行初始化。创建草的GameObject添加MeshFilterMeshRenderer,并赋予相应的MeshMaterial。每批次草根网格创建后都需要对草根进行上一批次顶点的清空操作!

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. 几何着色器
  1. 规定每株草需要生成的顶点数量,初始化顶点数组。
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(),
};
  1. 通过伪随机数生成草网格顶点的位置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;
  1. 遍历草的每个顶点

显而易见,草的偶数顶点,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;
    }
  1. 构建三角形
//在三角形输出流中将顶点加入其中,自动构建三角形
for (int p = 0; p < (vertexCount - 2); p++)
{
    
    
	triStream.Append(v[p]);
	triStream.Append(v[p + 1]);
	triStream.Append(v[p + 2]);
}
  1. 让草的分布更加随机

目前草都是朝一个方向进行生成,如果转动摄像机会法线到了一定角度由于“面片草”的效果,会出现缝隙的视觉效果,就好像一条小路一样。于是,需要对草进行随机旋转。因此,也是先通过正弦函数生成伪随机数作为旋转参数。之后使用旋转矩阵进行旋转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);
  1. 风动效果

由于定义了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);
  1. 片段着色器

使用基础光照模型进行光照渲染,其中加入一个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. 参考


  1. https://zhuanlan.zhihu.com/p/119307479 ↩︎

  2. https://zhuanlan.zhihu.com/p/29632347 ↩︎

  3. 知乎:如何获取高度图 ↩︎

  4. CSDN:伪随机数生成 ↩︎

  5. 旋转矩阵 ↩︎

猜你喜欢

转载自blog.csdn.net/weixin_38708854/article/details/110172558
今日推荐