
1. 高度贴图与法线贴图的基础认知第一次接触高度贴图(HeightMap)和法线贴图(NormalMap)时很多人会疑惑它们到底有什么区别。简单来说高度贴图就像一张地形等高线图用灰度值记录表面凹凸信息白色代表高处黑色代表低处。而法线贴图则像一张方向指南针用RGB颜色编码每个像素点的法线方向。我在做地形渲染时就踩过坑直接用高度贴图渲染的平面虽然能显示凹凸轮廓但光照效果非常假。这是因为标准光照计算依赖表面法线信息而高度贴图本身不包含方向数据。这时候就需要将高度数据转换为法线数据就像把等高线图变成带箭头指示的矢量图。2. Shader转换的核心算法2.1 Sobel算子实现最常用的转换方法是Sobel算子它的原理很像Photoshop中的查找边缘滤镜。在Shader中我们需要采样3x3像素区域用以下核进行卷积计算// Sobel水平梯度核 float kernelX[9] { 1, 0, -1, 2, 0, -2, 1, 0, -1 }; // Sobel垂直梯度核 float kernelY[9] { 1, 2, 1, 0, 0, 0, -1, -2, -1 };实际计算时我们会发现边缘处理很麻烦。我的经验是采用wrap采样模式让贴图首尾相连。就像下面这段处理UV坐标的代码float2 tlv float2(vUv.x - step.x, vUv.y step.y); tlv float2(tlv.x 0.0 ? tlv.x : (size.x tlv.x), tlv.y size.y ? tlv.y : (tlv.y - size.y));2.2 Scharr算子优化当需要更精细的边缘检测时可以改用Scharr算子。它的核系数更大对斜向边缘更敏感// Scharr算子计算示例 dx tl * 3.0 l * 10.0 bl * 3.0 - tr * 3.0 - r * 10.0 - br * 3.0; dy tl * 3.0 t * 10.0 tr * 3.0 - bl * 3.0 - b * 10.0 - br * 3.0;实测发现Scharr算子在处理岩石等复杂表面时能保留更多细节。但代价是计算量增加约30%需要根据项目需求权衡。3. 参数调优实战技巧3.1 强度参数_dz的玄机代码中的_dz参数控制法线强度但它的计算公式很特别float strength 2.5f; float level 7; cs.SetFloat(_dz, (float)(1.0 / strength * (1.0 Mathf.Pow(2f, level))));这个公式实际上是在做动态范围调整。当heightMap的灰度范围较大时需要适当降低_dz值避免法线过度扭曲。建议先用0.1-1.0范围测试再逐步调整。3.2 法线方向校正有时生成的法线方向会反转这时候就需要用到_invert系列参数float3 normal normalize(float3( dx * _invertR * _invertH * 255.0, dy * _invertG * _invertH * 255.0, _dz ));_invertR控制X轴方向_invertG控制Y轴方向_invertH整体高度系数遇到法线方向异常时可以尝试组合设置(1或-1)来校正。4. 几何着色器替代方案当发现传统方法生成的法线不够精确时可以尝试用Geometry Shader直接计算三角形法线。这种方法虽然性能开销大但精度极高[maxvertexcount(3)] void geom(triangle v2f p[3], inout TriangleStreamv2f stream) { for(int i0; i3; i){ float3 AB p[(i1)%3].pos - p[i].pos; float3 AC p[(i2)%3].pos - p[i].pos; p[i].normal normalize(cross(AB, AC)); stream.Append(p[i]); } }这种方法特别适合硬表面模型我在处理建筑类地形时发现它能完美还原90度转角处的法线。5. 切线空间深度解析5.1 TBN矩阵的构建很多新手会对切线空间(Tangent Space)感到困惑。其实TBN矩阵就是把模型表面的局部坐标系转换为世界坐标系的桥梁| Tx Ty Tz | | Bx By Bz | | Nx Ny Nz |构建切线(T)和副切线(B)的关键在于UV方向float3 edge1 pos2 - pos1; float3 edge2 pos3 - pos1; float2 deltaUV1 uv2 - uv1; float2 deltaUV2 uv3 - uv1; float f 1.0 / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y); tangent.x f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x); tangent.y f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y); tangent.z f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);5.2 法线贴图的蓝紫色之谜为什么法线贴图总是蓝紫色因为默认法线(0,0,1)经过转换后就是(0.5,0.5,1)。当我们在Shader中采样法线贴图时需要先进行反向转换float3 normal tex2D(_NormalMap, uv).xyz * 2 - 1;这个转换过程就像把压缩的数据解压还原。我在项目中曾经忘记这个步骤结果光照完全错乱调试了整整一天才发现问题。6. 性能优化实战6.1 计算精度取舍在移动端项目中可以将float改为half节省带宽。但要注意高度图的采样精度// 移动端优化版 half tl abs(_HeightTex.SampleLevel(sampler, tlv, 0).r);测试发现对于1024x1024以下的高度图half精度足够但4K地形还是需要float。6.2 Mipmap生成策略法线贴图的mipmap不能简单降采样否则会导致法线方向偏差。正确做法是先为高度图生成mipmap每级mipmap单独生成法线贴图或者使用特殊的法线贴图mipmap生成算法Unity中可以通过修改Texture Import设置实现textureImporter.mipmapFilter TextureImporterMipFilter.KaiserFilter;7. 常见问题排查遇到法线效果异常时建议按以下步骤检查确认高度图是线性空间而非sRGB检查UV环绕模式是否为Clamp验证_dz参数是否过小/过大检查法线贴图导入设置是否正确用帧调试器查看生成的中间纹理我在项目中遇到过最诡异的问题是由于RT格式设置为ARGB32而非ARGBFloat导致高度数据被截断生成了完全错误的法线。