Shader学习笔记 03 - 草
草对象
- 线性排列
(图片取自gpu gem1)
顶点最少、需要对应纹理、法线等贴图。效果不好。固定视角;远处;或者加上广告牌(billboard)效果(eg:游戏漫漫长夜,如果不绕着草转不一定能发现)可采用。
- 交叉多边形
顶点少、需要对应纹理、法线等贴图。
- 自定义模型
顶点多,更灵活,效果更好。
- C#程序或几何着色器(geometry shader)生成
参考博客,就是程序生成顶点,然后连接面片生成草模型,几何着色器需要dx10,目前手机应该不支持。
批量草 //todo
生成
- GPU instance
- mesh 合并,一般需要程序式生成草体,也就是需要保存草的分布信息,或者使用随机生成。
排列
https://caseymuratori.com/blog_0011
优化 //TODO
chunk分块;lod;
动画
- 草的摆动一般只需要根据风向移动顶点xz值,但移动幅度过大便会拉长模型,这时候便需要计算出y的偏移,从而保持草的长度相对不变。
- 需要使用草的世界坐标影响偏移值,如果没有,批量草的摆动便会一样。
底部刚性
- 使用uv的y值或者顶点的y值
- 比较复杂的是使用顶点颜色
xz偏移
- 使用三角函数
// 1. 直接使用正弦函数
float speed = _Time.x * _Speed;
float x = sin(wpos.x + speed);// x偏移
float z = sin(wpos.z + speed);// z偏移
// 2. 来自 https://blogs.unity3d.com/2018/08/07/shader-graph-updates-and-sample-project/
float sine = sin(_Time.y*_WindSpeed)*0.1;
sine = lerp(0.1, sine, min(sine,1));
// 使正弦波有一些随机的小波动
float wind = Remap(_WindStrength*_SinTime,float2(-1,1),float2(-0.1,0.1)) + _WindStrength*sine;
- 使用噪声
float noise = fbmNoise(worldPos.xz+_Time.y*_WindSpeed);
float wind = (noise*2.0-1.0)*_WindStrength;
- unity内置
文件TerrainEngine.cginc函数TerrainWaveGrass。核心原理也是使用正弦函数。
y偏移
模型坐标的中心一般不在底部,草根的位置可以估算,也可以在外部通过bounds.size获取实际尺寸得到准确的草根。
- 通过xz偏移量估算
示例vert:
float _Rigidness; float _WindSpeed; float _WindStrength; float _YOffsetRate; void vert(inout appdata_full v) { float4 worldPos = mul(unity_ObjectToWorld, v.vertex); float2 uv = v.texcoord; float offset = pow(uv.y,5);
float2 dir = normalize(_WindDirection.xz); float noise = fbmNoise(worldPos.xz/_Rigidness+_Time.y*dir*_WindSpeed); float wind = (noise*2.0-1.0)*_WindStrength; worldPos.xz += wind*dir; float gravityForce = abs (wind*_YOffsetRate); worldPos.y -= gravityForce * offset; float4 swayPos = mul(unity_WorldToObject, worldPos); v.vertex.xyz = lerp(v.vertex.xyz, swayPos.xyz, offset);
}
- 通过勾股定理
示例vert:
float _Rigidness; float _WindSpeed; float _WindStrength; float4 _RootPos; void vert(inout appdata_full v) { UNITY_INITIALIZE_OUTPUT(Input,o); float4 worldPos = mul(unity_ObjectToWorld, v.vertex); float2 uv = v.texcoord; float offset = pow(uv,5);
float2 dir = normalize(_WindDirection.xz); float noise = fbmNoise(worldPos.xz/_Rigidness+_Time.y*dir*_WindSpeed); float wind = (noise*2.0-1.0)*_WindStrength; worldPos.xz += wind*dir; float4 vertexAdj = v.vertex - _RootPos; float c = length(vertexAdj.xyz); float a = length(wind); worldPos.y -= (c - sqrt(c*c - a*a)); float4 swayPos = mul(unity_WorldToObject, worldPos); v.vertex.xyz = lerp(v.vertex.xyz, swayPos.xyz, offset);
}
- 通过三角函数
示例vert:
float _Rigidness;
float _WindSpeed;
float4 _RootPos;
float _MaxAngle;
float Remap(float x, float2 inMinMax, float2 outMinMax)
{
return (x - inMinMax.x)* (outMinMax.y - outMinMax.x)/(inMinMax.y - inMinMax.x) + outMinMax.x;
}
float3 AngleRot(float4 vet, float angle, float2 dir)
{
float s,c;
sincos(angle,s,c);
float3 rot = 0;
float4 vertexAdj = vet - float4(0,-1,0,0);
float vlen = length(vertexAdj.xyz);
float xzL = vlen*s;
rot.xz = dir*xzL;
float yl = vlen - vlen*c;
rot.y = -yl;
return rot;
}
void vert(inout appdata_full v)
{
UNITY_INITIALIZE_OUTPUT(Input,o);
float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
float2 uv = v.texcoord;
float offset = pow(uv,5);
float2 dir = normalize(_WindDirection.xz);
float noise = fbmNoise(worldPos.xz/_Rigidness+_Time.y*dir*_WindSpeed);
float maxAngle = _MaxAngle*UNITY_PI/180;
float angle = Remap(noise, float2(0,1),float2(-maxAngle,maxAngle));
worldPos.xyz += AngleRot(v.vertex, angle, dir);
float4 swayPos = mul(unity_WorldToObject, worldPos);
v.vertex.xyz = lerp(v.vertex.xyz, swayPos.xyz, offset);
}
- 通过旋转矩阵
示例vert:
float _Rigidness; float _WindSpeed; float _WindStrength; float4 _RootPos; float _MaxAngle;
// Construct a rotation matrix that rotates around the provided axis, sourced from:
// https://gist.github.com/keijiro/ee439d5e7388f3aafc5296005c8c3f33
float3x3 AngleAxis3x3(float angle, float3 axis)
{
float c, s;
sincos(angle, s, c);float t = 1 - c; float x = axis.x; float y = axis.y; float z = axis.z; return float3x3( t * x * x + c, t * x * y - s * z, t * x * z + s * y, t * x * y + s * z, t * y * y + c, t * y * z - s * x, t * x * z - s * y, t * y * z + s * x, t * z * z + c );
}
void vert(inout appdata_full v)
{
UNITY_INITIALIZE_OUTPUT(Input,o);
float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
float2 uv = v.texcoord;
float offset = pow(uv,5);float2 dir = normalize(_WindDirection.xz); float noise = fbmNoise(worldPos.xz/_Rigidness+_Time.y*dir*_WindSpeed); float maxAngle = _MaxAngle*UNITY_PI/180; float angle = Remap(noise, float2(0,1),float2(-maxAngle,maxAngle)); float3x3 bendRotationMatrix = AngleAxis3x3(angle, normalize(float3(dir.y,0,dir.x))); float3 vertexAdj = mul(bendRotationMatrix,v.vertex.xyz - _RootPos.xyz); vertexAdj += _RootPos.xyz; v.vertex.xyz = lerp(v.vertex.xyz, vertexAdj, offset);
}
交互
障碍物推动草向物体外偏移
// c#脚本,将障碍物坐标,总障碍物数量赋值给shader变量
{
public Transform[] obstacles;
private Vector4[] obstaclePositions = new Vector4[10];
Update {
for (int n = 0; n < obstacles.Length; n++)
{
obstaclePositions[n] = obstacles[n].position;
}
Shader.SetGlobalFloat("_ObstacleLength", obstacles.Length);
Shader.SetGlobalVectorArray("_ObstaclePositions", obstaclePositions);
}
}
// shader
uniform float3 _ObstaclePositions[100];
uniform float _ObstacleLength;
vert {
float2 xzShift = 0;
for (int i = 0; i < _ObstacleLength; i++){
float3 dis = distance(_ObstaclePositions[i], worldPos.xyz); // 顶点于障碍物坐标中心距离
float3 radius = 1 - saturate(dis /_Radius); // 半径内越接近障碍物中心值越大,中心为1,半径外为0
float3 sphereDisp = worldPos - _ObstaclePositions[i]; // 障碍物中心指向顶点的向量
sphereDisp *= radius; // 偏移值衰减
xzShift += sphereDisp.xz; // 偏移值累加
}
// 方法1、2、4,xzShift累加
// 方法3
float len = length(xzShift);
if(len != 0)
{
float obsMaxAngle = _MaxAngle*UNITY_PI/180;
float obsAngle = Remap(len, float2(0,_MaxGrassLength),float2(0,obsMaxAngle));
worldPos.xyz += AngleRot(v.vertex, obsAngle, normalize(xzShift));
}
}
碰撞摇摆
碰撞草体触发草的摇晃,触发器+动画(或者在c#脚本控制顶点偏移)就是比较好的解决方案。
使用shader比较麻烦,难点在于确认触发点所在草体的所有顶点,并且需要在脚本里根据时间不断修正偏移。github.com/wachel/UnityInteractiveGrass,这个是找到以shader实现的交互草,里面保存了草体的mesh,通过mesh获取整个草体的顶点并占用了Tangant属性来与shader传递摇摆的信息。感觉使用shader来实现碰撞摇摆不是很好的方案。