此文是基于之前的一篇:Unity Shader - 使用Noise噪点图生成简单山脉(使用tex2Dlod控制顶点高度)写的,这篇使用tex2Dlod的,虽然将控制顶点的高度逻辑放到了vertex shader(顶点着色器)中读取高度图来控制了,但是却丢失了实时的法线控制,因为之前在CSharp脚本控制山脉顶点高度时有调用:Mesh.RecalculateNormals();
,所以运行后,可以看到山脉可以与灯光有正常的交互。
那么这儿重新写的这篇就是既能在vertex shader中控制高度,也能保留实时调整顶点法线,让山脉与灯光能正常交互。
效果
材质有个:Rebuild Normal 的下拉列表项:On就是开启重构法线,Off就是关闭。
可以看到Off时,整个山脉就如同一个Quad的光照一下,没有立体感。On之后,可以看到山脉可以与方向光有交互了。
Shader
先大概看看代码,后面会将到思路。
主要在vertex shader处理就好了。
其他代码不变:
请详细查看注释,该说明的都说了。
// jave.lin 2020.03.14 - vertex shader重构法线核心逻辑
v2f vert (appdata v) {
v2f o;
...
#if _REBUILD_NORMAL_ON // 开始重构法线
// reconstruct normals
// 这个4x4数据也可以通过外部传入,可以节省顶点着色器ALU的计算量与L1 caches缓存量。
const float4x4 offset_xz = {
{+1,+0, /* gap **/ +1,-1}, // 右下
{+0,-1, /* gap **/ -1,-1}, // 左下
{-1,+0, /* gap **/ -1,+1}, // 左上
{+0,+1, /* gap **/ +1,+1} // 右上
};
// gridGap是没个顶点上下左右的距离(在CSharp端生成时设置,可以传入shader也行,这里我偷懒,就写死了)
const float gridGap = 5;
// 默认向量也可以外部传入,因为上面的默认法线是可以调整的
// 下面我讲默认法线初始化为:up
float3 sumNormal = float3(0, 1, 0);
for (int i = 0; i < 4; i++) {
// 获取偏移数据
float4 uvs_offset = offset_xz[i];
// 获取偏移数据分别的高度
half h1 = tex2Dlod(_NoiseTex, float4(v.uv + uvs_offset.xy * _NoiseTex_TexelSize.xy, 0, 0)).r * _MountainHeight;
half h2 = tex2Dlod(_NoiseTex, float4(v.uv + uvs_offset.zw * _NoiseTex_TexelSize.xy, 0, 0)).r * _MountainHeight;
// 根据偏移方向,高度重构从当前顶点,指向附近偏移点的高度两个向量
float3 dir1 = float3(uvs_offset.x * gridGap, h1 - centerH, uvs_offset.y * gridGap);
float3 dir2 = float3(uvs_offset.z * gridGap, h2 - centerH, uvs_offset.w * gridGap);
// 根据平面两个向量(两个向量可以确定一个平面,如同:TBN当中的TB两个切向量)
// 叉乘得到平面的法线向量
float3 newNormal = (cross(dir1, dir2));
// 累加到混合向量中
sumNormal += newNormal;
}
// 均值混合
sumNormal /= 5; // 改为 sumNormal *= 0.2; 效果更高,乘法比除法效率要高
o.normal = UnityObjectToWorldNormal(sumNormal);
#else // _REBUILD_NORMAL_OFF
o.normal = UnityObjectToWorldNormal(v.normal);
#endif
...
return o;
}
思路
- 每个顶点的高度,都可是通过vertex shader中,tex2Dlod来读取纹理中的高度中。
- 当前点的高度得到后,取附近两个点的高度。
- 通过高度,gridGrap与生成:从当前点,指向两个附近点的向量:
dir1
,dir2
。 - 通过
dir1
,dir2
叉乘计算出法线,并累加到混合法线:sumNormal
。 - 最后
sumNormal
混合法线均值混合为该顶点的法线。
为了方便理解,我在Excel与Win10 Paint.exe绘制了一些图解:
下面为图1:
下面为图2:
关于叉乘可以参考这篇:Basis Orientations in 3ds Max, Unity 3D and Unreal Engine
下面引用博主的两张图:
Basis orientations in 3ds Max, Unity 3D and Unreal Engine (right and left hand rules are shown) - 在3ds Max, Unity 3D 和 Unreal 引擎 的基本朝向(左右手坐标的规则显示)
- 3ds Max – right handed, z-up - 右手坐标,z 向上
- Unity 3D – left handed, y-up - 左手坐标,y 向上
- Unreal Engine – left handed, z-up - 左手坐标,y 向上
右手坐标的 叉乘规则
Project
backup : UnityShader_NoiseMountain_RebuildNormals_2018.3.0f2
2021/09/01 的今天,偶然在反编译某个插件中的代码,法线 c sharp 上的脚本的重构法线,也可以这么写,思路差不多:
private static void GenerateNormals(Vector3[] _vertices, int[] _trinagles, out Vector3[] _normals)
{
_normals = new Vector3[_vertices.Length];
List<List<Vector3>> list = new List<List<Vector3>>();
for (int i = 0; i < _vertices.Length; i++)
{
list.Add(new List<Vector3>());
}
for (int j = 0; j < _trinagles.Length; j += 3)
{
Vector3 b = _vertices[_trinagles[j]];
Vector3 a = _vertices[_trinagles[j + 1]];
Vector3 a2 = _vertices[_trinagles[j + 2]];
Vector3 lhs = a - b;
Vector3 rhs = a2 - b;
Vector3 item = Vector3.Cross(lhs, rhs);
item.Normalize();
list[_trinagles[j]].Add(item);
list[_trinagles[j + 1]].Add(item);
list[_trinagles[j + 2]].Add(item);
}
for (int k = 0; k < _vertices.Length; k++)
{
Vector3 zero = Vector3.zero;
for (int l = 0; l < list[k].Count; l++)
{
zero += list[k][l];
}
_normals[k] = zero / list[k].Count;
}
}