Shader学习笔记 03 - 草

草对象

  1. 线性排列

image(图片取自gpu gem1)

顶点最少、需要对应纹理、法线等贴图。效果不好。固定视角;远处;或者加上广告牌(billboard)效果(eg:游戏漫漫长夜,如果不绕着草转不一定能发现)可采用。

  1. 交叉多边形

image
image

顶点少、需要对应纹理、法线等贴图。

  1. 自定义模型

image

顶点多,更灵活,效果更好。

  1. C#程序或几何着色器(geometry shader)生成

参考博客,就是程序生成顶点,然后连接面片生成草模型,几何着色器需要dx10,目前手机应该不支持。

批量草 //todo

生成
  1. GPU instance
  2. mesh 合并,一般需要程序式生成草体,也就是需要保存草的分布信息,或者使用随机生成。
排列

https://caseymuratori.com/blog_0011

优化 //TODO

chunk分块;lod;

动画

  • 草的摆动一般只需要根据风向移动顶点xz值,但移动幅度过大便会拉长模型,这时候便需要计算出y的偏移,从而保持草的长度相对不变。
  • 需要使用草的世界坐标影响偏移值,如果没有,批量草的摆动便会一样。
底部刚性
  1. 使用uv的y值或者顶点的y值
  2. 比较复杂的是使用顶点颜色
xz偏移
  1. 使用三角函数
// 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;
  1. 使用噪声
float noise = fbmNoise(worldPos.xz+_Time.y*_WindSpeed);
float wind = (noise*2.0-1.0)*_WindStrength;
  1. unity内置

文件TerrainEngine.cginc函数TerrainWaveGrass。核心原理也是使用正弦函数。

y偏移

模型坐标的中心一般不在底部,草根的位置可以估算,也可以在外部通过bounds.size获取实际尺寸得到准确的草根。

  1. 通过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);

}

  1. 通过勾股定理
示例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);

}

  1. 通过三角函数
示例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);

}

  1. 通过旋转矩阵
示例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);

}

交互

障碍物推动草向物体外偏移

https://www.patreon.com/posts/19844414

// 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来实现碰撞摇摆不是很好的方案。

posted @ 2020-08-11 11:10  Lain_vv  阅读(477)  评论(0编辑  收藏  举报