文章目录
- 前言
- 一、什么是世界空间法线纹理映射?
- 1. 世界空间法线纹理映射工作原理
- 2. 什么是世界空间?
- 3. 切线空间法线纹理映射和世界空间法线纹理映射对比
- 世界空间法线纹理映射:
- 优点:
- 缺点:
- 切线空间法线纹理映射:
- 优点:
- 缺点:
- 4. 法线映射例图
- 二、使用步骤
- 1. Shader 属性定义
- 2. SubShader 设置
- 3. 渲染 Pass
- 4. 定义结构体和顶点着色器函数
- 5. 片元着色器函数
- 三、效果
- 四、总结
前言
法线纹理映射(Normal Mapping)是一种在计算机图形学中常用的技术,它可以在不增加几何体细节的情况下,为物体表面增加凹凸的视觉效果。在这篇博客中,我们将介绍一种特殊的法线纹理映射技术——世界空间法线纹理映射(World Space Normal Mapping)的实现原理和效果展示。
一、什么是世界空间法线纹理映射?
1. 世界空间法线纹理映射工作原理
世界空间法线纹理映射是一种常用的法线纹理映射技术,它在计算光照效果时将法线贴图中的法线向量从切线空间(Tangent Space)变换到世界空间(World Space),以实现更加真实的光照效果。与切线空间法线纹理映射相比,世界空间法线纹理映射不受物体旋转、缩放和变形等变换的影响,因此具有更高的灵活性和通用性。
2. 什么是世界空间?
在计算机图形学中,"世界空间"指的是一个三维坐标系,用来描述整个场景中各个物体的位置、旋转和缩放关系。它是一个全局的坐标系,相对于整个场景固定不变。在渲染过程中,所有的物体都是相对于世界空间进行位置和旋转的。
3. 切线空间法线纹理映射和世界空间法线纹理映射对比
世界空间法线纹理映射:
优点:
相对简单: 实现起来相对简单直观,不需要进行切线空间到世界空间的转换。
适用范围广: 对于一些不需要考虑物体形变的情况下,如地面、天空等,世界空间法线纹理映射是一种有效的技术。
不受物体形变影响: 由于是在世界空间中进行计算,不受物体形变的影响,对于静态物体效果较好。
缺点:
不适用于形变物体: 对于需要进行形变的物体,如角色动画、流体等,世界空间法线纹理映射无法准确表现表面细节。
纹理拉伸问题: 在物体形变时,可能会出现纹理拉伸或者变形的问题,影响视觉效果。
切线空间法线纹理映射:
优点:
适用于形变物体: 由于是在切线空间中进行计算,可以准确地跟随物体形变,适用于动态变化的物体表面。
纹理映射稳定: 在物体形变时,切线空间法线纹理映射能够保持纹理映射的稳定性,不会出现拉伸或变形的问题。
缺点:
计算复杂: 需要进行切线空间到世界空间的转换,计算复杂度较高,对性能有一定的要求。
需要切线信息: 需要额外的切线信息来进行计算,增加了模型制作和导入的复杂度。
不适用于全局效果: 在一些需要考虑全局效果的情况下,如全局光照、全局反射等,切线空间法线纹理映射可能会出现不理想的效果。
综上所述,世界空间法线纹理映射适用于静态物体或者不需要考虑形变的情况,实现简单但不适用于动态形变物体。而切线空间法线纹理映射适用于动态形变物体,能够准确跟随形变,但需要额外的切线信息并且计算复杂度较高。选择合适的方法取决于具体的应用场景和需求。
4. 法线映射例图
切线空间下法线映射图看起来几乎全部都是蓝色的,这是因为,每个法线方向所在的坐标空间是不一样的,即是表面每点各自的切线空间。切线空间是一种相对于物体表面的局部坐标系,它的原点是物体表面上的某个顶点,它的三个坐标轴分别是切线方向(Tangent)、副切线方向(Binormal)和法线方向(Normal)。
切线空间下法线映射图的RGB通道分别对应了切线空间的XYZ轴,也就是说,红色通道表示切线方向的分量,绿色通道表示副切线方向的分量,蓝色通道表示法线方向的分量。由于法线一般都是指向表面外侧,也就是切线空间的Z轴正方向,因此蓝色通道的值一般都比较大,接近于1.0,而红色和绿色通道的值则根据法线的偏移而变化,一般在0.0到1.0之间。因此,切线空间下法线映射图的颜色会偏向于蓝色,而且越是平坦的表面,越是纯蓝色,越是凹凸的表面,越是有其他颜色的混合。
二、使用步骤
1. Shader 属性定义
// 定义属性
Properties
{
_MainTex("MainTex",2D)="white"{} // 主纹理贴图
_BumpMap("Normal Map",2D)="bump"{} // 法线贴图
_BumpScale("BumpScale",float)=1 // 法线贴图的缩放系数
_Diffuse("Diffuse",Color)=(1,1,1,1) // 漫反射颜色属性,默认白色
_Specular("Specular",Color)=(1,1,1,1) // 高光颜色属性,默认白色
_Gloss("Gloss",Range(1,256))=5 // 高光反射系数
}
2. SubShader 设置
SubShader
{
Tags
{
"RenderType" = "Opaque" // 渲染类型为不透明
}
LOD 100 // 细节级别
}
SubShader 定义了一组渲染设置,包括标签和细节级别。在这里,我们将渲染类型标签设置为 “Opaque”,表示物体是不透明的。
3. 渲染 Pass
Pass
{
CGPROGRAM
// 顶点着色器函数声明
#pragma vertex vert
// 片段着色器函数声明
#pragma fragment frag
// 包含Unity CG库
#include "UnityCG.cginc"
// 包含光照CG库
#include "Lighting.cginc"
// 漫反射颜色属性
fixed4 _Diffuse;
// 高光颜色属性
fixed4 _Specular;
// 高光系数属性
float _Gloss;
//贴图
sampler2D _MainTex;
//_MainTex附属属性,不需要在Properties定义
float4 _MainTex_ST;
//纹理法线贴图
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
}
这里开始了渲染 Pass 部分。在这里,我们使用了 CGPROGRAM 指令来声明顶点着色器和片元着色器函数。#pragma vertex vert 和 #pragma fragment frag 分别指定了顶点着色器函数和片元着色器函数的名称。
然后,我们包含了 UnityCG.cginc 和 Lighting.cginc,它们提供了许多有用的函数和宏,用于简化编写 Shader。
4. 定义结构体和顶点着色器函数
// 定义结构体:从顶点到片段的数据传递
struct v2f {
float4 vertex: SV_POSITION; // 顶点位置
float4 uv: TEXCOORD0; // 纹理坐标
float4 TtoW0: TEXCOORD1; // 切线空间到世界空间的转换矩阵
float4 TtoW1: TEXCOORD2; // 切线空间到世界空间的转换矩阵
float4 TtoW2: TEXCOORD3; // 切线空间到世界空间的转换矩阵
};
// 顶点着色器函数
v2f vert(appdata_tan v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex); // 顶点位置变换到裁剪空间
// 让外面的属性可以影响到uv
// o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
// uv计算简化函数
o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
// 求世界顶点坐标
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// 求世界法线
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
// 求世界切线
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
// 求世界坐标的副切线
fixed3 worldbinormal = cross(worldNormal, worldTangent) * v.tangent.w;
// 按列摆放得到从切线空间到世界空间的变换矩阵
o.TtoW0 = float4(worldTangent.x, worldbinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldbinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldbinormal.z, worldNormal.z, worldPos.z);
return o;
}
顶点着色器的输入是一个结构体 appdata_tan ,它包含了顶点的位置、法线、切线和贴图坐标等信息。顶点着色器的输出是一个结构体 v2f ,它包含了顶点的裁剪空间位置、uv、切线空间到世界空间的转换矩阵等信息。
5. 片元着色器函数
// 片段着色器函数
fixed4 frag(v2f i): SV_Target {
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
// 计算世界空间下的光照方向和视角方向
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
// 获得法线纹理
// 获取切线空间下的光源方向
// 法线贴图采样
fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
// 把0,1还原成-1,1
// 没有把textureType设置成normalMap属性
// fixed3 tangentNormal;
// tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
// tangentNormal.z = sqrt(1 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
// 把textureType设置成normalMap属性
// UnpackNormal解压packedNormal
fixed3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
// 把切线空间法线转换到世界坐标
fixed3 worldNormal = normalize(float3(dot(i.TtoW0.xyz, tangentNormal), dot(i.TtoW1.xyz, tangentNormal),
dot(i.TtoW2.xyz, tangentNormal)));
// 获取环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 纹理采样
fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb;
// 漫反射
fixed3 diffuse = _LightColor0.rgb * albedo * _Diffuse.rgb * max(0, dot(lightDir, worldNormal) * 0.5 + 0.5);
// 高光反射
// 计算半向量
fixed3 halfDir = normalize(lightDir + viewDir);
// 计算高光颜色
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
// 组合最终颜色
fixed3 color = diffuse + ambient + specular;
return fixed4(color, 1); // 输出颜色
}
片元着色器的输入是一个结构体 v2f ,包含了顶点的裁剪空间位置、uv、切线空间到世界空间的转换矩阵。片元着色器的输出是一个 fixed4 类型的颜色值,它表示了最终屏幕上的像素颜色。
三、效果
右:切线空间 左:世界空间
四、总结
法线纹理映射是一种技术,用于在低多边形模型上模拟高多边形模型的细节。它通过使用一张法线贴图,存储每个像素的法线方向,来影响光照计算。
法线纹理映射有两种常见的方式:世界空间法线纹理映射和切线空间法线纹理映射。它们的区别在于法线贴图中的法线方向是相对于哪个坐标空间的。
世界空间法线纹理映射是指法线贴图中的法线方向是相对于世界空间的,它们是绝对的,不随模型的变换而变化。这种方式的优点是简单直观,不需要额外的计算或数据来转换法线方向。缺点是不适用于动态变换的模型,比如骨骼动画,因为法线方向不会随着模型的变形而变化,导致光照错误。
切线空间法线纹理映射是指法线贴图中的法线方向是相对于切线空间的,它们是相对的,随模型的变换而变化。切线空间是位于模型表面上的一个局部坐标空间,它的三个轴分别是切线方向、副切线方向和法线方向。这种方式的优点是通用性好,适用于动态变换的模型,因为法线方向会随着模型的变形而变化,保持正确的光照。缺点是需要额外的计算或数据来转换法线方向,比如需要存储或计算每个顶点的切线和副切线,以及构建一个从切线空间到世界空间的变换矩阵。