【Unity Shaders】法线纹理(Normal Mapping)的实现细节


写在前面


写这篇的目的是为了总结我长期以来的混乱。虽然题目是“法线纹理的实现细节”,但其实我想讲的是如何在shader中编程正确使用法线进行光照计算。这里面最让人头大的就是各种矩阵运算和坐标系之间的转换,很容易因为坐标系错误而造成光照结果的错误。


我们将要讨论以下几个问题

  1. 为什么法线纹理通常都是偏蓝色的?

  2. 在Unity里,法线纹理是需要把“Texture Type”设置成“Normal Map”才能正确显示,为什么?

  3. 把“Texture Type”设置成“Normal Map”后,有一个复选框是“Create from grayscale”,这个是做什么用的?

  4. 为什么在Unity Shaders里,对法线纹理的采样要使用UnpackNormal函数?直接采样不行么?

当然,法线纹理映射是要在shaders中实现的。为此,我们讨论以下几种情况
  1. 在Surface Shader中,使用模型自带的法线如何计算光照;

  2. 在Surface Shader中,使用法线纹理如何计算光照;

  3. 在Vertex & Fragment Shader中,使用模型自带的法线如何计算光照;

  4. 在Vertex & Fragment Shader中,使用法线纹理如何计算光照;


要先说明的是,这篇文章有点长,有点绕,希望大家能耐心看完。我们先从最简单的地方开始。



Surface Shader中的法线


在Surface Shader中,无论是使用模型自带的法线或者使用法线纹理都是一件比较方便的事。原因是Unity封装了很多矩阵操作。我们会在这一节回答关于法线纹理的那几个问题。


使用模型自带的法线


法线实际上就是在光照模型中使用的,也就是Surface Shader的Lighting<Name>函数。Unity最常见的两种光照函数参数列表如下:


  1. half4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half atten); This is used in forward rendering path for light models that are not view direction dependent (e.g. diffuse).

    用于不依赖视角的光照模型计算,例如漫反射。

  2. half4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten); This is used in forward rendering path for light models that are view direction dependent.

    用于依赖视角的光照模型的计算,例如高光反射。

而想要访问法线的话就是使用 SurfaceOutput中的的 o.Normal即可。需要注意的是, 这些方便都是建立在Unity在背后为我们把normal、lightDir、viewDir转换到了同一坐标系下的基础上。这个坐标系 一般是指World Space


使用法线纹理


如果使用法线纹理的话,就需要我们在进入光照函数之前修改SurfaceOutput中的的o.Normal。我们会在void surf (Input INinout SurfaceOutput o函数里完成这件事。一般,代码都长下面这个样子:

		void surf (Input IN, inout SurfaceOutput o) 
		{
			//Get the normal data out of the normal map textures
			//using the UnpackNormal() function.
			float3 normal = UnpackNormal(tex2D(_NormalTex, IN.uv_NormalTex));
						
			//Apply the new normals to the lighting model
			o.Normal = normal;
		}

代码很简单。修改了法线后就可以按照上一节中的方法进行光照模型的计算。看起来和第一种方法好像一样,只是更改了o.Normal。但其实,Unity在背后做了很多。虽然我们使用了同样的Lighting<Name>函数,但其中normal、lightDir、viewDir所在的坐标系已经被Unity转换过了。这次,它们使用的坐标系是Tangent Space。如果你不知道它,没关系我们马上就会讲这个坐标系的细节。


但你有没有想过为什么要使用UnpackNormal这个函数。这就牵扯到我们的第一个问题:为什么法线纹理通常都是偏蓝色的?它里面到底是存储的什么呢?你会说,当然是法线啦!那么它的所在坐标系是什么呢?是World Space?Object Space?还是View Space?


实际上,我们通常见到的这种偏蓝色的法线纹理中,存储的是在Tangent Space中的顶点法线方向。那么,问题又来了,什么是Tangent Space(有时也叫object local coordinate system)?看到新名词不要怕,坐标系嘛,无非就是原点+三个坐标轴决定的一个相对空间嘛,我们只要搞清楚原点和三个坐标轴是什么就可以了。在Tangent Space中,坐标原点就是顶点的位置,其中z轴是该顶点本身的法线方向(N)。这样,另外两个坐标轴就是和该点相切的两条切线。这样的切线本来有无数条,但模型一般会给定该顶点的一个tangent,这个tangent方向一般是使用和纹理坐标方向相同的那条tangent(T)。而另一个坐标轴的方向(B)就可以通过normal和tangent的叉乘得到。上述过程可以如下图所示(来源:http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-13-normal-mapping/):


我们用一幅图(来源:《OpenGL 4 Sharding Language Cookbook》)来说明这样的关系:



也就是说,通常我们所见的法线纹理还是基于原法线信息构建的坐标系来构建出来的。那种偏蓝色的法线纹理其实就是存储了在每个顶点各自的Tangent Space中,法线的扰动方向。也就是说,如果一个顶点的法线方向不变,那么在它的Tangent Space中,新的normal值就是z轴方向,也就是说值为(0, 0, 1)。但这并不是法线纹理中存储的最终值,因为一个向量每个维度的取值范围在(-1, 1),而纹理每个通道的值范围在(0, 1),因此我们需要做一个映射,即pixel = (normal + 1) / 2。这样,之前的法线值(0, 0, 1)实际上对应了法线纹理中RGB的值为(0.5, 0.5, 1),而这个颜色也就是法线纹理中那大片的蓝色。这些蓝色实际上说明顶点的大部分法线是和模型本身法线一样的,不需要改变。总结一下就是,法线纹理的RGB通道存储了在每个顶点各自的Tangent Space中的法线方向的映射值。


我们现在来解决第四个问题:为什么在Unity Shaders里,对法线纹理的采样要使用UnpackNormal函数。我们先来看,在用OpenGL这种基本的着色语言时,我们是怎么做的。它的代码一般长成下面这样:

    // Lookup the normal from the normal map
    vec4 normal = texture( NormalMapTex, TexCoord );
    normal.xyz = normal.xyz * 2 - 1;

上述代码很简单,就是将法线纹理中的颜色值重新映射回正确的法线方向值。如果我们要在Unity中完成这样的功能,可以这样做:

	// Set "Texture Type" to "Texture"
	fixed4 normal = tex2D(_Bump, i.uv);
	norm.xyz = norm.xyz * 2 - 1

注意,这里并没有把法线纹理的“Texture Type”设置成“Normal Map”。上述方法是可以得到正确的法线方向的。


Unity为了某些原因把上述过程进行了封装,也就是说上述代码在Unity里可以这么做:把法线纹理的“Texture Type”设置成“Normal Map”,在代码中使用UnpackNormal函数得到法线方向。这其中的原因,我猜想一方面是为了方便它对不同平台做优化和调整,一方面是为了解析不同格式的法线纹理。


我们现在可以来回答第一个问题:为什么需要把法线纹理的“Texture Type”设置成“Normal Map”才能正确显示。这样的设置可以让Unity根据不同平台对纹理进行压缩,通过

  • 39
    点赞
  • 120
    收藏
    觉得还不错? 一键收藏
  • 42
    评论
Unity中,我们可以使用Shader实现各种效果,包括雾效模拟。雾效模拟可以让场景中的物体看起来更加立体,增强深度感。 实现雾效模拟的Shader需要使用到Unity内置的全局变量和函数,具体步骤如下: 1. 在Shader中添加一个属性,表示雾的颜色。 ``` Properties { _FogColor ("Fog Color", Color) = (1, 1, 1, 1) } ``` 2. 设置着色器的渲染模式为透明。 ``` Tags { "Queue"="Transparent" "RenderType"="Transparent" } ``` 3. 在Shader中添加一个片元着色器函数,并在该函数中计算出雾的强度。 ``` fixed4 frag (v2f i) : SV_Target { // 计算相机到片元的距离 float depth = UNITY_ACCESS_DEPTH(i.uv); float distance = LinearEyeDepth(depth); // 计算雾的强度 float fogDensity = distance * _FogDensity; float fogFactor = 1.0 - exp(-fogDensity * fogDensity); // 计算最终颜色 fixed4 col = tex2D(_MainTex, i.uv) * i.color; return lerp(col, _FogColor, fogFactor); } ``` 在上面的代码中,我们首先计算相机到片元的距离,然后根据距离和雾的密度计算出雾的强度,最后使用lerp函数将雾的颜色和物体的颜色进行插值,得到最终的颜色。 4. 在Unity中创建一个材质,并将上述Shader应用到该材质上。 5. 将该材质应用到场景中的物体上,即可看到雾效果的模拟。 以上就是在Unity实现雾效模拟的基本步骤。需要注意的是,雾效模拟需要耗费一定的计算资源,因此在实际应用中需要根据场景的复杂度和设备性能进行调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 42
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值