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]几何着色器与可交互草地的主要内容,如果未能解决你的问题,请参考以下文章