UnityShader[4]几何着色器与可交互草地

Posted 仓鼠毛吉

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UnityShader[4]几何着色器与可交互草地相关的知识,希望对你有一定的参考价值。

GeometryShader执行顺序在顶点着色器之后,片元着色器以前。GeometryShader以一个/多个顶点组成的图元为输入,开发人员可以修改/添加顶点,修改为完全不同的网格,得到更多好看的效果。
缺点:并行困难,对移动端不友好,需要ShaderModel4.0以上
定义一个几何着色器,首先需要在声明模块添加几何着色器的声明;添加顶点着色器向几何着色器输出的结构体;修改ShaderModel版本为4.0以上

#pragma vertex vert
#pragma geometry geo
#pragma fragment frag

#include "UnityCG.cginc"
#pragma target 5.0

struct appdata

    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
;

struct v2g

    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
;

struct g2f

    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;
;

然后编写geometryShader主体:

[maxvertexcount(3)] // 最多调用3个顶点
// 输入:point / line / lineadj / triangle / triangleadj
// 输出:LineStream / PointStream / TriangleStream 
void geo(triangle v2g input[3], inout PointStream<g2f> outStream)

    g2f o;
    o.vertex = input[1].vertex;
    o.uv = input[1].uv;
    outStream.Append(o);

  • [maxvertexcount(value)] 代表告诉Shader该几何着色器最多单次调用多少个顶点
  • triangle v2g input[3] 参数代表以一个三角形图元为单位进行输入,包含3个顶点;
  • inout PointStream outStream 参数代表以一个点(PointStream)为单位进行输出;
  • outStream.Append(o) 代表将o点添加到outStream中;

可以看到几何着色器是以流为单位进行输入输出的,输入和输出关键字的区别会让流的解析发生改变,例如输出选择了PointStream,该类型的流会认为给定的数据中包含一个顶点,然后进行解析,将这个顶点输出;而选择了TriangStream则会认为给定的数据包含三个顶点,进行解析时会将三个顶点合成为一个三角形输出
上述代码:输入一个三角形,输出该三角形中的2号顶点(数组中顶点编号0,1,2代表三角形顶点编号1,2,3)。可以得到一个点阵Shader:

[maxvertexcount(3)]
void geo(triangle v2g input[3], inout LineStream<g2f> outStream)

    g2f o;
    for(int i=0; i<2; i++)
    
        o.vertex = input[i].vertex;
        o.uv = input[i].uv;
        outStream.Append(o);
    


上述代码:输入一个三角形,输出该三角形的0顶点、1顶点连接成的线段。(网格效果)

几何着色器还可以根据已有顶点生成新的顶点并构建图形,达到一些其他效果。此处尝试给每个三角形生成中心点:

[maxvertexcount(9)]
void geo(triangle v2g input[3], inout TriangleStream<g2f> outStream)

    g2f o;

    // 获取中心顶点
    float3 centerPos = (input[0].vertex + input[1].vertex + input[2].vertex) / 3;
    float2 centerUV = (input[0].uv + input[1].uv + input[2].uv) / 3;

    for(uint i=0; i<3; i++)
    
        o.vertex = UnityObjectToClipPos(input[i].vertex);
        o.uv = input[i].uv;
        outStream.Append(o);

        uint j = (i + 1) % 3;
        o.vertex = UnityObjectToClipPos(input[j].vertex);
        o.uv = input[j].uv;
        outStream.Append(o);

        o.vertex = UnityObjectToClipPos(float4(centerPos, 1.0));
        o.uv = centerUV;
        outStream.Append(o);
                    
        // 重置剥
        outStream.RestartStrip();
    

此时要适当提高控制的顶点数(9个,因为会输出3个三角面)。算法计算得出中心点在模型空间中的位置、uv等参数,将三个顶点(0号、1号、中心点)合成一个三角面添加进入outStream,直到将中心点分割得到的三个三角面均添加进入outStream,最后输出:

需要注意的是,如果先进行了顶点着色,即进入几何着色步骤时顶点已经转换到齐次裁剪空间下,会导致中心顶点计算错误,法线不匹配等问题。解决的方法就是将空间变换算法移动到中心顶点计算之后进行,如上述算法就是将UnityObjectToClipPos写到中心顶点计算完成之后(VertexShader只进行了数据转移操作)。
之后让中心点根据自身法线进行外扩,获取法线方向,然后对中心点坐标进行移动:

float3 edgeA = input[1].vertex - input[0].vertex;
float3 edgeB = input[2].vertex - input[0].vertex;
float3 normal = normalize(cross(edgeA, edgeB));
// 中心点向外挤出
centerPos += normal.xyz * _Length;

可以获得刺球效果:

生成大面积草地

几何着色器可用于生成网格,如果在其中增加一些随机数,就能让网格获得不同的旋转角度、弯曲程度、高度、摆动速度…足够生成一片随机的草地,而且因为没有使用预设的网格/实例,性能也变得可观。
参考:https://zhuanlan.zhihu.com/p/29632347
首先利用CPU生成大量随机位置,然后GPU在其上生成网格,渲染为草。
草的网格:

偶数顶点在左侧、奇数顶点在右侧,方便遍历和计算uv。
Shader需要根据给定顶点产生多个上述网格:

[maxvertexcount(30)]
void geo(point v2g input[1], inout TriangleStream<g2f> outStream)

    const uint vertexCount = 12; // 顶点数量
    g2f o[vertexCount];

    float currentVertexHeight = 0; // 当前顶点距离生成位置的高度
    float currentV = 0; // 当前顶点在uv中的v坐标
    float offsetV = 1.0 / (vertexCount / 2.0 - 1.0); // uv中v轴向相邻顶点之间的v值差距
    float4 root = input[0].vertex; // 定义草起始点

    for(uint i = 0; i < vertexCount; i++)
    
        o[i].vertex = float4(0.0, 0.0, 0.0, 1.0);
        o[i].normal = float3(0.0, 0.0, 1.0);
        o[i].uv = float2(0.0, 0.0);
        if(fmod(i, 2) == 0) // 处理偶数号顶点
        
            o[i].vertex = float4(root.x - _Width, root.y + currentVertexHeight, root.z, 1);
            o[i].uv = float2(0, currentV);
        
        else // 处理奇数号顶点
        
            o[i].vertex = float4(root.x + _Width, root.y + currentVertexHeight, root.z, 1);
            o[i].uv = float2(1, currentV);

            // 抬升uv和坐标
            currentV += offsetV;
            currentVertexHeight += currentV * _Height;
        

        o[i].vertex = UnityObjectToClipPos(o[i].vertex);
    

    for(uint j = 0; j < vertexCount-2; j++)
    
        outStream.Append(o[j]);
        outStream.Append(o[j+2]);
        outStream.Append(o[j+1]);
    

技术要点:

  • 以point为输入,即获取网格的所有顶点,在其上进行操作
  • 根据上个顶点的uv、位置,增量推导出下一个顶点的uv、位置

此时可以根据给定的网格,每个顶点生成一个草网格

然后需要对生成的草进行随机处理,如宽高、角度、摆动等

// 对草的宽高进行随机增减(基于平面坐标xz)
float random = sin(UNITY_HALF_PI * frac(root.x) + UNITY_HALF_PI * frac(root.z));
_Width += random/50.0;
_Height += random/5.0;

进行渲染:

SubShader

    Tags  "RenderType"="Transparent" "Queue"="AlphaTest" "IgnoreProjector" = "True"
    Pass
    
        Cull off
        Tags "LightMode"="ForwardBase"
        AlphaToMask On
        ...
        #include "UnityLightingCommon.cginc"
          
        fixed4 frag (g2f i) : SV_Target
    
        half lDirWS = _WorldSpaceLightPos0.xyz;
        half hDirWS = normalize(lDirWS + (_WorldSpaceCameraPos.xyz - i.posWS));

        fixed3 diffuse = saturate(dot(i.normal, lDirWS)) * _LightColor0;
        fixed3 specular = pow(saturate(dot(i.normal, hDirWS)), _PhongPow) * _LightColor0;

        fixed4 var_MainTex = tex2D(_MainTex, i.uv);

        fixed3 finalRGB = (diffuse + specular) * var_MainTex.rgb;
        fixed alpha = var_MainTex.a;

        return fixed4(finalRGB, alpha);
    


之后就需要生成草地网格,有两种生成方式:**按照给定高度图生成地形与草地、直接按照给定的模型生成覆于其上的草地。**此处使用第二种方式。
首先根据灰度图生成一个错落有致的网格:

并且根据给定贴图生成顶点色,rgb通道规定草地的颜色,a通道规定草的高度:


具体代码:

public Texture2D heightMap;
public Texture2D ColorMap;
public float terrainHeight;
public int terrainSize = 250;
public Material terrainMat;

private void GenerateTerrain()

    List<Vector3> verts = new List<Vector3>();
    List<int> tris = new List<int>();

    for(int i=0;i<terrainSize;i++)
    
        for(int j=0;j<terrainSize;j++)
        
            // 添加顶点
            verts.Add(new Vector3(i, heightMap.GetPixel(i, j).grayscale * terrainHeight, j));
            colors.Add(ColorMap.GetPixel(i,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);
        
    
  
    // 地形游戏对象
    GameObject plane = new GameObject("TerrainPlane");
    plane.AddComponent<MeshFilter>();
    MeshRenderer renderer = plane.AddComponent<MeshRenderer>(); // 渲染器
    renderer.material = terrainMat;

    // 地形网格
    Mesh terrainMesh = new Mesh();
    terrainMesh.vertices = verts.ToArray();
    terrainMesh.triangles = tris.ToArray();
    terrainMesh.RecalculateNormals();
    terrainMesh.colors = colors.ToArray();
    plane.GetComponent<MeshFilter>().mesh = terrainMesh;

    verts.Clear();

生成地形(Terrain)网格后(或从外部导入网格也可以,保证网格的顶点色符合需求即可),需要给Terrain网格添加脚本在其上生成草坪(Grass)网格,也就是在Terrain网格上生成随机顶点构成新的Grass网格,经由Grass网格上的几何着色器生成草地。
对于Terrain网格上的每个三角形,都可以生成0个或多个顶点,以便适配网格密度小/大的不同情况。设三角形的三个顶点为ABC,首先记录ABC三点的坐标,将B点和C点的坐标位置改为从A点出发到B/C的偏移量(这样方便获取AB、AC的方向以便生成随机点)

然后以A为起点,加上[0,1]随机数与AB、AC偏移量的乘积就能得到三角形附近的随机点(大致)。

具体代码:

public Material grassMat;
public Texture2D ColorMap;

private List<Vector3> verts = new List<Vector3>(); // 顶点集
private List<Color> colors = new List<Color>(); // 顶点颜色集
private List<Vector3> norm = new List<Vector3>(); // 顶点法线集
private Vector3[] vertices;
private Vector3[] normals;
private Color[] col;
private int[] tris;
private Random random;
void Start()

    random = new Random();
    Mesh mesh = GetComponent<MeshFilter>().sharedMesh;
    vertices = mesh.vertices;
    normals = mesh.normals;
    tris = mesh.triangles;
    col = mesh.colors;
    GenerateField(1); // 1为期望在生成草的草单元内生成1个草


private void GenerateField(int patchGrassCount)

    List<int> indices = new List<int>();
    for(int i=0; i<65000; i++)
    
        indices.Add(i);
    

    // 生成草单元
    // 此处因为网格比较密集,所以让每四个三角形生成一个草单元,也就是每隔12个顶点
    for(int x = 0; x < tris.Length; x+=12) 
    
        GenerateGrass(x, patchGrassCount);
    

    GameObject grass;
    MeshFilter filter;
    MeshRenderer renderer;
    Mesh mesh;

    // 顶点数过多时需要拆分部分顶点到新建的草网格
    while(verts.Count > 65000)
    
        mesh = new Mesh();
        mesh.vertices = verts.GetRange(0, 65000).ToArray(); // 设定顶点
        mesh.SetIndices(indices.GetRange(0, 65000).ToArray(), MeshTopology.Points, 0); // 设定点间关系
        mesh.colors = colors.GetRange(0, 65000).ToArray();
        mesh.normals = norm.GetRange(0, 65000).ToArray();
        // 生成网格
        grass = new GameObject("Grass");
        filter = grass.AddComponent<MeshFilter>();
        renderer = grass.AddComponent<MeshRenderer>();
        renderer.material = grassMat;
        filter.mesh = mesh;
        verts.RemoveRange(0, 65000);
    
    // 顶点数小于65000则正常生成
    mesh = new Mesh();
    mesh.vertices = verts.ToArray();
    mesh.SetIndices(indices.GetRange(0, verts.Count).ToArray(), MeshTopology.Points, 0);
    mesh.colors = colors.GetRange(0, verts.Count).ToArray();
    mesh.normals = norm.GetRange(0, verts.Count).ToArray();
    grass = new GameObject("Grass");
    filter = grass.AddComponent<MeshFilter>();
    renderer = grass.AddComponent<MeshRenderer>();
    renderer.sharedMaterial = grassMat;
    filter.mesh = mesh;

草单元生成函数:

// index 草单元起始点编号
// count 草单元内草数量
private void GenerateGrass(int index, int count)

    Vector3 pointA = vertices[tris[index]];
    Vector3 pointB = vertices[tris[index+1]] - pointA;
    Vector3 pointC = vertices[tris[index+2]] - pointA;

    Color colA =

: 渲染管线

【Unity shader 入门精要】内容精要--第二章: 渲染管线

what&why

什么是渲染管线? 为什么要学渲染管线,即了解渲染管线的意义是啥?
渲染管线(Render Pipeline)
渲染 + 管线
渲染:3D模型转化成2D图像的过程
管线:其实就是流水线,pipe英文有管道之意,管线就这么翻译了,其实就是流水线。
一个过程具备两个特点,就可以认为是流水线,1有多道工序(可并行)2.工序之间有前后顺序
那所以渲染管线就是 计算机把3D模型转化成2D图像的程序化过程
为什么要了解
(升职加薪,了解更多,增加生命厚度)
想要对一个过程进行控制与干预,就要了解这个过程,了解的越详细,可控的粒度越小。
控制手段:编写着色器
控制工具:着色器语言(shader)

正文(精要)

阶段一: 应用阶段

参与者:
CPU,GPU
做的事情

  • 准备场景数据(把数据加载到内存以及显存)
  • 剔除(culling):删除不可见的物体
  • 设置渲染状态(参数):包括但不限于配置 材质、纹理、使用的着色器,就像配置部署流水线的工人一样。
  • CPU向GPU发起Draw Call指令,并告诉GPU这个指令对应的渲染图元列表

输入:人的设置
输出:一堆渲染图元(rendering primitives),可渲染的最小几何单位,包括点、线、三角面

阶段二: 几何阶段

参与者:
GPU
做的事情

  • 坐标系的转化,把顶点的坐标变换到屏幕坐标

输入:一堆渲染图元
输出:一堆屏幕坐标系下的图元顶点坐标,包括这些顶点对应的深度值、着色、法向、视角方向等相关信息

子阶段

顶点着色器(必要,可编程)

Vertex Shader
每个顶点都会被输入到顶点着色器中 执行一遍顶点着色器的逻辑,具体逻辑是可编程的。
做的事情

  • 光栅化:把图元栅格化像素,即计算每个图元覆盖了哪些像素
  • 计算像素的颜色

曲面细分着色器(可选,可编程)

几何着色器(可选,可编程)

执行 逐图元的着色操作

裁剪(必要,可配置,不可编程)

图元和摄像机视野的关系:

  • 完全在视野内
  • 部分在视野内(裁剪 clipping):与视野边界求交点
  • 完全在视野外(剔除)

几何阶段到次此为止,会得到归一化的设备坐标(Normalized Device Coordinates, NDC)

屏幕映射(必要,GPU固定实现,不可配置,不可编程)

坐标会进一步转化,坐标从NDC转化到屏幕坐标系下(Screen Coordinates)

阶段三: 光栅化阶段

参与者:
GPU
做的事情

  • 坐标系的转化,把顶点的坐标变换到屏幕坐标

输入:一堆屏幕坐标系下的图元顶点坐标,还有一些额外信息
输出:图像(主色后的像素阵列)

子阶段

三角形设置(必要,可配置,不可编程)

得到三角网格三边的表达方式

三角形遍历(必要,可配置,不可编程)

三角网格的栅格化,检查每个像素是否被一个三角网格覆盖,如果是,就会生成一个片元(fragment),该过程也被称为扫描变换,会生成一个片元序列
片元:显示在屏幕上的叫像素,而最终颜色还没有计算出来之前,叫片元,一个片元是一堆状态的集合,这些状态都是计算一个特定像素最终颜色所需要的各种信息,比如屏幕坐标、深度信息、纹理坐标等等。

片元着色器(可选,可编程)

Fragment Shader, 根据一个片元的特定信息计算最终颜色,计算方式可编程。

逐片元操作(必要,可配置,不可编程)

所有片元的合并,包括计算可见性以及一个像素的最终颜色(一个像素可能由多个片元共同决定,比如涉及到半透明物体的时候)
计算可见性的操作比如:深度测试、模板测试
颜色混合:多片元颜色合并的方式,会得到一个屏幕坐标,也就是一个像素的最终颜色。

以上是关于UnityShader[4]几何着色器与可交互草地的主要内容,如果未能解决你的问题,请参考以下文章

shader几何运算效果

: 渲染管线

UnityShader RenderType

学unityshader之前学unity吗

UnityShader入门精要-3.5 UnityShader的形式

python 迭代器与可迭代对象