欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  科技

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

程序员文章站 2022-03-07 23:49:07
通过几何着色器生成大片草场...

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中还需要对高层纹理图片进行可读设置
    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. 旋转矩阵 ↩︎

本文地址:https://blog.csdn.net/weixin_38708854/article/details/110172558

相关标签: unity学习笔记