首页 > 其他分享 >【Unity3D】法线贴图和凹凸映射

【Unity3D】法线贴图和凹凸映射

时间:2023-06-18 20:34:33浏览次数:47  
标签:贴图 Unity3D 法线 切线 纹理 空间 tangentNormal 向量

1 法线贴图原理

表面着色器中介绍了使用表面着色器进行法线贴图,实现简单快捷。本文将介绍使用顶点和片元着色器实现法线贴图和凹凸映射,实现更灵活。

​ 本文完整代码资源见→法线贴图和凹凸映射

1)光照原理

​ Phong 光照模型和 Blinn Phong 光照模型是应用比较广泛的光照模型,两者区别在与镜面反射光的计算,Phong 光照模型根据反向量和观察向量计算镜面反射光,Blinn Phong 光照模型根据半向量和法向量计算镜面反射光。

img

​ 光照计算如下:

// 模型自身颜色
fixed4 albedo = tex2D(_MainTex, i.uv) * _ModelColor;
// 环境光
fixed4 ambient = UNITY_LIGHTMODEL_AMBIENT * albedo;
// 漫反射光
fixed4 diffuse = _LightColor0 * albedo * max(0, dot(normal, lightDir)); 
// 镜面反射光(Phong光照模型)
// fixed4 specular = _LightColor0 * _Specular * pow(max(0, dot(reflectDir, viewDir)), _Gloss); 
// 镜面反射光(Blinn Phong光照模型)
fixed4 specular = _LightColor0 * _Specular * pow(max(0, dot(normal, halfDir)), _Gloss); 
// 合成颜色
fixed4 finalColor = fixed4(ambient + diffuse + specular, 1.0);

​ 其中,_MainTex 表示 主纹理,_ModelColor、_LightColor0 分别表示模型颜色、灯光颜色,UNITY_LIGHTMODEL_AMBIENT 表示环境光强度,normal、lightDir、viewDir、halfDir 分别表示法向量、灯光向量、观察向量、半向量(见上图,它们都已归一化)。

2)法线纹理

​ 如下,左侧是纹理图,右侧是其对应的法线纹理图。

img

​ 法线向量归一化后,每个分量的值域是 [-1, 1],为了使用 RGB 颜色显示法线向量,需要将法线向量映射到区间 [0, 1],映射函数是:y = (x + 1) / 2;由法线纹理还原到法线向量,映射函数是:y = x * 2 - 1。由于法线向量都是归一化的,并且方向始终是由内侧指向外侧,因此可以省去 z 维的存储空间,通过 z = sqrt(x * x + y * y) 推出 z 值。

​ 由法线纹理获取法线向量的方法如下:

fixed4 packedNormal = tex2D(_NormalTex, i.uv);
fixed3 tangentNormal = UnpackNormal(packedNormal); // 切线空间法线向量
// 如果法线纹理未被标记为"Normal map", 可以使用以下方式求出法线纹理中片元的法线值
// tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
// tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

​ 说明:法线纹理需要设置 Texture Type 为 Normal map,如下,否则 UnpackNormal 函数会失效,需要使用注释中的代码还原法线向量。其中,_BumpScale 反映了物体表面的凹凸程度。

img

3)切线空间

​ 从法线纹理图中采样并通过 UnpackNormal 还原后得到的法线向量,是切线空间中的向量,切线空间坐标系的定义如下。注意:切线空间坐标系是右手坐标系。

img

​ 切线空间坐标轴正方向单位向量对应的世界坐标系中的向量如下:

struct a2v {
	float4 vertex : POSITION; // 模型空间顶点坐标
	float3 normal : NORMAL; // 模型空间法线坐标(几何法线, 非纹理法线)
	float4 tangent : TANGENT; // 模型空间切线坐标(几何切线, 非纹理切线)
};

v2f vert(a2v v) {
	v2f o;
	fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); // 法线(z轴)
	fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz); // 切线(x轴)
	fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; // 副切线(y轴)
	return o;
}

​ 说明: worldTangent、worldBinormal、worldNormal 分别为切向空间坐标系 x、y、z 轴正方向单位向量(世界坐标系下的坐标)。

4)切线空间与世界空间的变换

​ 漫反射和镜面反射光照计算使用了法线向量、灯光向量、观察向量,但是它们所处的坐标系不相同,为方便计算光照,需要统一坐标系,即将纹理法线向量由切线坐标系转换到世界坐标系,或者将灯光向量和观察向量由世界坐标系转换到切线坐标系。注意,之所以不在模型空间计算光照(即将法线向量、灯光向量、观察向量转换到模型空间,再进行光照计算),因为如果模型变换中存在非统一缩放,会导致世界空间下的法线与切线不垂直,详见空间和变换 中 2.5 节法线变换。

​ 假设切线空间 x、y、z 坐标轴正方向单位向量对应的世界坐标系中的向量分别为 r、u、f(即上文中的 worldTangent、worldBinormal、worldNormal),世界坐标系下 x、y、z 轴正方向对应的方向向量分别为 e1、e2、e3,因此存在以下关系:

img

​ 由于 r、u、f 两两正交,并且都是单位向量,因此它们组成的的矩阵是正交矩阵,即 A-1 = A',因此 e1、e2、e3 在一组基向量 r、u、f 下的表示如下:

img

​ 切线坐标系下的任意向量 v 对应的世界坐标系下的向量如下:

img

​ 因此,切线空间→世界空间的变换矩阵如下:

img

​ 世界坐标系下的任意向量 v 对应的切线坐标系下的向量如下:

img

​ 因此,世界空间→切线空间的变换矩阵如下:

img

5)切线坐标系下法线向量对应的世界坐标系坐标

// 1. 计算[切线空间->世界空间]的变换矩阵
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); // 法线(z轴)
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz); // 切线(x轴)
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; // 副切线(y轴)
float3x3 tangentToWorld = transpose(float3x3(worldTangent, worldBinormal, worldNormal)); // 切线空间->世界空间
// 2. 计算切线空间的法线向量
fixed4 packedNormal = tex2D(_NormalTex, i.uv.zw);
fixed3 tangentNormal = UnpackNormal(packedNormal); // 切线空间法线向量
// 3. 计算世界空间的法线向量
float3 normal = mul(tangentToWorld, tangentNormal);

6)世界坐标系下灯光向量、观察向量对应的切线坐标系坐标

// 1. 计算[世界空间->切线空间]的变换矩阵
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); // 法线(z轴)
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz); // 切线(x轴)
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; // 副切线(y轴)
float3x3 worldToTangent = float3x3(worldTangent, worldBinormal, worldNormal); // 世界空间->切线空间
// 2. 计算切线空间的灯光向量、观察向量
float3 lightDir = mul(worldToTangent, WorldSpaceLightDir(v.vertex));
float3 viewDir = mul(worldToTangent, WorldSpaceViewDir(v.vertex));

7)凹凸映射

​ 为了使物体表面呈现动态凹凸变化,可以通过以下方式动态调整法线。

fixed4 packedNormal = tex2D(_NormalTex, i.uv); // 法线纹理采样
fixed3 tangentNormal = UnpackNormal(packedNormal); // 切线空间法线向量
tangentNormal.xy *= _BumpScale; // 法线凹凸映射
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy))); // 重新调整法线z值

2 切线空间中计算光照

​ NormalMapInTangentSpace.shader

Shader "MyShader/NormalMapInTangentSpace" {
	Properties {
		_Color ("Color", Color) = (1, 1, 1, 1) // 贴图颜色
		_MainTex ("MainTex", 2D) = "white" {} // 主纹理
		_NormalTex ("NormalTex", 2D) = "bump" {} // 法线纹理
		_BumpScale ("Bump Scale", Range(-2, 2)) = -2 // 法线纹理凹凸比例
		_Specular ("Specular", Color) = (1, 1, 1, 1) // 镜面反射颜色
		_Gloss ("Gloss", Range(8.0, 256)) = 20 // 镜面反射光泽度
	}

	SubShader {
		Pass { 
			Tags { "LightMode"="ForwardBase" }
		
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Lighting.cginc"
			
			fixed4 _Color; // 贴图颜色
			sampler2D _MainTex; // 主纹理
			float4 _MainTex_ST; // 主纹理缩放和偏移
			sampler2D _NormalTex; // 法线纹理
			float4 _NormalTex_ST; // 法线纹理缩放和偏移
			float _BumpScale; // 法线纹理凹凸比例
			fixed4 _Specular; // 镜面反射光颜色
			float _Gloss; // 镜面反射光泽度
			
			struct a2v {
				float4 vertex : POSITION; // 模型空间顶点坐标
				float3 normal : NORMAL; // 模型空间法线坐标(仅用于构造世界空间到切线空间的变换矩阵)
				float4 tangent : TANGENT; // 模型空间切线坐标(仅用于构造世界空间到切线空间的变换矩阵)
				float2 texcoord : TEXCOORD0; // 纹理坐标
			};
			
			struct v2f {
				float4 pos : SV_POSITION; // 裁剪空间顶点坐标
				float4 uv : TEXCOORD0; // xy存储主纹理坐标, zw存储法线纹理坐标
				float3 lightDir: TEXCOORD1; // 切线空间光线向量(顶点指向光源)
				float3 viewDir : TEXCOORD2; // 切线空间观察向量(顶点指向相机)
			};

			v2f vert(a2v v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex); // 模型空间顶点坐标变换到裁剪空间顶点坐标, 等价于: mul(UNITY_MATRIX_MVP, v.vertex)
				o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // 主纹理缩放和偏移
				o.uv.zw = v.texcoord.xy * _NormalTex_ST.xy + _NormalTex_ST.zw; // 法线纹理缩放和偏移
				// 世界空间法线、切线、副切线
				fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
				fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);  
				fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; 
				// 世界空间坐标变换到切线空间坐标的变换矩阵
				float3x3 worldToTangent = float3x3(worldTangent, worldBinormal, worldNormal);
				// 将光线向量、观察向量由世界空间变换到切线空间
				o.lightDir = mul(worldToTangent, WorldSpaceLightDir(v.vertex));
				o.viewDir = mul(worldToTangent, WorldSpaceViewDir(v.vertex));
				// 如果模型变换中不包含非统一缩放, 可以将光线向量和观察向量直接由模型空间变换到切线空间, 而不必先转换到世界空间
				// float3 objectBinormal = cross(normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w;
				// float3x3 objectToTangent = float3x3(v.tangent.xyz, objectBinormal, v.normal);
				// o.lightDir = mul(objectToTangent, ObjSpaceLightDir(v.vertex));
				// o.viewDir = mul(objectToTangent, ObjSpaceViewDir(v.vertex));
				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target {				
				fixed3 tangentLightDir = normalize(i.lightDir); // 切线空间光线向量归一化
				fixed3 tangentViewDir = normalize(i.viewDir); // 切线空间观察向量归一化
				fixed4 packedNormal = tex2D(_NormalTex, i.uv.zw); // 法线纹理采样
				fixed3 tangentNormal = UnpackNormal(packedNormal); // 切线空间法线向量
				// 如果法线纹理未被标记为"Normal map", 可以使用以下方式求出法线纹理中片元的法线值
				//tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
				//tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
				tangentNormal.xy *= _BumpScale; // 法线凹凸映射
				tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy))); // 重新调整法线z值
				fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb; // 物体自身颜色
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; // 环境光
				fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir)); // 漫反射光
				fixed3 halfDir = normalize(tangentLightDir + tangentViewDir); // 半向量
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss); // 镜面反射光
				return fixed4(ambient + diffuse + specular, 1.0);
			}
			
			ENDCG
		}
	}

	FallBack "Specular"
}

​ 通过调节 _BumpScale 值在区间 [-2, 2] 之间变化,使得胶囊体呈现动态凹凸变化,效果如下:

img

3 世界空间中计算光照

​ NormalMapInWorldSpace.shader

Shader "MyShader/NormalMapInWorldSpace" {
	Properties {
		_Color ("Color", Color) = (1, 1, 1, 1) // 贴图颜色
		_MainTex ("MainTex", 2D) = "white" {} // 主纹理
		_NormalTex ("NormalTex", 2D) = "bump" {} // 法线纹理
		_BumpScale ("Bump Scale", Range(-2, 2)) = -2 // 法线纹理凹凸比例
		_Specular ("Specular", Color) = (1, 1, 1, 1) // 镜面反射颜色
		_Gloss ("Gloss", Range(8.0, 256)) = 20 // 镜面反射光泽度
	}

	SubShader {
		Pass { 
			Tags { "LightMode"="ForwardBase" }
		
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Lighting.cginc"
			
			fixed4 _Color; // 贴图颜色
			sampler2D _MainTex; // 主纹理
			float4 _MainTex_ST; // 主纹理缩放和偏移
			sampler2D _NormalTex; // 法线纹理
			float4 _NormalTex_ST; // 法线纹理缩放和偏移
			float _BumpScale; // 法线纹理凹凸比例
			fixed4 _Specular; // 镜面反射光颜色
			float _Gloss; // 镜面反射光泽度
			
			struct a2v {
				float4 vertex : POSITION; // 模型空间顶点坐标
				float3 normal : NORMAL; // 模型空间法线坐标(仅用于构造世界空间到切线空间的变换矩阵)
				float4 tangent : TANGENT; // 模型空间切线坐标(仅用于构造世界空间到切线空间的变换矩阵)
				float2 texcoord : TEXCOORD0; // 纹理坐标
			};
			
			struct v2f {
				float4 pos : SV_POSITION; // 裁剪空间顶点坐标
				float4 uv : TEXCOORD0; // xy存储主纹理坐标, zw存储法线纹理坐标
				float4 T2W0 : TEXCOORD1; // 切线空间到世界空间的变换矩阵的第一行
				float4 T2W1 : TEXCOORD2; // 切线空间到世界空间的变换矩阵的第二行
				float4 T2W2 : TEXCOORD3; // 切线空间到世界空间的变换矩阵的第三行
			};

			v2f vert(a2v v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex); // 模型空间顶点坐标变换到裁剪空间顶点坐标, 等价于: mul(UNITY_MATRIX_MVP, v.vertex)
				o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // 主纹理缩放和偏移
				o.uv.zw = v.texcoord.xy * _NormalTex_ST.xy + _NormalTex_ST.zw; // 法线纹理缩放和偏移
				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; 
				// 切线空间到世界空间的变换矩阵(为充分利用GPU插值寄存器, 将顶点坐标保存到w维中)
				o.T2W0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
				o.T2W1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
				o.T2W2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target {				
				float3 worldPos = float3(i.T2W0.w, i.T2W1.w, i.T2W2.w); // 世界空间顶点坐标
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos)); // 世界空间光线向量
				fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos)); // 世界空间观察向量
				fixed4 packedNormal = tex2D(_NormalTex, i.uv.zw); // 法线纹理采样
				fixed3 tangentNormal = UnpackNormal(packedNormal); // 切线空间法线向量
				// 如果法线纹理未被标记为"Normal map", 可以使用以下方式求出法线纹理中片元的法线值
				//tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
				//tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
				tangentNormal.xy *= _BumpScale; // 法线凹凸映射
				tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy))); // 重新调整法线z值
				// 世界空间法线坐标
				fixed3 worldNormal = normalize(half3(dot(i.T2W0.xyz, tangentNormal), dot(i.T2W1.xyz, tangentNormal), dot(i.T2W2.xyz, tangentNormal)));
				fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb; // 物体自身颜色
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; // 环境光
				fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir)); // 漫反射光
				fixed3 halfDir = normalize(worldLightDir + worldViewDir); // 半向量
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss); // 镜面反射光
				return fixed4(ambient + diffuse + specular, 1.0);
			}
			
			ENDCG
		}
	}

	FallBack "Specular"
}

​ 运行效果同第 2 节。

​ 声明:本文转自【Unity3D】法线贴图和凹凸映射

标签:贴图,Unity3D,法线,切线,纹理,空间,tangentNormal,向量
From: https://www.cnblogs.com/zhyan8/p/17489693.html

相关文章

  • 【Unity3D】阴影原理及应用
    1阴影原理​光源照射到不透明物体上,会向该物体的后面投射阴影,如果阴影区域存在其他物体,这些物体不被光源照射的部分就需要渲染阴影。因此,我们可以将阴影的生成抽象出2个流程:物体投射阴影、物体接收阴影。1.1阴影相关开关​1)开启Light组件渲染阴影NoShadows......
  • 【Unity3D】魔方
    1需求实现​绘制魔方中基于OpenGLES实现了魔方的绘制,实现较复杂,本文基于Unity3D实现了2~10阶魔方的整体旋转和局部旋转。​本文完整代码资源见→基于Unity3D的2~10阶魔方实现。下载资源后,进入【Build/Windows】目录,打开【魔方.exe】文件即可体验产品。......
  • three.js 置换贴图 alpha贴图 的妙用 - 3D文字不引入字体文件
    实现将文字绘制到canvascanvas生成置换贴图alpha贴图将canvas转换成texture将texture贴到material修改shader将黑色背景区域去掉视频教程请移步b站canvas生成贴图classCanvas{canvas:HTMLCanvasElement=document.createElement("canvas");protectedctx:CanvasRen......
  • Unity3D:场景视图视图选项
    推荐:将NSDT场景编辑器加入你的3D工具链3D工具集:NSDT简石数字孪生“场景视图视图选项”工具栏您可以使用“场景视图视图选项”工具栏“叠加”来选择用于查看场景以及启用/禁用照明和音频的各种选项。这些控件仅在开发期间影响场景视图,对构建的游戏没有影响。绘制模式(Drawmo......
  • Unity3D学习笔记(二)创建地形和漫游
    七月3201212:35上午上一章粗略介绍了一下Unity游戏引擎的概念定义和界面功能,这次就来实践一下。我们的目标是没有蛀牙(误),目标是创建一个地形,上面有山脉和盆地,然后再放置一个人物,以第一人称的视角来漫游、观察我们所创建的世界。 在开始设计游戏之前我们需要先重新......
  • Unity3D学习笔记(一)界面介绍
    六月2020128:05下午从开始学习Unity到现在已经过去近三个月了,期间零零散散地在网上找教程、实例,感觉印象不够深刻。好多知识点不是被忽略了,就是被遗忘了。有幸在六一儿童节的时候发现了3DBuzz的基础视频教程,犹如介绍所言,几乎详细到每个菜单和按钮。为了部落(误),为......
  • Unity3D:Pick and select GameObjects
    推荐:将NSDT场景编辑器加入你的3D工具链3D工具集:NSDT简石数字孪生PickandselectGameObjects可以在Scene视图中或从Hierarchy窗口中选择一个游戏对象。也可以一次选择多个游戏对象。Unity会在Scene视图中突出显示选择的游戏对象及其子项。默认情况下,选择轮廓颜色为橙......
  • Unity3D:Scene 视图导航
    推荐:将NSDT场景编辑器加入你的3D工具链3D工具集:NSDT简石数字孪生Scene视图导航场景视图具有一组导航控件,可帮助您高效地四处移动:场景视图辅助图标移动、旋转和缩放工具居中工具场景视图辅助图标场景辅助图标将显示在场景视图中。这将显示场景视图摄像机的当前方向,并允......
  • [unity3d]屏幕坐标跟世界坐标的转换
    更多教程请访问:http://dingxiaowei.cn/ keepstudyveryday!写写今天的学习收获,今天学习到了平面坐标跟世界坐标的相互转换。效果:点击鼠标中键,创建一个小球,虽然看起来是屏面的,但实则是在三维空间里面创建的哦!代码挂在摄像机上:usingUnityEngine;usingSystem.Collections;publ......
  • Unity3D:Project窗口
    推荐:将NSDT场景编辑器加入你的3D工具链3D工具集:NSDT简石数字孪生Project窗口“项目”窗口显示与项目相关的所有文件,是您在应用程序中导航和查找资源和其他项目文件的主要方式。默认情况下,当您启动新项目时,此窗口处于打开状态。但是,如果找不到它,或者它已关闭,您可以通过“常规>......