项目地址:
https://github.com/Remyuu/Unity-Compute-Shader-Learngithub.com/Remyuu/Unity-Compute-Shader-Learn
L5 草地渲染
当前做的效果非常丑陋,还有很多细节没有完善,仅仅是“实现”了。由于我也是菜鸡,写/做的不够好的地方望各位指正。
知识点小结:
- 草地渲染方案
- UNITY_PROCEDURAL_INSTANCING_ENABLED
- bounds.extents
- 射线检测
- 罗德里格旋转
- 四元数旋转
前言1
前言参考文章:
草地渲染有很多方法。
最简单的是直接一张草地的纹理贴上去。
除此之外,将一个个Mesh草拖到场景中也很常见。这种方法操作空间大,每一颗草都在掌控中。虽然可以用Batching等方法优化,减少CPU到GPU的传输时间,但是这会损耗您键盘上的Ctrl、C、V和D键的寿命。不过可以在Transform组件里面用 L(a, b) 让选中的物体平均分布在 a 和 b 之间。想随机,可以用 R(a, b) 。更多相关的操作可以看官方文档。
还可以结合几何着色器和曲面细分着色器,这个方法看起来不错的,但是一个着色器只能对应一种几何(草),如果想要在这个网格生成花或者岩石,就需要在几何着色器中修改代码。这个问题其实不是最关键的,更要命的问题是很多移动设备还有Metal根本就不支持几何着色器,就算支持也只是软件模拟的,性能差劲。并且每一帧都会重新计算一次草地Mesh,浪费性能。
广告牌技术渲染草也是一种广泛流传经久不衰的方法。当我们不需要高保真的画面时,这个方法非常奏效。这个方法是简单的渲一个Quad+贴图(Alpha裁切)。用DrawProcedural就可以了。但是这个方法只可远观不可近看,否则就会大露馅。
用Unity的地形系统也可以画出非常nice的草。并且Unity使用了instancing技术确保了性能。其中最好用的地方莫过于他的笔刷工具,但是如果你的工作流没有地形系统的身影,那么你还可以用第三方插件做到。
在搜索资料的时候我还发现了一种叫Impostors「冒名顶替」技术。结合了广告牌的顶点节省优势和从多个角度真实重现对象的能力,还挺有意思。这个技术通过预先从多个角度“拍下”一个真实草的Mesh照片,通过Texture存起来。运行的时候根据当前相机的观看方向选择合适的纹理进行渲染。相当于广告牌技术的升级版。我认为Impostors技术非常适合用于那些大型但玩家可能需要从多个角度查看的对象,如树木或复杂建筑。然而,当相机非常接近或者在两个角度之间变换时,这种方法可能会出现问题。比较合理的方案是:在距离非常近用基于Mesh的方法,中等距离用Impostors,远距离用广告牌。
本文要实现的方法是基于GPU Instancing的,应该称之为「per-blade mesh grass」。在《對馬島之魂》、《原神》和《薩爾達傳說:曠野之息》等游戏上都是使用这种方案。每个草都有自己的实体,光影效果也相当真实。
渲染流程:
前言2
Unity的Instancing技术比较复杂,我也只是管中窥豹,出现错误请指正。目前的代码都是仿照文档写的。GPU instancing目前支持的平台:
- Windows: DX11 and DX12 with SM 4.0 and above / OpenGL 4.1 and above
- OS X and Linux: OpenGL 4.1 and above
- Mobile: OpenGL ES 3.0 and above / Metal
- PlayStation 4
- Xbox One
另外Graphics.DrawMeshInstancedIndirect目前已经淘汰了,应该使用 Graphics.RenderMeshIndirect ,这个函数会自动计算Bounding Box,这个就是后话了。详细请看官方文档:RenderMeshIndirect 。这篇文章也很有帮助https://zhuanlan.zhihu.com/p/403885438。
GPU Instancing原理是将多个具有相同Mesh的对象发一次Draw Call。CPU首先收集好所有信息,然后放到数组里一次性发给GPU。局限就是这些对象的Material和Mesh都要相同。这就是一次能绘制这么多草而保持高性能的原理。要实现GPU Instancing绘制上百万的Mesh,就需要遵循一些规定:
- 所有的网格需使用相同的Material
- 勾选GPU Instancing
- Shader需支持实例化
- 不支持Skin Mesh Renderer
由于不支持Skin Mesh Renderer,在上一篇文章中,我们绕过了SMR,直接取了不同关键帧的Mesh出来传给GPU,这也是上一篇文章最后提出那个问题的原因。
Unity中的Instancing分为两种主要类型:GPU Instancing和Procedural Instancing(涉及到Compute Shaders和Indirect Drawing技术),还有一种是立体渲染路径(UNITY_STEREO_INSTANCING_ENABLED),这里就不深入了。在Shader中,前者用#pragma multi_compile_instancing 后者用#pragma instancing_options procedural:setup 。具体的请看官方文档Creating shaders that support GPU instancing 。
然后目前SRP管线不支持自定义的GPU Instancing Shader,只有BIRP可以。
然后就是UNITY_PROCEDURAL_INSTANCING_ENABLED 。这个宏用于表示是否启用了Procedural Instancing。在使用Compute Shader或Indirect Drawing API时,实例的属性(如位置、颜色等)可以在GPU上实时计算并直接用于渲染,无需CPU的介入。在源代码中,关于这个宏的核心代码是:
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
#ifndef UNITY_INSTANCING_PROCEDURAL_FUNC
#error "UNITY_INSTANCING_PROCEDURAL_FUNC must be defined."
#else
void UNITY_INSTANCING_PROCEDURAL_FUNC(); // 前向声明程序化函数
#define DEFAULT_UNITY_SETUP_INSTANCE_ID(input) { UnitySetupInstanceID(UNITY_GET_INSTANCE_ID(input)); UNITY_INSTANCING_PROCEDURAL_FUNC();}
#endif
#else
#define DEFAULT_UNITY_SETUP_INSTANCE_ID(input) { UnitySetupInstanceID(UNITY_GET_INSTANCE_ID(input));}
#endif
要求Shader定义一个UNITY_INSTANCING_PROCEDURAL_FUNC函数,其实就是 setup() 函数。没有这个setup()函数,就会报错。
一般来说,setup()函数要做的就是从Buffer中取出对应(unity_InstanceID) 的数据,然后计算当前实例的位置、变换矩阵、颜色、金属度或者是自定义数据等属性。
GPU Instancing只是Unity众多优化手段的一种,仍然需要继续学习。
1. 摇曳的3-Quad草
这一章所运用关于CS的知识点在上一篇文章都已全部涉及,只不过换一个背景罢了。简单画一个示意图。
实现是使用GPU Instancing,也就是一次性渲染一大片Mesh。核心的代码就一句:
Graphics.DrawMeshInstancedIndirect(mesh, 0, material, bounds, argsBuffer);
Mesh采用三个Quad共六个三角形组成。
然后上一张贴图+Alpha Test。
草的数据结构:
- 位置
- 倾斜角度
- 随机噪声值(用于计算随机的倾斜角度)
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;
}
将需要渲染的草的Buffer(世界坐标需要计算)传给GPU。首先确定草在哪里生成、生成多少。获取当前物体的Mesh(暂时假设是一个Plane Mesh)的AABB。
Bounds bounds = mf.sharedMesh.bounds;
Vector3 clumps = bounds.extents;
确定草的范围,然后在xOz平面上随机生成草。
添加图片注释,不超过 140 字(可选)
需要注意,当前还是在物体空间,因此需要将Object Space转换到World Space。
pos = transform.TransformPoint(pos);
再结合密度density参数和物体缩放系数,计算出一共要渲染多少个草。
Vector3 vec = transform.localScale / 0.1f * density;
clumps.x *= vec.x;
clumps.z *= vec.z;
int total = (int)clumps.x * (int)clumps.z;
由于Compute Shader的逻辑是每个线程计算一棵草,极有可能需要渲染的草的数量不是线程的倍数。因此将需要渲染的草的数量向上取整到线程的倍数。也就是说,当密度因子=1的时候,渲染的草的数量等于一个线程组中线程的数量。
groupSize = Mathf.CeilToInt((float)total / (float)threadGroupSize);
int count = groupSize * (int)threadGroupSize;
让Compute Shader计算每个草的倾斜角度。
GrassClump clump = clumpsBuffer[id.x];
clump.lean = sin(time) * maxLean * clump.noise;
clumpsBuffer[id.x] = clump;
将草的位置、旋转角度传给GPU Buffer还没完,还得拜托Material决定渲染实例的最终外观,才能最终执行Graphics.DrawMeshInstancedIndirect。
渲染流程中,在实例化阶段之前(也就是procedural:setup函数内),使用unity_InstanceID确定现在渲的是哪个草。获取当前草的世界空间,草的倾倒值。
GrassClump clump = clumpsBuffer[unity_InstanceID];
_Position = clump.position;
_Matrix = create_matrix(clump.position, clump.lean);
具体的旋转+位移矩阵:
float4x4 create_matrix(float3 pos, float theta){
float c = cos(theta); // 计算旋转角度的余弦值
float s = sin(theta); // 计算旋转角度的正弦值
// 返回一个4x4变换矩阵
return float4x4(
c, -s, 0, pos.x, // 第一行:X轴旋转和位移
s, c, 0, pos.y, // 第二行:Y轴旋转(对于2D足够,但草丛可能不使用)
0, 0, 1, pos.z, // 第三行:Z轴不变
0, 0, 0, 1 // 第四行:均匀坐标(保持不变)
);
}
这个公式怎么推的呢?将(0,0,1)带入罗德里格斯公式得到一个的旋转矩阵,然后扩展到重心坐标。带入 就是代码的公式了。
用这个矩阵乘上Object Space的顶点,得到倾倒+位移的顶点坐标。
v.vertex.xyz *= _Scale;
float4 rotatedVertex = mul(_Matrix, v.vertex);
v.vertex = rotatedVertex;
这时候问题来了。目前草并不是一个平面,而是三组Quad组成的立体图形。
如果简单的将所有顶点按照z轴旋转,就会出现草根大偏移的问题。
因此借助 v.texcoord.y ,将旋转前后的顶点位置lerp起来。这样,纹理坐标的Y值越高(即顶点在模型上的位置越靠近顶部),顶点受到的旋转影响就越大。由于草根的Y值为0,lerp之后草根就不会乱晃了。
v.vertex.xyz *= _Scale;
float4 rotatedVertex = mul(_Matrix, v.vertex);
// v.vertex = rotatedVertex;
v.vertex.xyz += _Position;
v.vertex = lerp(v.vertex, rotatedVertex, v.texcoord.y);
效果很差,草太假了。这种Quad草只有在远处用用。
- 摆动僵硬
- 叶片僵硬
- 光影效果很差
当前版本代码:
- 脚本:https://github.com/Remyuu/Unity-Compute-Shader-Learn/blob/L5_3QuadGrass/Assets/Scripts/GrassClumps.cs
- Shader:https://github.com/Remyuu/Unity-Compute-Shader-Learn/blob/L5_3QuadGrass/Assets/Shaders/GrassClumps.shader
- Compute Shader:https://github.com/Remyuu/Unity-Compute-Shader-Learn/blob/L5_3QuadGrass/Assets/Shaders/GrassClumps.compute
2. 程式化草叶
上一节用几个Quad和带Alpha贴图的草,用sin wave做扰动,效果非常一般。现在用程式化的草和Perlin噪声改善。
在 C# 中定义草的顶点、法线和uv作为Mesh传到GPU上。
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)
};
Vector3 normal = new Vector3(0, 0, -1);
Vector3[] normals =
{
normal, normal, normal, normal, normal, normal, normal, normal, normal
};
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)
};
Unity的Mesh还有一个顶点顺序需要设定,默认是逆时针。如果顺时针写并且开启背面剔除,那就啥也看不见了。
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
};
mesh.SetIndices(indices, MeshTopology.Triangles, 0);
在代码那边设置好风的方向、大小还有噪声比重,打包进一个float4里面,传给Compute Shader计算一片草叶的摆动方向。
Vector4 wind = new Vector4(Mathf.Cos(theta), Mathf.Sin(theta), windSpeed, windScale);
一个草叶的数据结构
struct GrassBlade
{
public Vector3 position;
public float bend; // 随机草叶倾倒
public float noise;// CS计算噪声值
public float fade; // 随机草叶明暗
public float face; // 叶片朝向
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);
face = Random.Range(0, Mathf.PI);
}
}
当前的草叶都是一个方向的。Setup函数里,先修改叶片朝向。
// 创建绕Y轴的旋转矩阵(面向)
float4x4 rotationMatrixY = AngleAxis4x4(blade.position, blade.face, float3(0,1,0));
将草叶倾倒的逻辑(由于AngleAxis4x4是包含了位移,下图只是单独演示了叶片倾倒而没有随机朝向,如果要得到下图的效果代码中记得加入位移):
// 创建绕X轴的旋转矩阵(倾倒)
float4x4 rotationMatrixX = AngleAxis4x4(float3(0,0,0), blade.bend, float3(1,0,0));
然后合成两个旋转矩阵。
_Matrix = mul(rotationMatrixY, rotationMatrixX);
现在的光照是非常奇怪的。因为法线没有修改。
// 计算逆转置矩阵用于法线变换
float3x3 normalMatrix = (float3x3)transpose(((float3x3)_Matrix));
// 变换法线
v.normal = mul(normalMatrix, v.normal);
这里逆矩阵的代码:
float3x3 transpose(float3x3 m)
{
return float3x3(
float3(m[0][0], m[1][0], m[2][0]), // Column 1
float3(m[0][1], m[1][1], m[2][1]), // Column 2
float3(m[0][2], m[1][2], m[2][2]) // Column 3
);
}
为了代码可读性,再补上齐次坐标变换矩阵,这里升级为那个著名的旋转公式:
float4x4 AngleAxis4x4(float3 pos, float angle, float3 axis){
float c, s;
sincos(angle*2*3.14, s, c);
float t = 1 - c;
float x = axis.x;
float y = axis.y;
float z = axis.z;
return float4x4(
t * x * x + c , t * x * y - s * z, t * x * z + s * y, pos.x,
t * x * y + s * z, t * y * y + c , t * y * z - s * x, pos.y,
t * x * z - s * y, t * y * z + s * x, t * z * z + c , pos.z,
0,0,0,1
);
}
想要在不平坦的地面生成怎么办?
只需要修改生成草地初始位置高度的逻辑,用MeshCollider加射线检测,
bladesArray = new GrassBlade[count];
gameObject.AddComponent<MeshCollider>();
RaycastHit hit;
Vector3 v = new Vector3();
Debug.Log(bounds.center.y + bounds.extents.y);
v.y = (bounds.center.y + bounds.extents.y);
v = transform.TransformPoint(v);
float heightWS = v.y + 0.01f; // 浮点数误差
v.Set(0, 0, 0);
v.y = (bounds.center.y - bounds.extents.y);
v = transform.TransformPoint(v);
float neHeightWS = v.y;
float range = heightWS - neHeightWS;
// heightWS += 10; // 稍微调高一点 误差自行调整
int index = 0;
int loopCount = 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 = heightWS;
if (Physics.Raycast(pos, Vector3.down, out hit))
{
pos.y = hit.point.y;
GrassBlade blade = new GrassBlade(pos);
bladesArray[index++] = blade;
}
}
这里用射线检测每个草的位置,计算其正确高度。
还可以调整一下,海拔越高,草地越稀疏。
如上图。计算两个绿色箭头的比值,越高的海拔生成的概率越低。
float deltaHeight = (pos.y - neHeightWS) / range;
if (Random.value > deltaHeight)
{
// 生草
}
当前代码链接:
- CSharp:https://github.com/Remyuu/Unity-Compute-Shader-Learn/blob/L5_PG/Assets/Scripts/GrassBlades.cs
- Shader:https://github.com/Remyuu/Unity-Compute-Shader-Learn/blob/L5_PG/Assets/Shaders/GrassBlades.shader
- Compute Shader:https://github.com/Remyuu/Unity-Compute-Shader-Learn/blob/L5_PG/Assets/Shaders/GrassBlades.compute
现在光影啥的都没问题了。
3. 交互草
上一节中,我们先是旋转了草的朝向,又是改变了草的倾倒。现在我们还要加上一个旋转,当一个物体靠近草,就让草朝着与物体相反的方向伏倒。这意味着又来一个旋转。这个旋转并不好设置,因此改为四元数进行。而四元数的计算在Compute Shader进行。传给材质的也是四元数,存在草片的结构体中。最后在顶点着色器中将四元数转换回仿射矩阵应用旋转。
这里再加入草的随机宽和身高。因为目前每个草Mesh都是一样的,没办法通过修改Mesh的方法修改草的高度。因此只能在Vert做顶点偏移了。
// C#
[Range(0,0.5f)]
public float width = 0.2f;
[Range(0,1f)]
public float rd_width = 0.1f;
[Range(0,2)]
public float height = 1f;
[Range(0,1f)]
public float rd_height = 0.2f;
GrassBlade blade = new GrassBlade(pos);
blade.height = Random.Range(-rd_height, rd_height);
blade.width = Random.Range(-rd_width, rd_width);
bladesArray[index++] = blade;
// Setup 开头
GrassBlade blade = bladesBuffer[unity_InstanceID];
_HeightOffset = blade.height_offset;
_WidthOffset = blade.width_offset;
// Vert 开头
float tempHeight = v.vertex.y * _HeightOffset;
float tempWidth = v.vertex.x * _WidthOffset;
v.vertex.y += tempHeight;
v.vertex.x += tempWidth;
整理一下,当前的一个草Buffer存了:
struct GrassBlade{
public Vector3 position; // 世界坐标位置 - 需初始化
public float height; // 草的身高偏移 - 需初始化
public float width; // 草的宽度偏移 - 需初始化
public float dir; // 叶片朝向 - 需初始化
public float fade; // 随机草叶明暗 - 需初始化
public Quaternion quaternion; // 旋转参数 - CS计算->Vert
public float padding;
public GrassBlade( Vector3 pos){
position.x = pos.x;
position.y = pos.y;
position.z = pos.z;
height = width = 0;
dir = Random.Range(0, 180);
fade = Random.Range(0.99f, 1);
quaternion = Quaternion.identity;
padding = 0;
}
}
int SIZE_GRASS_BLADE = 12 * sizeof(float);
用来表示从向量 v1 旋转到向量 v2 的四元数 q :
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;
}
想要组合两个旋转的四元数,需要用乘法(注意顺序)。
假设有两个四元数 和 。它们的乘积 计算公式是 :
其中 是 的实部和虚部分量, 是 的实部和虚部分量。
float4 quatMultiply(float4 q1, float4 q2) {
// q1 = a + bi + cj + dk
// q2 = x + yi + zj + wk
// Result = q1 * q2
return float4(
q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y, // X component
q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x, // Y component
q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w, // Z component
q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z // W (real) component
);
}
要确定草是往哪个地方倒,就需要获取交互物体trampler的Pos,也就是其Transform组件。并且每一帧都通过SetVector传到GPU Buffer中,给Compute Shader用,所以把GPU的内存地址当作ID存着,不需要每次都用字符串访问。还要确定多大范围内的草要倒下,倒与不倒之间怎么过渡,给GPU传一个 trampleRadius ,由于这个是常数,就不用每一帧都修改,因此直接用字符串Set一下就好了。
// CSharp
public Transform trampler;
[Range(0.1f,5f)]
public float trampleRadius = 3f;
...
Init(){
shader.SetFloat("trampleRadius", trampleRadius);
tramplePosID = Shader.PropertyToID("tramplePos");
}
Update(){
shader.SetVector(tramplePosID, pos);
}
本节把所有旋转的操作都丢进Compute Shader里面一次算完,直接返回一个四元数给材质。首先是q1计算随机朝向的四元数,q2计算随机倾倒,qt计算交互的倾倒。这里可以在Inspector开放一个交互的系数。
[numthreads(THREADGROUPSIZE,1,1)]
void BendGrass (uint3 id : SV_DispatchThreadID)
{
GrassBlade blade = bladesBuffer[id.x];
float3 relativePosition = blade.position - tramplePos.xyz;
float dist = length(relativePosition);
float4 qt;
if (dist<trampleRadius){
float eff = ((trampleRadius - dist)/trampleRadius) * 0.6;
qt = MapVector(float3(0,1,0), float3(relativePosition.x*eff,1,relativePosition.z*eff));
}else{
qt = MapVector(float3(0,1,0),float3(0,1,0));
}
float2 offset = (blade.position.xz + wind.xy * time * wind.z) * wind.w;
float noise = perlin(offset.x, offset.y) * 2 - 1;
noise *= maxBend;
float4 q1 = MapVector(float3(0,1,0), (float3(wind.x * noise,1,wind.y*noise)));
float faceTheta = blade.dir * 3.1415f / 180.0f;
float4 q2 = MapVector(float3(1,0,0),float3(cos(faceTheta),0,sin(faceTheta)));
blade.quaternion = quatMultiply(qt,quatMultiply(q2,q1));
bladesBuffer[id.x] = blade;
}
然后四元数到旋转矩阵的方法是:
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
float tempHeight = v.vertex.y * _HeightOffset;
float tempWidth = v.vertex.x * _WidthOffset;
v.vertex.y += tempHeight;
v.vertex.x += tempWidth;
// 应用模型顶点变换
v.vertex = mul(_Matrix, v.vertex);
v.vertex.xyz += _Position;
// 计算逆转置矩阵用于法线变换
v.normal = mul((float3x3)transpose(_Matrix), v.normal);
#endif
}
void setup()
{
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
// 获取Compute Shader计算结果
GrassBlade blade = bladesBuffer[unity_InstanceID];
_HeightOffset = blade.height_offset;
_WidthOffset = blade.width_offset;
_Fade = blade.fade; // 设置明暗
_Matrix = quaternion_to_matrix(blade.quaternion); // 设置最终转转矩阵
_Position = blade.position; // 设置位置
#endif
}
当前代码链接:
- CSharp:https://github.com/Remyuu/Unity-Compute-Shader-Learn/blob/L5_Finally/Assets/Scripts/GrassBlades.cs
- Shader:https://github.com/Remyuu/Unity-Compute-Shader-Learn/blob/L5_Finally/Assets/Shaders/GrassBlades.shader
- Compute Shader:https://github.com/Remyuu/Unity-Compute-Shader-Learn/blob/L5_Finally/Assets/Shaders/GrassBlades.compute
4. 总结/小测试
How do you programmatically get the thread group sizes of a kernel?
When defining a Mesh in code, the number of normals must be the same as the number of vertex positions. True or false.
发表回复