1.Using Blender to create a single grass clump
首先blender与unity的坐标轴不同,z轴向上,不是y轴
通过小键盘的数字键可以快速切换视图,选中物体以后按下小键盘的点可以将物体聚焦于屏幕中心
首先我们创建一个平面,宽度为0.2m,然后切换到正交前视图,复制两个平面。shift+D可以复制面
接着将上下两个面旋转45°至中间面的中心。先按下R然后按下Y可以绕y轴旋转,然后按G键可以移动面
然后切换到正交顶视图(数字键7)
将两个复制的面分别向左向右旋转10.5°左右
最后加上材质和贴图以后的效果就是下面这样
然后就可以保存退出Blender了,后面我们在unity中批量产出这个grass
2. Using instancing to cover a surface with grass
首先就是定义草的类,包含了草的位置,摆动角度
struct GrassClump
{
public Vector3 position;
public float lean;
public float noise;
public GrassClump( Vector3 pos)
{
position.x = pos.x;
position.y = pos.y;
position.z = pos.z;
lean = 0;
noise = Random.Range(0.5f, 1);
if (Random.value < 0.5f) noise = -noise;
}
}
接着还有全局的草的密度,大小,最大摆动角度
[Range(0,1)]
public float density = 0.8f;
[Range(0.1f,3)]
public float scale = 0.2f;
[Range(10, 45)]
public float maxLean = 25;
然后就是初始化草丛的位置信息,生成一个 ComputeBuffer
来存储这些位置数据,并通过计算着色器来模拟草丛的摆动效果。
获取附加的 MeshFilter
组件中的网格边界,bounds.extents
返回边界框的一半大小(每个轴的范围的一半)
MeshFilter mf = GetComponent<MeshFilter>();
Bounds bounds = mf.sharedMesh.bounds;
Vector3 clumps = bounds.extents;
使用对象的缩放值(transform.localScale
)和一个密度因子来调整草丛的分布范围,主要是 x 和 z 轴 。并且计算草丛的总数量
Vector3 vec = transform.localScale / 0.1f * density;
clumps.x *= vec.x;
clumps.z *= vec.z;
int total = (int)clumps.x * (int)clumps.z;
获取计算着色器中的内核 LeanGrass
,并计算每个线程组的大小。groupSize
是用于处理草丛的线程组数,而 count
则是实际生成的草丛总数
kernelLeanGrass = shader.FindKernel("LeanGrass");
shader.GetKernelThreadGroupSizes(kernelLeanGrass, out threadGroupSize, out _, out _);
groupSize = Mathf.CeilToInt((float)total / (float)threadGroupSize);
int count = groupSize * (int)threadGroupSize;
随机生成 count
个草丛的 pos
位置。这个位置基于网格的边界和中心生成,使用 TransformPoint
将局部坐标转换为全局坐标 (世界坐标)
clumpsArray = new GrassClump[count];
for (int i = 0; i < count; i++)
{
Vector3 pos = new Vector3(Random.value * bounds.extents.x * 2 - bounds.extents.x + bounds.center.x,
0,
Random.value * bounds.extents.z * 2 - bounds.extents.z + bounds.center.z);
pos = transform.TransformPoint(pos);
clumpsArray[i] = new GrassClump(pos);
}
创建了一个 ComputeBuffer
来存储所有的草丛位置信息,并将 clumpsArray
赋值到缓冲区中。
clumpsBuffer = new ComputeBuffer(count, SIZE_GRASS_CLUMP);
clumpsBuffer.SetData(clumpsArray);
将缓冲区 clumpsBuffer
绑定到计算着色器的 clumpsBuffer
参数,并将草丛最大倾斜角度 maxLean
传递给着色器。
shader.SetBuffer(kernelLeanGrass, "clumpsBuffer", clumpsBuffer);
shader.SetFloat("maxLean", maxLean * Mathf.PI / 180);
timeID = Shader.PropertyToID("time");
通过 argsArray
设置绘制调用的参数(索引数量和实例数量),并使用 ComputeBuffer
类型为 IndirectArguments
创建一个缓冲区,用于 DrawMeshInstancedIndirect
函数的调用
-
argsArray[0] = mesh.GetIndexCount(0);
- 这行代码获取的是
mesh
的索引数量,也就是用来渲染的几何体有多少个顶点索引。每个网格都有其顶点、法线、UV 等信息,而索引决定了如何连接这些顶点来形成三角形。 - 在
IndirectArguments
绘制时,第一个参数就是表示绘制网格时使用的顶点索引数量。
- 这行代码获取的是
-
argsArray[1] = (uint)count;
count
是实例化对象的数量。通过Graphics.DrawMeshInstancedIndirect
方法可以在一次绘制调用中实例化多个对象。- 这里第二个参数表示要绘制的实例化网格的数量
argsArray[0] = mesh.GetIndexCount(0);
argsArray[1] = (uint)count;
argsBuffer = new ComputeBuffer(1, 5 * sizeof(uint), ComputeBufferType.IndirectArguments);
argsBuffer.SetData(argsArray);
然后看一下我们的计算着色器
很简短,就是设置了个倾斜角度,方便后续在表面shader中进行旋转
[numthreads(THREADGROUPSIZE,1,1)]
void LeanGrass (uint3 id : SV_DispatchThreadID)
{
GrassClump clump = clumpsBuffer[id.x];
clump.lean = sin(time + clump.noise) * maxLean * clump.noise;
clumpsBuffer[id.x] = clump;
}
接着继续编写表面着色器
首先是设置每个草丛的位置以及旋转平移矩阵
void setup()
{
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
GrassClump clump = clumpsBuffer[unity_InstanceID];
_Position = clump.position;
_Matrix = create_matrix(clump.position, clump.lean);
#endif
}
然后是创建矩阵函数,这是一个绕z轴旋转的矩阵
float4x4 create_matrix(float3 pos, float theta){
float c = cos(theta);
float s = sin(theta);
return float4x4(
c,-s, 0, pos.x,
s, c, 0, pos.y,
0, 0, 1, pos.z,
0, 0, 0, 1
);
}
最后就是顶点函数的设置
首先乘上缩放系数,然后计算经过旋转和平移的顶点位置,接着计算只经过平移的位置,最后根据uv的y值来插值坐标,也就是高度越高,弯曲幅度越大
void vert(inout appdata_full v, out Input data)
{
UNITY_INITIALIZE_OUTPUT(Input, data);
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
v.vertex.xyz *= _Scale;
float4 rotatedVertex = mul(_Matrix, v.vertex);
v.vertex.xyz += _Position;
v.vertex = lerp(v.vertex, rotatedVertex, v.texcoord.y);
#endif
}
最终效果:
3. Bending blades of grass
这次我们要自己通关代码创建一个草的mesh,并且设置顶点位置,法线,uv,索引。
下面几个图分别是这几个属性的图解。
这里面设置顶点索引要按照逆时针的顺序是因为unity会按照逆时针顺序的三角形认定为正面,防止它被背面剔除
代码:
Mesh Blade
{
get
{
Mesh mesh;
if (blade != null)
{
mesh = blade;
}
else
{
mesh = new Mesh();
float height = 0.2f;
float rowHeight = height / 4;
float halfWidth = height / 10;
//1. Use the above variables to define the vertices array
Vector3[] vertices =
{
new Vector3(-halfWidth, 0, 0),
new Vector3( halfWidth, 0, 0),
new Vector3(-halfWidth, rowHeight, 0),
new Vector3( halfWidth, rowHeight, 0),
new Vector3(-halfWidth*0.9f, rowHeight*2, 0),
new Vector3( halfWidth*0.9f, rowHeight*2, 0),
new Vector3(-halfWidth*0.8f, rowHeight*3, 0),
new Vector3( halfWidth*0.8f, rowHeight*3, 0),
new Vector3( 0, rowHeight*4, 0)
};
//2. Define the normals array, hint: each vertex uses the same normal
Vector3 normal = new Vector3(0, 0, -1);
Vector3[] normals =
{
normal,
normal,
normal,
normal,
normal,
normal,
normal,
normal,
normal
};
//3. Define the uvs array
Vector2[] uvs =
{
new Vector2(0,0),
new Vector2(1,0),
new Vector2(0,0.25f),
new Vector2(1,0.25f),
new Vector2(0,0.5f),
new Vector2(1,0.5f),
new Vector2(0,0.75f),
new Vector2(1,0.75f),
new Vector2(0.5f,1)
};
//4. Define the indices array
int[] indices =
{
0,1,2,1,3,2,//row 1
2,3,4,3,5,4,//row 2
4,5,6,5,7,6,//row 3
6,7,8//row 4
};
//5. Assign the mesh properties using the arrays
// for indices use
// mesh.SetIndices( indices, MeshTopology.Triangles, 0 );
mesh.vertices = vertices;
mesh.normals = normals;
mesh.uv = uvs;
mesh.SetIndices(indices, MeshTopology.Triangles, 0);
}
return mesh;
}
}
接下来加入风向的影响
风向相关的属性主要是风速,风向以及风的强度。
float theta = windDirection * Mathf.PI / 180;
Vector4 wind = new Vector4(Mathf.Cos(theta), Mathf.Sin(theta), windSpeed, windScale);
shader.SetVector("wind", wind);
之后在每一帧调用 Graphics.DrawMeshInstancedIndirect(mesh, 0, material, bounds, argsBuffer);来进行大规模绘制。
之后是计算着色器。
主要是计算bend属性,noise的取值范围是-1到1,这样草既可以向左也可也向右弯曲。
[numthreads(THREADGROUPSIZE,1,1)]
void BendGrass (uint3 id : SV_DispatchThreadID)
{
GrassBlade blade = bladesBuffer[id.x];
float2 offset = (blade.position.xz + wind.xy * time * wind.z) * wind.w;
float noise = perlin(offset.x, offset.y) * 2 - 1;
blade.bend = noise * maxBend * blade.noise;
bladesBuffer[id.x] = blade;
}
至于表面着色器则是和之前的差不多。
最终效果:
完整代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GrassBlades : MonoBehaviour
{
struct GrassBlade
{
public Vector3 position;
public float bend;
public float noise;
public float fade;
public GrassBlade( Vector3 pos)
{
position.x = pos.x;
position.y = pos.y;
position.z = pos.z;
bend = 0;
noise = Random.Range(0.5f, 1) * 2 - 1;
fade = Random.Range(0.5f, 1);
}
}
int SIZE_GRASS_BLADE = 6 * sizeof(float);
public Material material;
public ComputeShader shader;
public Material visualizeNoise;
public bool viewNoise = false;
[Range(0,1)]
public float density;
[Range(0.1f,3)]
public float scale;
[Range(10, 45)]
public float maxBend;
[Range(0, 2)]
public float windSpeed;
[Range(0, 360)]
public float windDirection;
[Range(10, 1000)]
public float windScale;
ComputeBuffer bladesBuffer;
ComputeBuffer argsBuffer;
GrassBlade[] bladesArray;
uint[] argsArray = new uint[] { 0, 0, 0, 0, 0 };
Bounds bounds;
int timeID;
int groupSize;
int kernelBendGrass;
Mesh blade;
Material groundMaterial;
Mesh Blade
{
get
{
Mesh mesh;
if (blade != null)
{
mesh = blade;
}
else
{
mesh = new Mesh();
float height = 0.2f;
float rowHeight = height / 4;
float halfWidth = height / 10;
//1. Use the above variables to define the vertices array
Vector3[] vertices =
{
new Vector3(-halfWidth, 0, 0),
new Vector3( halfWidth, 0, 0),
new Vector3(-halfWidth, rowHeight, 0),
new Vector3( halfWidth, rowHeight, 0),
new Vector3(-halfWidth*0.9f, rowHeight*2, 0),
new Vector3( halfWidth*0.9f, rowHeight*2, 0),
new Vector3(-halfWidth*0.8f, rowHeight*3, 0),
new Vector3( halfWidth*0.8f, rowHeight*3, 0),
new Vector3( 0, rowHeight*4, 0)
};
//2. Define the normals array, hint: each vertex uses the same normal
Vector3 normal = new Vector3(0, 0, -1);
Vector3[] normals =
{
normal,
normal,
normal,
normal,
normal,
normal,
normal,
normal,
normal
};
//3. Define the uvs array
Vector2[] uvs =
{
new Vector2(0,0),
new Vector2(1,0),
new Vector2(0,0.25f),
new Vector2(1,0.25f),
new Vector2(0,0.5f),
new Vector2(1,0.5f),
new Vector2(0,0.75f),
new Vector2(1,0.75f),
new Vector2(0.5f,1)
};
//4. Define the indices array
int[] indices =
{
0,1,2,1,3,2,//row 1
2,3,4,3,5,4,//row 2
4,5,6,5,7,6,//row 3
6,7,8//row 4
};
//5. Assign the mesh properties using the arrays
// for indices use
// mesh.SetIndices( indices, MeshTopology.Triangles, 0 );
mesh.vertices = vertices;
mesh.normals = normals;
mesh.uv = uvs;
mesh.SetIndices(indices, MeshTopology.Triangles, 0);
}
return mesh;
}
}
// Start is called before the first frame update
void Start()
{
bounds = new Bounds(Vector3.zero, new Vector3(30, 30, 30));
blade = Blade;
MeshRenderer renderer = GetComponent<MeshRenderer>();
groundMaterial = renderer.material;
InitShader();
}
private void OnValidate()
{
if (groundMaterial != null)
{
MeshRenderer renderer = GetComponent<MeshRenderer>();
renderer.material = (viewNoise) ? visualizeNoise : groundMaterial;
//TO DO: set wind using wind direction, speed and noise scale
float theta = windDirection * Mathf.PI / 180;
Vector4 wind = new Vector4(Mathf.Cos(theta), Mathf.Sin(theta), windSpeed, windScale);
shader.SetVector("wind", wind);
visualizeNoise.SetVector("wind", wind);
}
}
void InitShader()
{
MeshFilter mf = GetComponent<MeshFilter>();
Bounds bounds = mf.sharedMesh.bounds;
Vector3 blades = bounds.extents;
Vector3 vec = transform.localScale / 0.1f * density;
blades.x *= vec.x;
blades.z *= vec.z;
int total = (int)blades.x * (int)blades.z * 20;
kernelBendGrass = shader.FindKernel("BendGrass");
uint threadGroupSize;
shader.GetKernelThreadGroupSizes(kernelBendGrass, out threadGroupSize, out _, out _);
groupSize = Mathf.CeilToInt((float)total / (float)threadGroupSize);
int count = groupSize * (int)threadGroupSize;
bladesArray = new GrassBlade[count];
for(int i=0; i<count; i++)
{
Vector3 pos = new Vector3( Random.value * bounds.extents.x * 2 - bounds.extents.x + bounds.center.x,
0,
Random.value * bounds.extents.z * 2 - bounds.extents.z + bounds.center.z);
pos = transform.TransformPoint(pos);
bladesArray[i] = new GrassBlade(pos);
}
bladesBuffer = new ComputeBuffer(count, SIZE_GRASS_BLADE);
bladesBuffer.SetData(bladesArray);
shader.SetBuffer(kernelBendGrass, "bladesBuffer", bladesBuffer);
shader.SetFloat("maxBend", maxBend * Mathf.PI / 180);
//TO DO: set wind using wind direction, speed and noise scale
float theta = windDirection * Mathf.PI / 180;
Vector4 wind = new Vector4(Mathf.Cos(theta), Mathf.Sin(theta), windSpeed, windScale);
shader.SetVector("wind", wind);
timeID = Shader.PropertyToID("time");
argsArray[0] = blade.GetIndexCount(0);
argsArray[1] = (uint)count;
argsBuffer = new ComputeBuffer(1, 5 * sizeof(uint), ComputeBufferType.IndirectArguments);
argsBuffer.SetData(argsArray);
material.SetBuffer("bladesBuffer", bladesBuffer);
material.SetFloat("_Scale", scale);
}
// Update is called once per frame
void Update()
{
shader.SetFloat(timeID, Time.time);
shader.Dispatch(kernelBendGrass, groupSize, 1, 1);
if (!viewNoise)
{
Graphics.DrawMeshInstancedIndirect(blade, 0, material, bounds, argsBuffer);
}
}
private void OnDestroy()
{
bladesBuffer.Release();
argsBuffer.Release();
}
}
// Each #kernel tells which function to compile; you can have many kernels
#define THREADGROUPSIZE 128
#pragma kernel BendGrass
#include "noiseSimplex.cginc"
struct GrassBlade
{
float3 position;
float bend;
float noise;
float fade;
};
RWStructuredBuffer<GrassBlade> bladesBuffer;
float time;
float maxBend;
float4 wind;
[numthreads(THREADGROUPSIZE,1,1)]
void BendGrass (uint3 id : SV_DispatchThreadID)
{
GrassBlade blade = bladesBuffer[id.x];
float2 offset = (blade.position.xz + wind.xy * time * wind.z) * wind.w;
float noise = perlin(offset.x, offset.y) * 2 - 1;
blade.bend = noise * maxBend * blade.noise;
bladesBuffer[id.x] = blade;
}
Shader "Custom/GrassBlades"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader
{
Tags{ "RenderType"="Opaque" }
LOD 200
Cull Off
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard vertex:vert addshadow fullforwardshadows
#pragma instancing_options procedural:setup
sampler2D _MainTex;
struct Input
{
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
float _Scale;
float _Fade;
float4x4 _Matrix;
float3 _Position;
float4x4 create_matrix(float3 pos, float theta){
float c = cos(theta);
float s = sin(theta);
return float4x4(
c,-s, 0, pos.x,
s, c, 0, pos.y,
0, 0, 1, pos.z,
0, 0, 0, 1
);
}
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
struct GrassBlade
{
float3 position;
float lean;
float noise;
float fade;
};
StructuredBuffer<GrassBlade> bladesBuffer;
#endif
void vert(inout appdata_full v, out Input data)
{
UNITY_INITIALIZE_OUTPUT(Input, data);
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
v.vertex.xyz *= _Scale;
float4 rotatedVertex = mul(_Matrix, v.vertex);
v.vertex.xyz += _Position;
v.vertex = lerp(v.vertex, rotatedVertex, v.texcoord.y);
#endif
}
void setup()
{
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
GrassBlade blade = bladesBuffer[unity_InstanceID];
_Matrix = create_matrix(blade.position, blade.lean);
_Position = blade.position;
_Fade = blade.fade;
#endif
}
void surf (Input IN, inout SurfaceOutputStandard o)
{
// Albedo comes from a texture tinted by color
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color * _Fade;
o.Albedo = c.rgb;
// Metallic and smoothness come from slider variables
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
}
ENDCG
}
FallBack "Diffuse"
}
4. Grass on an uneven surface
之前都是在平面上生成草地,这次我们在不平坦的土地上面生成草地。
主要修改了随机生成草丛位置的函数。
初始化一个长度为 count
的 GrassClump
数组,用于存储草丛的位置数据
clumpsArray = new GrassClump[count];
给当前游戏对象添加 MeshCollider
,方便后续使用 Physics.Raycast
做物理碰撞检测,以确定草丛的具体位置
gameObject.AddComponent<MeshCollider>();
v
是用于存储坐标的临时Vector3
变量。bounds.center.y + bounds.extents.y
表示边界框的顶点高度,并将该高度变换到世界空间transform.TransformPoint(v)
中,得到射线起点的y
值(castY
)。minY
表示边界框的最低点,经过同样的变换后获得其在世界坐标系中的y
值。range
是castY
和minY
之间的高度差,用于之后的高度比例计算。- 最后,
castY += 10
将射线的起始位置在边界框上方偏移 10 个单位,确保射线从足够高的地方开始射向地面。
RaycastHit hit;
Vector3 v = new Vector3();
v.y = (bounds.center.y + bounds.extents.y);
v = transform.TransformPoint(v);
float castY = v.y;
v.Set(0, 0, 0);
v.y = (bounds.center.y - bounds.extents.y);
v = transform.TransformPoint(v);
float minY = v.y;
float range = castY - minY;
castY += 10;
开始一个 while
循环,直到生成的草丛数达到 count
或循环次数达到 count * 10
。这个限制防止循环无休止进行(在某些情况下可能无法找到适合的位置)
- 生成随机的
x
和z
坐标,使得草丛分布在bounds
的范围内。Random.value * bounds.extents.x * 2 - bounds.extents.x
随机生成一个在bounds
范围内的x
坐标。bounds.center.x
和bounds.center.z
用于确保随机位置是围绕bounds
中心生成的。
- 使用
transform.TransformPoint(pos)
将位置变换到世界坐标系中。 - 将生成的位置
y
赋值为castY
,即射线从该高度开始向下检测
int loopCount = 0;
int index = 0;
while (index < count && loopCount < (count * 10))
{
loopCount++;
Vector3 pos = new Vector3(
(Random.value * bounds.extents.x * 2 - bounds.extents.x) + bounds.center.x,
0,
(Random.value * bounds.extents.z * 2 - bounds.extents.z) + bounds.center.z);
pos = transform.TransformPoint(pos);
pos.y = castY;
}
- 发射一条从
pos
向下(Vector3.down
)的射线,检测地形表面。如果射线击中了地形,hit
将包含碰撞信息。 - 将
pos.y
设为射线击中地面的y
值,即草丛的实际高度 - 计算
deltaHeight
,这是草丛的高度比例,heightAffect
是影响草丛生成的高度因子。 - 随机数与
deltaHeight
相比较,用于控制草丛的分布概率,位置越高(deltaHeight
越大),草丛生成的概率可能越低,反之越高。 - 如果条件符合,将生成的
pos
位置赋给一个新的GrassClump
对象,并将其存入clumpsArray
。
while (index < count && loopCount < (count * 10))
{
if (Physics.Raycast(pos, Vector3.down, out hit))
{
pos.y = hit.point.y;
float deltaHeight = ((pos.y - minY) / range) * heightAffect;
if (Random.value > deltaHeight)
{
GrassClump clump = new GrassClump(pos);
clumpsArray[index++] = clump;
}
}
}
最终效果:
完整代码:
void InitPositionsArray(int count, Bounds bounds)
{
clumpsArray = new GrassClump[count];
gameObject.AddComponent<MeshCollider>();
RaycastHit hit;
Vector3 v = new Vector3();
v.y = (bounds.center.y + bounds.extents.y);
v = transform.TransformPoint(v);
float castY = v.y;
v.Set(0, 0, 0);
v.y = (bounds.center.y - bounds.extents.y);
v = transform.TransformPoint(v);
float minY = v.y;
float range = castY - minY;
castY += 10;
int loopCount = 0;
int index = 0;
while (index < count && loopCount < (count * 10))
{
loopCount++;
Vector3 pos = new Vector3(
(Random.value * bounds.extents.x * 2 - bounds.extents.x) + bounds.center.x,
0,
(Random.value * bounds.extents.z * 2 - bounds.extents.z) + bounds.center.z);
pos = transform.TransformPoint(pos);
pos.y = castY;
if (Physics.Raycast(pos, Vector3.down, out hit))
{
pos.y = hit.point.y;
float deltaHeight = ((pos.y - minY) / range) * heightAffect;
if (Random.value > deltaHeight)
{
GrassClump clump = new GrassClump(pos);
clumpsArray[index++] = clump;
}
}
}
Debug.Log("GrassTerrain:InitPositionArray count:" + count + " index:" + index);
}
5. Trampling the grass
这次要完成的是角色与草地的交互。
主要是判断根据角色位置与草丛位置的距离是否小于踩踏半径,是的话就说明正在被踩踏,对草地进行旋转,当角色与草地逐渐远离的时候,再逐渐将草丛恢复。
这里还有一个返回四元数的函数。
四元数是一种数学表示方法,常用于描述三维空间中的旋转。四元数由四个分量组成,一个实部和三个虚部,通常表示为 q=w+xi+yj+zk,可以写成一个四元组 q=(w,x,y,z)
w(实部):
- 代表旋转角度的余弦值(通常对应旋转角的一半)
x,y,z(虚部):
- 表示旋转轴的方向。
因此首先通过两个向量相加取得中间向量,夹角也就变为了一半,接着直接点乘计算出余弦值,最后通过叉乘计算出旋转轴的方向。
float4 MapVector(float3 v1, float3 v2){
v1 = normalize(v1);
v2 = normalize(v2);
float3 v = v1+v2;
v = normalize(v);
float4 q = 0;
q.w = dot(v, v2);
q.xyz = cross(v, v2);
return q;
}
好,接下来就是在compute shader中根据距离计算踩踏幅度以及四元数。如果不在踩踏范围了,那么就逐渐减小踩踏幅度。
[numthreads(THREADGROUPSIZE,1,1)]
void UpdateGrass (uint3 id : SV_DispatchThreadID)
{
GrassClump clump = clumpsBuffer[id.x];
float3 relativePosition = clump.position - tramplePos.xyz;
float dist = length(relativePosition);
if (dist<trampleRadius){
clump.trample = ((trampleRadius - dist)/trampleRadius) * 0.9;
clump.quaternion = MapVector(float3(0,1,0), normalize(relativePosition));
}else{
clump.trample *= 0.98;
}
clump.lean = sin(time*speed) * maxLean * clump.noise;
clumpsBuffer[id.x] = clump;
}
接着在表面着色器利用踩踏幅度对草进行旋转
这个函数将四元数变化成一个旋转矩阵。
float4x4 quaternion_to_matrix(float4 quat)
{
float4x4 m = float4x4(float4(0, 0, 0, 0), float4(0, 0, 0, 0), float4(0, 0, 0, 0), float4(0, 0, 0, 0));
float x = quat.x, y = quat.y, z = quat.z, w = quat.w;
float x2 = x + x, y2 = y + y, z2 = z + z;
float xx = x * x2, xy = x * y2, xz = x * z2;
float yy = y * y2, yz = y * z2, zz = z * z2;
float wx = w * x2, wy = w * y2, wz = w * z2;
m[0][0] = 1.0 - (yy + zz);
m[0][1] = xy - wz;
m[0][2] = xz + wy;
m[1][0] = xy + wz;
m[1][1] = 1.0 - (xx + zz);
m[1][2] = yz - wx;
m[2][0] = xz - wy;
m[2][1] = yz + wx;
m[2][2] = 1.0 - (xx + yy);
m[0][3] = _Position.x;
m[1][3] = _Position.y;
m[2][3] = _Position.z;
m[3][3] = 1.0;
return m;
}
然后就是在顶点着色器中根据踩踏幅度来决定顶点的位置。
void vert(inout appdata_full v, out Input data)
{
UNITY_INITIALIZE_OUTPUT(Input, data);
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
v.vertex.xyz *= _Scale;
float4 rotatedVertex = mul(_Matrix, v.vertex);
float4 trampledVertex = mul(_TrampleMatrix, v.vertex);
v.vertex.xyz += _Position;
trampledVertex = lerp(v.vertex, trampledVertex, v.texcoord.y);
v.vertex = lerp(v.vertex, rotatedVertex, v.texcoord.y);
v.vertex = lerp(v.vertex, trampledVertex, _Trample);
#endif
}
最终效果: