unity3d 法线贴图

2019-11-21 21:36:03 DY_1024 阅读数 143
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

制作法线贴图还是得美术提供你几张基础的法线贴图之后,然后再去制作。

现在我们手里有两张贴图,一张是正常贴图,一张是法线贴图;

1、选择美术提供的基础的法线贴图:

Tip:其中Filtering选项有的地方说是应该选择Sharp,但是我的默认就是Sharp。

2、选项设置完成之后,点击右下角的“Apply”就会看见贴图变成这样:

此时我们的法线法线贴图就做好了。

 

代码里面怎么改变我们的法线贴图呢?其实实质就是获取材质然后更改材质上面的贴图。

我在场景里面放置了一个模型,这个模型可以放置两个材质:

此时Material就是一个Size为2的数组,我们可以在C#的时候给Material赋值一个Size为2的数组。

using System.Collections;  
using System.Collections.Generic;  
using UnityEngine;  
  
public class ChangeMaterial : MonoBehaviour  
{  
  // 定义两个公共的变量,这样就可以在Unity里面将制作好的两个材质拖过来赋值
    public Material diffusemap;  
    public Material normalmap;  
  
private bool isnormal = false;  
// 用来获取模型的meshrenderer组件
    private MeshRenderer meshrender;  
    // Start is called before the first frame update  
    void Start()  
{  
  // 获取模型的meshrenderer组件
        this.meshrender = this.GetComponent<MeshRenderer>();  
   // 将拖过来的两个材质,组成数组,赋值给meshrenderer组件
        meshrender.materials = new Material[2] { this.diffusemap, this.diffusemap };  
    }  
  
    // Update is called once per frame  
    void Update()  
{  
  // 使用鼠标左键来改变当前材质
        if(Input.GetMouseButtonDown(0))  
        {  
            if (isnormal) {  
                meshrender.materials = new Material[2] { this.diffusemap, this.diffusemap };  
            }  
            else {  
                meshrender.materials = new Material[2] { this.normalmap, this.normalmap };  
            }  
  
            isnormal = !isnormal;  
        }  
    }  
}  
2018-08-10 18:10:40 qq_38061677 阅读数 2396
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

法线贴图

法线贴图是比较常用的一种贴图,作用是使一些面数比较少的模型的纹理更加的精致和逼真。这样在大幅度减少了运行的性能消耗,也能达到比较好的游戏画面。unity3d中有比较简单的法线贴图,就是看起来与3D效果无异的2D贴图。如果做3D模型的话,就会浪费显示芯片,使游戏性能下降,便会用法线贴图,既不影响玩家体验游戏又不影响游戏性能.
下面让我们一制作一张简单逼真的纹理贴图,让它作用在一个游戏对象上。

效果图

这里写图片描述

第一首先在Project栏里面创建一个TextPicture文件夹来放置图片,我在网上找的一张砖头的图片,鼠标拖入项目Assets/Texture文件夹中。多复制一张,修改TextType属性

这里写图片描述

TextType是纹理的类型
Default:纹理
Normal map:法线贴图
Editor GUI and Legacy GUI:编辑器GUI和传统GUI。Sprite(2D and UI):精灵,用于2D对象和UGUI贴图。
Cursor:自定义光标。
Cookie:场景光的Cookie。
Lightmap:光照贴图,将贴图编码成特定的格式,并对纹理数据进行后处理。
SingleChannel:单通道。
选择Normal Map
Create from Grayscale勾上
Create from Grayscale:从灰度高度图(Heightmap)创建。
有两个属性可以调节
Bumpiness:崎岖度。
Filtering:滤波算法。
1.Smooth:平滑,标准前向差分算法。
2.Sharp:尖锐,Sobel滤波器。
崎岖度根据需要,自由调节,滤波算法,我们选择尖锐Sharp
这里写图片描述
选择完后,记得点击Apply。

第二步,制作Metrial。创建一个Metrial文件夹,在里面创建一个Metrial。点击Project的Create来创建

这里写图片描述

Material里面最重要的是Shader,Shader(着色器):Shader负责将输入的Mesh(网格)以指定的方式和输入的贴图或者颜色等组合作用,然后输出。绘图单元可以依据这个输出来将图像绘制到屏幕上。

材质(Meterial):将输入的贴图或者颜色,加上对应的Shader,以及对Shader的特定的参数设置,将这些打包在一起就是一个材质了。我们便可以将材质赋予合适的renderer(渲染器)来进行渲染。
单击Meterial的Inspecter面板,在Albedo属性上选择多复制的那张正常图片,然后在Normal Map上选择制作好的法线贴图。一个带有法线贴图的Meteral做好了,重命名wall.
这里写图片描述

第三步,新建一个cude,把我们建好的材质包鼠标托上去就建好了。

这里写图片描述
这里写图片描述

光线烘培

当我们进行游戏时,有些场面避免不了使用大量灯光渲染。大量的灯光渲染对于计算机来说无疑是个负担,为了尽量节省计算机的显卡资源,对于一些静态的不动的物体,可以采用烘培的方法,把灯光的效果,制作成游戏物体纹理的一部分,这样即使没有灯光的渲染。该游戏物品也会呈现一样的视觉效果。烘培非常的常见,请注意需要烘培的的游戏物品一定时静止的
下面做一个实例

第一步 在场景中创键一个cude和一个plane,为了效果更佳,把直射光源的强度稍微调小一点。

这里写图片描述

第二步、把所有要渲染的游戏对象全选,在Inspecto面板里面单击Static选项,选择Lightmap static。

这里写图片描述
创建一个点光源做为要渲染的的光源,为了着重看出效果。我把光源的颜色改成紫色。在实际的需求中,大家可以根据自己的需求进行改变。将需要烘焙的光源的Mode模式改为Baked
这里写图片描述

第三步、选择菜单栏的window选项里面的Lighting的Settings面板,在Object Maps选项中讲勾选的Auto Generate按钮去掉后,单击Generate Lighting按钮。场景就在烘焙中,右下角会出现进度条,当进度条完成后。烘培完成。完成烘焙后,即使删掉了光源,游戏物品依然具有之前的纹理。

这里写图片描述这里写图片描述
这里写图片描述

最后完成。神奇的一幕来了,把目标光源删除掉后,纹理依旧
这里写图片描述

2017-12-18 09:19:04 wangkun1340378 阅读数 7242
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

unity3D 法线贴图的制作与使用

工具:unity3D 5.4.0


示例:

 首先,获取一张图片用以制作法线贴图


随后,拖入unity3D的project目录


选中图片,在右侧的inspector目录中,texture type选择normal map,filtering选择sharp,并根据需要设置bumpiness的数值,本例中为0.3


随后,点击apply即可生成法线贴图。

利用unity3D搭建一个3d模型,如图所示



最后选中所建立的3d模型,在右侧的inspector目录中,将normal map设置为刚才制作的法线贴图


完成法线贴图后的结果如图所示


2016-12-20 23:53:44 puppet_master 阅读数 29061
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

简介


以前经常听说“模型不好看啊,怎么办啊?”答曰“加法线”,”做了个高模,准备烘一下法线贴图”,“有的美术特别屌,直接画法线贴图”.....法线贴图到底是个什么鬼,当年天真的我真的被这个图形学的奇淫杂技忽悠了,然而毕竟本人还算有点刨根问底的精神,决定研究一下法线贴图的原理以及Unity下的实现。本人才疏学浅,如有错误,欢迎指正。


法线贴图是目前游戏开发中最常见的贴图之一。我们知道,一般情况下,模型面数越高,可以表现的细节越多,效果也越好。但是,由于面数多了,顶点数多了,计算量也就上去了,效果永远是和性能成反比的。怎么样用尽可能简单模型来做出更好的效果就成了大家研究的方向之一。纹理映射是最早的一种,通过纹理直接贴在模型表面,提供了一些细节,但是普通的纹理贴图只是影响最终像素阶段输出的颜色值,不能让模型有一些凹凸之类的细节表现。而法线贴图就是为了解决上面的问题,给我们提供了通过低面数模型来模拟高面数模型的效果,增加细节层次感,效果与高模相差不多,但是大大降低了模型的面数。


法线贴图原理


要模拟一个圆球,要想越平滑,就需要更多的面数,否则会很容易地发现面和面之间的明显边界。最早时的GPU是没有fragement编程能力的,也就是说在这种情况下,在计算时需要逐顶点计算光照,然后每个像素的颜色在各个顶点的颜色之间插值,也就是高洛德着色,这种情况下,面数决定一切效果,没有什么好办法。而当像素着色器出现之后,我们可以逐像素来计算光照效果,这时候,在计算每个像素的光照时,会计算这个像素所在的面的法向量,而这个面的法向量也是由这个面周围的顶点法线(也就是我们之前vertex shader中出现的normal)插值得来的,当然,如果面数很低,那么效果也好不到哪里去。但是,逐像素计算光照时,我们每一个像素都会根据该点的法向量来计算最终该点的光照结果,那么,我们如果能够改变这个法线的方向,不是就可以改变这个点的光照结果了呢!那么,把纹理采样的思想用在这里,我们直接用一张图来存储法线(或者法线偏移值,见下文),逐像素计算时,在采样diffuse贴图的时候,再采样一张法线的贴图,就可以修改法线了,进而修改最终的效果。

为什么法线贴图会让我们感觉有凹凸感呢?看下面一张图,在现实世界中,你要相信你的眼睛,眼见为实还有点道理,在计算机世界中,一切以忽悠你为目的。在平面的情况下,我们感觉物体是凹陷还是凸起,很大一部分取决于这个面的亮度,像下面这张图,有了这种亮度的对比,我们就很容易感觉这个按钮有周围的一圈凸起。


如果还是没理解,再看一套图片,同样一张图片,旋转180度后的结果完全相反。不信可以去截图放到MSPaint里面转一下试试,反正我是试了....

既然一个面的光照条件(亮度)的改变,就可以让我们感觉这个面有凹凸感,那么上面说的,通过改变法线来改变面上某点的光照条件,进而忽悠观察者,让他们感觉这个面有凹凸感的方法就行得通了。

假如下面是我们的低面数模型,上面是我们的高面数模型,上面的模型在计算光照时,由于面数多,每个面的法线方向不同,所以各个面的光照计算结果都不同,就有凹凸的感觉了,而下面的低模,只有一个面,整个面的光照条件都是一致的,就没有凹凸的感觉了。我们如果把上面的高模的法线信息保存下来,类似纹理贴图那样,存在一张图里,再给低模使用,低模就可以有跟高模一样的法线,进而在计算光照时达到和高模类似的效果,这也就是常说的烘法线的原理。


凹凸贴图(Bump Map)


既然说了要研究法线贴图,所以肯定要从老一辈的开始,首先来看一下凹凸贴图(Bump Map)。Bump Map是最早的法线贴图实现方式,这也是制作上最容易的一种模式,可以直接通过一张灰度图,默认为黑色,越凸起的地方颜色越亮,这种就是可以直接在PhotoShop中画的法线,但是这种法线贴图的原理理解起来比较难,我只说一下我的理解,然后附上unity中的shader实现。这种技术现在貌似已经过时了,但是思想还是流传下来了,而且这种画灰度图,或者通过灰度图生成法线贴图的方式现在仍然在使用,Unity就支持这种直接通过灰度图生成法线贴图。

首先,通过灰度图来表现凹凸,那么,我们怎样判断一个点处在凹凸的边缘呢?答案是通过斜率,比如我要对(x,y)进行采样,怎样求这一点的斜率呢,学过数学的都知道,我们可以通过两点确定一条直线,进而求出这条直线的斜率。那么我们就可以对(x-1,y)和(x+1,y)两点进行采样,竖向也是一样,通过(x,y-1)和(x,y+1)进行采样,那么,我们就可以获得这一点上灰度值的变化,如果灰度值不变,说明该点不在边缘,如果灰度值有改变,那么说明该点在边缘,那么我们就可以根据这个斜率值来修改法线,进而修改光照结果。

我又掏出了我十分不熟练的PhotoShop,画了一张传说中的Bump Map,恩,感觉还不错,目前RGB通道都有信息,反正只是实验,和shader对应就好了:

Bump Map类型的shader如下(仅仅是实验基于灰度的Bump,完全不实用....)
//Bump Map
//by:puppet_master
//2016.12.13
Shader "ApcShader/BumpMap"
{
	//属性
	Properties{
		_Diffuse("Diffuse", Color) = (1,1,1,1)
		_MainTex("Base 2D", 2D) = "white"{}
		_BumpMap("Bump Map", 2D) = "black"{}
		_BumpScale ("Bump Scale", Range(0.1, 30.0)) = 10.0
	}

	//子着色器	
	SubShader
	{
		Pass
		{
			//定义Tags
			Tags{ "RenderType" = "Opaque" }

			CGPROGRAM
			//引入头文件
			#include "Lighting.cginc"
			//定义Properties中的变量
			fixed4 _Diffuse;
			sampler2D _MainTex;
			//使用了TRANSFROM_TEX宏就需要定义XXX_ST
			float4 _MainTex_ST;
			sampler2D _BumpMap;
			float4 _BumpMap_TexelSize;
			float _BumpScale;

			//定义结构体:应用阶段到vertex shader阶段的数据
			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};
			//定义结构体:vertex shader阶段输出的内容
			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				//转化纹理坐标
				float2 uv : TEXCOORD1;
			};

			//定义顶点shader
			v2f vert(a2v v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				//把法线转化到世界空间
				o.worldNormal = mul(v.normal, (float3x3)_World2Object);
				//通过TRANSFORM_TEX宏转化纹理坐标,主要处理了Offset和Tiling的改变,默认时等同于o.uv = v.texcoord.xy;
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				return o;
			}

			//定义片元shader
			fixed4 frag(v2f i) : SV_Target
			{
				//unity自身的diffuse也是带了环境光,这里我们也增加一下环境光
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;
				//归一化法线,即使在vert归一化也不行,从vert到frag阶段有差值处理,传入的法线方向并不是vertex shader直接传出的
				fixed3 worldNormal1 = normalize(i.worldNormal);
				//采样bump贴图,需要知道该点的斜率,xy方向分别求,所以对于一个点需要采样四次
				fixed bumpValueU = tex2D(_BumpMap, i.uv + fixed2(-1.0 * _BumpMap_TexelSize.x, 0)).r - tex2D(_BumpMap, i.uv + fixed2(1.0 * _BumpMap_TexelSize.x, 0)).r;
				fixed bumpValueV = tex2D(_BumpMap, i.uv + fixed2(0, -1.0 * _BumpMap_TexelSize.y)).r - tex2D(_BumpMap, i.uv + fixed2(0, 1.0 * _BumpMap_TexelSize.y)).r;
				//用上面的斜率来修改法线的偏移值
				fixed3 worldNormal = fixed3(worldNormal1.x * bumpValueU * _BumpScale, worldNormal1.y * bumpValueV * _BumpScale, worldNormal1.z);

				//把光照方向归一化
				fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
				//根据半兰伯特模型计算像素的光照信息
				fixed3 lambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
				//最终输出颜色为lambert光强*材质diffuse颜色*光颜色
				fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;
				//进行纹理采样
				fixed4 color = tex2D(_MainTex, i.uv);
				return fixed4(diffuse * color.rgb, 1.0);
			}

			//使用vert函数和frag函数
			#pragma vertex vert
			#pragma fragment frag	

			ENDCG
		}

	}
		//前面的Shader失效的话,使用默认的Diffuse
		FallBack "Diffuse"
}
效果如下:
这个过程还是很有意思的,Unity为我们封装了太多东西,尤其是surface shader,只需要一句unpacknormal,然后把输出赋给o.normal就ok了,我们基本不需要做什么,但是底层的实现对于学习来说还是很必要的。

法线贴图(Normal Map)


随着GPU的发展,Geforce3的出现,带来了真正的Normal Mapping技术,也叫作Dot3 bump mapping。这种Normal Map就是我们现在在使用的法线贴图技术。与之前通过灰度表现界面的凹凸程度,进而修改法线的方式完全不同,这种Normal Map直接将法线存储到了法线贴图中,也就是说,我们从法线贴图读取的法线直接就可以使用了,而不是需要像上面那样,再通过灰度渐变值来修改法线。这种法线对于制作来说,没有灰度图那样直白,但是却是真正的法线贴图技术,所谓烘焙法线,烘焙的就是这个。

虽然灰度图不会直接被用于实时计算法线了,但是在离线工具中却提供了直接通过灰度图生成法线的功能。Unity中就有这种功能:

我们把之前画的那张灰度图直接通过这种方式改成法线贴图,从法线贴图中我们就直接可以看到凹凸的效果了。在Unity里实现法线贴图的shader之前,首先看几个问题,也是困扰了我一段时间的几个问题。

法线贴图是怎样存储的


既然法线贴图中存储的是法线的方向,也就是说是一个Vector3类型的变量,刚好和图片的RGB格式不谋而合。但是向量毕竟要灵活得多,我们正常的RGBA贴图,一个通道是8位,可以表示的大小在(0,255),那么反过来除一下,贴图中可以存储的向量的精度就是0.0039,也就是说并不是真正意义上的浮点类型,精度要小得多,不过对于一般情况下,这种精度也足够了。再一个问题,就是向量是有方向滴,而贴图中只能存储的都是正数,所以,还需要一个映射的过程。映射在图形学中真是很多见呢,比如计算半兰伯特光照时,就通过把(0,1)的光照区间转化到了(0.5,1)提高了光的亮度,使效果更好。在法线贴图中,可以用0代表向量中的-1,用255代表向量中的1,不过,在shader中,贴图的颜色一般也是(0,1)区间,所以,我们在计算时只需要把从法线贴图中采样得到的法线值进行映射,将其从(0,1)区间转化到(-1,1)区间。

这个步骤,Unity已经为我们完成了,我们在计算法线的时候,只需要调用UnpackNormal这个函数就可以实现区间的重新映射。从UnityCG.cginc中可以看到UnpackNormal这个函数的实现:
inline fixed3 UnpackNormalDXT5nm (fixed4 packednormal)
{
	fixed3 normal;
	normal.xy = packednormal.wy * 2 - 1;
	normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
	return normal;
}

inline fixed3 UnpackNormal(fixed4 packednormal)
{
#if defined(UNITY_NO_DXT5nm)
	return packednormal.xyz * 2 - 1;
#else
	return UnpackNormalDXT5nm(packednormal);
#endif
}
做法很简单,乘2 减1大法好,转化区间没烦恼(什么鬼....)
这里,我们看到了两个UnpackNormal的函数,下面的就是我们所说的直接转化区间。而上面的那个函数,看定义来说,是为了专门解出DXT5nm格式的normal map,这种类型的normal map,只用存储法向量中的两个通道,然后解开的时候,需要计算一下,重新算出另一个向量方向。这样可以实现的原理在于,存储的向量是单位向量,长度一定的情况下,就可以通过sqrt(1 - x^2 - y^2)来求得,如下图:
不过这是一种时间换空间的做法,以牺牲时间的代价,换来更好的压缩比以及压缩后的效果。关于DXT5nm,附上一篇参考文章:Normal Map的dds压缩

为什么法线贴图存储在切线空间


既然知道了法线可以存储在贴图中,我们就再来看一下,为什么法线贴图中一般都存储的是切线空间,为什么不存储在世界空间或者模型空间。首先看一下世界空间,如果我们的法线贴图存储的世界空间的法线信息,我们可以直接解出法线的值,在世界空间进行计算,是最直接并且计算效率最高的做法,但是世界空间的法线贴图就跟当前环境之间耦合过大了,比如同样的两个模型,仅仅是旋转方向不同,也需要两张法线贴图,这很明显是多余的,于是就有人想出了基于模型空间的法线,基于模型空间,在计算时,把模型空间的法线转换到世界空间,虽然多了一步操作,但是同一个模型可以共用法线,不用考虑旋转等问题。但是,人们感觉模型空间的法线贴图跟模型的耦合度还是高,那就继续解耦吧,于是基于切线空间的法线贴图就诞生了。下图为模型空间与切线空间法线。



所谓的切线空间,跟那些比较常见的坐标系,比如世界坐标,模型坐标一样,也是一个坐标系,用三个基向量就可以表示。我们用模型上的一个点来看,这个点的有一个法线的方向,也就是这个点所在的面的法线的方向N,这个方向是确定的,我们可以用它作为Z轴。而剩下的两个轴,刚好就在这个面上,互相垂直,但是这两个轴的可选种类就多了,因为在这个面上任意两个向量都可以表示这个面。目前最常用的方式是以该点的uv二维坐标系表达该点的切线(tangent)和该点的次法线(binormal)所构成的切平面。它的法线既处处都垂直于它的表面。我们用展uv的方式,将纹理展开摊平,那么所有的法线就都垂直于这个纹理平面,法线就是z轴,而uv set,准确地说是该点uv朝着下一个顶点uv的方向向量分别作为tangent和binormal轴,也就是x,y轴。但是这样做有一个弊端,就是x轴和y轴之间不互相垂直,计算Tangent空间的公式如下:

T = normalize(dx/du, dy/du, dz/du)
N = T × normalize(dx/dv, dy/dv, dz/dv)
B = N × T

很遗憾我们在在Unity里面看不到全部源代码,不过从shader的定义中可以看到B的求解以及TBN矩阵的构建过程:
// Declares 3x3 matrix 'rotation', filled with tangent space basis
#define TANGENT_SPACE_ROTATION \
	float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w; \
	float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )
float3x3是行向量构建,可以参照这里,然后我们就可以通过mul(rotation,v)把需要的向量从模型空间转化到tangent空间。不过大部分内容Unity已经帮我们做好了,主要是TBN空间的创建,如果需要自己写渲染器的话,这个是一个比较麻烦的过程,也有类似3DMax中导出顶点tangent值中的做法,直接在导出的时候将tangent空间信息导出,存储在顶点中。


最后总结一下:tangent space下,其实跟我们上一节计算的斜率很像,我们计算斜率基本也是tangent值。而这里T(x轴)使用normalize(dx/du, dy/du, dz/du),相当于计算了模型空间下x,y,z值随着纹理u坐标方向的斜率,换句话说,切线空间反映了模型空间坐标xyz随着纹理坐标uv的变化率(坡度),这也正是normal map中要存储的信息,所以normal map中的内容正好可以使用切线空间进行存储。

为什么法线贴图都是蓝色的


既然我们知道了法线贴图中存储的是切线空间的法线。而法线贴图所对应的表面,绝大部分的位置肯定是平滑的,只有需要凹凸变化的地方才会有变化,那么大部分地方的法线方向不变,也就是在切线空间的(0,0,1),这个值按照上面介绍的映射关系,从(-1,1)区间变换到(0,1)区间:(0*0.5+0.5,0*0.5+0.5,1*0.5+0.5)= (0.5,0.5,1),再转化为颜色的(0,255)区间,最终就变成了(127,127,255)。好了,打开photoshop,看一下这个颜色值是什么:
法线一般就是这个颜色嘛!那么,其他的地方,如果有凹凸感,就需要调整法线的方向,那么颜色就不一样了。

Unity下法线贴图Shader实现


解决了上面几个问题之后,我们就可以看一下Unity Shader中实现法线贴图的方式。光照模型仍然采用之前的半兰伯特光照,vertex fragemnt shader实现(surface版本的就两句话,也就不写了):
//Bump Map
//by:puppet_master
//2016.12.14
Shader "ApcShader/NormalMap"
{
	//属性
	Properties{
		_Diffuse("Diffuse", Color) = (1,1,1,1)
		_MainTex("Base 2D", 2D) = "white"{}
		_BumpMap("Bump Map", 2D) = "bump"{}
		_BumpScale ("Bump Scale", Range(0.1, 30.0)) = 10.0
	}

	//子着色器	
	SubShader
	{
		Pass
		{
			//定义Tags
			Tags{ "RenderType" = "Opaque" }

			CGPROGRAM
			//引入头文件
			#include "Lighting.cginc"
			//定义Properties中的变量
			fixed4 _Diffuse;
			sampler2D _MainTex;
			//使用了TRANSFROM_TEX宏就需要定义XXX_ST
			float4 _MainTex_ST;
			sampler2D _BumpMap;
			float _BumpScale;

			//定义结构体:vertex shader阶段输出的内容
			struct v2f
			{
				float4 pos : SV_POSITION;
				//转化纹理坐标
				float2 uv : TEXCOORD0;
				//tangent空间的光线方向
				float3 lightDir : TEXCOORD1;
			};

			//定义顶点shader
			v2f vert(appdata_tan v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				//这个宏为我们定义好了模型空间到切线空间的转换矩阵rotation,注意后面有个;
				TANGENT_SPACE_ROTATION;
				//ObjectSpaceLightDir可以把光线方向转化到模型空间,然后通过rotation再转化到切线空间
				o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex));
				//通过TRANSFORM_TEX宏转化纹理坐标,主要处理了Offset和Tiling的改变,默认时等同于o.uv = v.texcoord.xy;
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				return o;
			}

			//定义片元shader
			fixed4 frag(v2f i) : SV_Target
			{
				//unity自身的diffuse也是带了环境光,这里我们也增加一下环境光
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;
				//直接解出切线空间法线
				float3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uv));
				//normalize一下切线空间的光照方向
				float3 tangentLight = normalize(i.lightDir);
				//根据半兰伯特模型计算像素的光照信息
				fixed3 lambert = 0.5 * dot(tangentNormal, tangentLight) + 0.5;
				//最终输出颜色为lambert光强*材质diffuse颜色*光颜色
				fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;
				//进行纹理采样
				fixed4 color = tex2D(_MainTex, i.uv);
				return fixed4(diffuse * color.rgb, 1.0);
			}

			//使用vert函数和frag函数
			#pragma vertex vert
			#pragma fragment frag	

			ENDCG
		}

	}
		//前面的Shader失效的话,使用默认的Diffuse
		FallBack "Diffuse"
}
结果:


总结

本篇文章简单探究了一下bump map以及normal map的原理以及在Unity中的实现。法线贴图可以很好地在低模上模拟高模的效果,虽然多采样了一次贴图,但是能模拟出数倍于模型本身面数的效果,极大地提升了实时渲染的效果。虽然法线贴图也有一些弊端,因为法线贴图只是给人造成一种凹凸的假象,所以在视角与物体平行时,看到的物体表面仍然是平的。并且还会有一些穿帮的现象,不过毕竟瑕不掩瑜,法线贴图仍然是目前渲染中最常使用的技术之一。为了解决上面的问题,一些更加高级的贴图技术,如视差贴图和位移贴图就诞生了。之后再研究这两种更加高级一点的贴图技术,本篇到此为止。上面给出了一些关于tangent空间求解的参考链接。最后再附上一些关于法线贴图原理的参考。


2017-12-18 10:22:51 qq_26900671 阅读数 12264
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

我们都知道,一个三维场景的画面的好坏,百分之四十取决于模型,百分之六十取决于贴图,可见贴图在画面中所占的重要性。在这里我将列举一些贴图,并且初步阐述其概念,理解原理的基础上制作贴图,也就顺手多了。

我在这里主要列举几种UNITY3D中常用的贴图,与大家分享,希望对大家有帮助




 
首先不得不说的是漫反射贴图:
漫反射贴图diffuse map 


 
漫反射贴图在游戏中表现出物体表面的反射和表面颜色。换句话说,它可以表现出物体被光照射到而显出的颜色和强度。我们通过颜色和明暗来绘制一幅漫反射贴图,在这张贴图中,墙的砖缝中因为吸收了比较多的光线,所以比较暗,而墙砖的表面因为反射比较强,所以吸收的光线比较少。上面的这张图可以看出砖块本身是灰色的,而砖块之间的裂缝几乎是黑色的。
刨去那些杂糅的东西,我们只谈明显的,漫反射贴图表现了什么? 列举一下,物体的固有色以及纹理,贴图上的光影。前面的固有色和纹理我们很容易理解,至于后面的光影,我们再绘制漫反射贴图的时候需要区别对待,比如我们做一堵墙,每一块砖都是用模型做出来的,那么我们就没有必要绘制砖缝,因为这个可以通过打灯光来实现。可是我们如果用模型只做了一面墙,上面的砖块是用贴图来实现,那么就得绘制出砖缝了。从美术的角度,砖缝出了事一条单独的材质带外,还有就是砖缝也是承接投影的,所以在漫反射图上,绘制出投影也是很有必要的,如下图:


  
没有什么物体能够反射出跟照到它身上相同强度的光。因此,让你的漫反射贴图暗一些是一个不错的想法。通常,光滑的面只有很少的光会散射,所以你的漫反射贴图可以亮一些。
漫反射贴图应用到材质中去是直接通过DiffuseMap的。再命名规范上它通常是再文件的末尾加上“_d”来标记它是漫反射贴图。


  
凹凸贴图Bump maps
凸凹贴图可以给贴图增加立体感。它其实并不能改变模型的形状,而是通过影响模型表面的影子来达到凸凹效果的。再游戏中有两种不同类型的凸凹贴图,法线贴图(normalmap)和高度贴图(highmap)。
Normal maps法线贴图


  
法线贴图定义了一个表面的倾斜度或者法线。换一种说法,他们改变了我们所看到的表面的倾斜度。


  
法线贴图把空间坐标的参数(X,Y,Z)记录在像素中(R,G,B),上面的范例图就是这个意思。
有两种制作法线贴图的方法:
1.从三维的模型渲染出一张法线贴图 (用高模跟低模重叠在一起,把高模上的细节烘焙到低模的UV上,这里需要低模有一个不能重叠的UV)
2.转换一张高度贴图成为一个法线贴图。(是用NVIDIA的PS插件来转换一张图成为法线贴图)
Height maps高度贴图


  
什么是HeightMap呢?所谓高度图实际上就是一个2D数组。创建地形为什么需要高度图呢?可以这样考虑,地形实际上就是一系列高度不同的网格而已,这样数组中每个元素的索引值刚好可以用来定位不同的网格(x,y),而所储存的值就是网格的高度(z)。
我们在这里叙述高度图,其实也是为了更好的绘制法线贴图,很多情况下我们的法线贴图只能在已有的漫反射贴图作为素材进行绘制,这样就是需要由一个HeightMap转换成法线贴图的一个过程,明白了这个原理,做起来也就可以更好的驾驭其效果。
高度贴图是一种黑白的图像,它通过像素来定义模型表面的高度。越亮的地方它的高度就越高,画面越白的地方越高,越黑的地方越低,灰色的在中间,从而表现不同的地形。
当然在UNITY中也是有HightMap出现的,比如在Terrain菜单中,就有导入和导出HightMap的命令。
高度贴图通常是在图形处理软件中绘制的。他们通常没有必要渲染这些,再DOOM3游戏中高度贴图是被转换成法线贴图来使用的。使用高度贴图仅仅是为了适应简单的工作流程。高度贴图通常通过“Heightmap”函数来调用到3D软件中去的,我们通常再文件名后面加一个"_h"来标示它。
Normal maps vs. height maps
法线贴图和高度贴图一般来说,Normal Map来自于Height Map。具体生成的方法如下:把Height Map的每个像素和它上面的一个像素相减,得到一个高度差,作为该点法线的x值;把Height Map的每个像素和它右边的一个像素相减,得到一个高度差,作为该点法线的y值;取1作为该点法线的z值。推导过程如下:x方向,每个像素和它下面的一个像素相减,得到向量<1, 0, hb - ha>,其中ha是该像素的高度值,hb是下一行的高度值;y方向,每个像素和它左边的一个像素相减,得到向量<0, 1, hc - ha>,其中ha是该像素的高度值,hb是左一列的高度值;两个向量Cross,得到简单来说,就是取两个方向的切线向量,对它们做Cross得到该点的法线向量。
还有另外一种做法,是根据每个象素四边的点计算,而该点象素本身不参与计算。没有试过,不知道哪种好一些。而且我觉得这种计算只适合于单块的HeightMap、NormalMap,像是DOOM3中的NormalMap就无法由HeightMap计算出来了。所以最好还是在美工建模的时候同时生成NormalMap和HeightMap而不是利用HeightMap生成NormalMap。
DOOM3游戏引擎可以把法线贴图和高度贴图合成在一张凸凹贴图上。
通常我们绘制一张具有足够细节的高度贴图要比建立一个足够细节的模型然后渲染成相应的法线贴图要实际的多。
法线和高度的凸凹贴图可以通过Addnormals函数来合并到一种材质中。
毫无疑问,高度贴图大多数游戏引擎中出现的不多。他们只是给电脑一种方法来计算曲面法线当使用动态灯光的时候。
这说明实际上,一张高度贴图被转换成一张法线贴图,以此可以计算出相邻两块不同高度的位置之间的倾斜面。高度贴图永远不能像法线贴图这样具有足够的细节,这是被肯定的。
很明显只有灰度的高度贴图并不能很好的表现应该有的细节,因为它是黑白的,RGB颜色就会遭到浪费,并且因此你只能只用256层级的强度。
相比较来言法线贴图的每一个图像通道都可以利用到,显而易见,法线贴图能够更好的来表现凸凹。


  
Specular maps高光贴图 


  
什么是高光贴图?
高光贴图是用来表现当光线照射到模型表面时,其表面属性的.(如金属和皮肤、布、塑料反射不同量的光)从而区分不同材质.
高光贴图再引擎中表现镜面反射和物体表面的高光颜色。
材质的反光程度就越强。(强弱度度是指,如果将这张Specularmap去色成为黑白图,图上越偏向RGB0,0,0,的部分高光越弱,越偏向RGB255,255,255的部分高光越强.)
我们建立高光贴图的时候,我们使用solid value来表现普通表面的反射,而暗的地方则会给人一种侵蚀风化的反射效果。(你头脑中要有很清晰的物件不同材质之间高光强弱的关系:高光最强的是那个部分,最弱的是那个部分,处在中间级别的是哪些部分.一般来说:金属的高光>塑料>木头>皮肤>不料,但是这个只是一个大致的分类,不要把它作为高光的指导.有时,你处理的物件可能是如上图一样,绝大部分都是同一类型材质的,比如布料,这时你也要小心的去分辨不同材料之间的高光强度的区别.切记,在这个阶段一定要保持清晰的头脑,不要急着去添加那些细节.在大的强弱关系还没有决定之前,就去添加那些细节会影响你的判断,而最后得到一张层次不清晰很“花”的高光.很多时候,我们容易范这样的毛病,就是将物件的高光处理的太过单一.)
上面的贴图有个问题,砖的表面与砖缝相比将会有比较少的反光,但是砖缝的位置其实应该几乎是没有反光的。(确定好整体高光的强弱之后,就开始在高光上叠加细节:比如金属划痕,金属倒角高光,锈渍周围的裸金属亮点,油渍,灰尘等.这时,你会发现,如果你在Diffusemap的绘制过程中,保留了纹理,划痕或以上提到过的细节的图层,你只需要将Diffusemap中的相应图层拖曳到Specularmap中,然后根据这些细节应该反映出来的高光强度调节就可以了.So,良好的图层管理习惯是非常必要的.)
颜色再高光贴图中将会用来定义高光的颜色,组成砖的材料应该是一些沙子,他们将会反射出一些微笑的具有质感的光,这些在上面的例子中已经展示了出来。(为了丰富高光贴图,我们有很多方法:做局部高光的细微变化,添加纹理(这个纹理要和材质本身的纹理区分开),叠加彩色图层(谨慎用))
高光贴图是通过Specularmap函数调用到引擎中的,通常我们再贴图的后面加一个"_s"来区别它。
凸凹贴图可以通过高光贴图来改进成相当漂亮的贴图。(要记住的是,单单凭借高光贴图是无法充分的表现材质特性的,只有Didffuse,Normal,和Specular三张配合才能充分的表现材质特性.)
在UNITY中,高光贴图通常放在漫反射贴图的透明通道里,我们是用相关的SHANDER就可以达到高光的效果。
 
AO贴图
Ambient Occlusiont简称AO贴图,中文一般叫做环境阻塞贴图。是一种目前次时代游戏中常用的贴图技术,很多朋友将其与全局光烘焙贴图混淆,其实二者本质是完全不同的。
首先,我们从简单的AO贴图的算法来讲:
AO贴图的计算是不受任何光线影响的,仅仅计算物体间的距离,并根据距离产生一个8位的通道。如下图所示,计算球形物体的AO贴图的时候,程序使每个像素,根据物体的法线,发射出一条光,这个光碰触到物体的时候,就会产生反馈,比如球右下方的一些像素锁发射的光,碰触到了旁边的政法提,产生反馈,标记这里附近有物体,就呈现黑色。、
而球上方的像素所发射的光,没有碰触到任何物体,因此标记为白色。
 


 
简单了解算法后,大家就明白,全局光的烘焙师模拟GI(全局光)所呈现的阴影效果,而AO贴图时模拟模型的各个面之间的距离。二者性质是完全不一样的。
我举例简单对比AO贴图和GI阴影贴图的区别。
根据这个低模,右边计算出的AO贴图的黑白关系,是根据物体模型距离产生的,不存在任何光源效果的影响,边缘部分等比较密集的结构,正确的产生了深色,强化了模型结构,在游戏引擎中,与其他通道贴图混合,可以提升游戏的效果。
右边的是全局光烘焙贴图的效果,是用MAX的天光计算结果进行烘焙,其阴影效果是模拟自然光线下的模型光影关系,在有结构接近的区域(比如裤袋、袖口)由于GI得光线跟踪计算会使其弱化,符合自然界光线效果,但是不是游戏所需要的效果。



  
在unity中,我们有两个地方可以调整AO,一个是在光照贴图渲染器中,有一个调整AO的参数,这个是确实渲染了一层AO。还有一个就是通过摄影机特效,有一个屏幕空间环境阻塞的特效screen speace ambient occlusion(SSAO).这两个都可以实现部分的AO效果,有兴趣的朋友可以自己尝试一下。
CUBEMAP


  
Cube map技术说到底就是用一个虚拟的立方体(cube)包围住物体,眼睛到物体某处的向量eyevec经过反射(以该处的法线为对称轴),反射向量reflectvec射到立方体上,就在该立方体上获得一个纹素了(见下图)。明显,我们需要一个类似天空盒般的6张纹理贴在这个虚拟的立方体上。按CUBE MAPPING原意,就是一种enviroment map,因此把周围场景渲染到这6张纹理里是“正统”的。也就是每次渲染时,都作一次离线渲染,分别在每个矩形中心放置相机“拍下”场景,用FBO渲染到纹理,然后把这张纹理作为一个cube map对象的六纹理之一。这样即使是动态之物也能被映射到物体表面了(虽然缺点是不能映射物体自身的任何部分)。
CUBEMAP的制作方法:
http://www.cgtextures.com/content.php?action=tutorial&name=cubemaps


  
unity3d的官网上有一段代码,叫做Camera.RenderToCubemap
讲的是怎样把我们的场景烘焙成cubemap,里面附有代码,有兴趣的可以在SCRIPT帮助文件中搜索我上一行提到的关键词。
 
LIGHTMAP
什么是烘焙? 简单地说, 就是把物体光照的明暗信息保存到纹理上, 实时绘制时不再进行光照计算, 而是采用预先生成的光照纹理(lightmap)来表示明暗效果. 那么, 这样有什么意义呢?
好处:
由于省去了光照计算, 可以提高绘制速度 
对于一些过度复杂的光照(如光线追踪, 辐射度, AO等算法), 实时计算不太现实. 如果预先计算好保存到纹理上, 这样无疑可以大大提高模型的光影效果 
保存下来的lightmap还可以进行二次处理, 如做一下模糊, 让阴影边缘更加柔和 
当然, 缺点也是有的:
模型额外多了一层纹理, 这样相当于增加了资源的管理成本(异步装载, 版本控制, 文件体积等). 当然, 也可以选择把明暗信息写回原纹理, 但这样限制比较多, 如纹理坐标范围, 物体实例个数... 
模型需要隔外一层可以展开到一张纹理平面的UV(范围只能是[0,1], 不能重合). 如果原模型本身就是这样, 可以结省掉. 但对于大多数模型来说, 可能会采用WRAP/MIRROR寻址, 这只能再做一层, 再说不能强制每个模型只用一张纹理吧? 所以, lightmap的UV需要美术多做一层, 程序展开算法这里不提及.... 
静态的光影效果与对动态的光影没法很好的结合. 如果光照方向改变了的话, 静态光影效果是无法进行变换的. 而且对于静态的阴影, 没法直接影响到动态的模型. 这一点, 反而影响了真实度 
肯定不只这几点,但我暂时只想到这几点
那么怎么生成lightmap呢?
最直接的办法: 光线追踪....(原理想想很简单, 按照物体定律来就可以了)
但是光线追踪这东西......就算用来离线生成我都嫌慢-_-
下面说的这个是利用GPU进行计算的, 跟实时光照没什么两样:
原理:
想想实时渲染的顶点变换流程: pos * WVP之后, 顶点坐标就变换到屏幕空间了[-1, 1]
如果VertexShader里直接把纹理坐标做为变换结果输出(注意从[0,1]变换到[-1,1]), 那么相当于直接变换到了纹理坐标系, 这时在PixelShader里还是像原来那样计算光照, 输出的结果就可以拿来做lightmap了
静态模型的Lightmap(光照贴图)与Vertex-Lighting(顶点光照)之比较
通常有个误解就是,Vertex-Lighting是一种不费的静态模型打光手段,因此应该被作为提升地图运行效率和减少文件尺寸的手段。这种观点,在这两方面其实都有问题Lightmap使用平展开的一套UV,如同普通皮肤贴图所需的。Lightmap的贴图大小可以灵活设置,比如64x64。这种方式提供了每像素的光照数据Vertex-Lighting使用的数据结构,包含每个顶点所受光照的亮度和色彩信息。
该数据结构消耗特定量的内存,这个量是由模型的顶点数量决定的,不能随意改变在多数情况下,静态模型应该设成使用Lightmap,因为这可以产生最好的视觉效果,最好的运行效率,而且比Vertex-Lighting消耗更少的内存Lightmap和Vertex-Lighting相比较,具有如下优点:- Lightmap可以减少CPU和GPU的占用- Lightmap让CPU需要计算的光照和物体间的互动更少- Lightmap不需要在GPU的多重pass中被渲染- Lightmap pass被整合进Emissive(自发光)pass中,因此可以缩短渲染时间- Lightmap可以表现交错覆盖于静态模型三角面上的复杂的每像素光照,然而Vertex-Lighting只能表现顶点到顶点之间线形的渐变- 使用Lightmap的静态模型,可以通过优化使用更少的三角形,获得额外的效率提升。
为使用Vertex-Lighting而制作的模型,通常需要较高的细分度,获得更多的顶点来改善顶点之间的光照过渡,然而这种做法的副作用是提升了模型的三角形数量并影响运行效率- 静态模型上的Lightmap可以设置为使用很小的分辨率,比如16x16或32x32,来减少内存开支。这对于远离游戏中心区域的静态模型来说,非常有用,这同样也适合受光很均匀的模型。
Vertex-Lighting就不具有这种优化的便利,它总是消耗同样数量的内存来存放模型全部顶点的数据结构- Lightmap可以通过调整UV的布局,来进行优化以提供尽可能好的光照质量。比如,有一个球形岩石,可以将它的底部的三角形的UV尺寸做得很小,从而让这部分在整个Lightmap的UV上面只占据很小一块,这样,对于顶部和侧面来说,就获得了更大的贴图面积于是有更精细的光照效果。Vertex-Lighting的精度总是对应于顶点数,而效果又受模型实际大小的影响(就是说缩小了看还可以的模型,放大比如一百倍,由于顶点不能改变,所以效果也变糙一百倍,而Lightmap因为可以灵活设置精度不存在这个问题),并且不能被优化如果静态模型的三角形和顶点数量很少的话,那使用Vertex-Lighting可能会比使用Lightmap占用更少的内存,然而,使用Lightmap绝对是看起来更好的,效率也更高的。
使用Lightmap让LD可以优化光照的质量和内存的占用所以Lightmap显然是比Vertex-Lighting更好的选择举个例子:比如使用UT3这游戏的静态模型HU_Deco_Pipes.SM.Mesh.S_HU_Deco_Pipes_SM_Pipe01该模型有2555个三角形和2393个顶点如果在场景中放置此模型的420个实例,并且都使用Vertex-Lighting,那么总共消耗11MB内存如果在场景中放置此模型的420个实例,并且都使用32x32的Lightmap,那么总共消耗850kb内存如果在场景中放置此模型的420个实例,并且都使用64x64的Lightmap,那么总共消耗3.3MB内存占用内存的量,也会在地图文件的尺寸上有所表现这个例子中的一部分实例,其所用的Lightmap的精度,可以设到128x128或者更高以便获得最佳的光照效果,而仍然使用相比Vertex-Lighting来说更少的内存。并且使用Lightmap的版本,要比Vertex-Lighting版本在渲染上快8-10个百分点。
 
Mipmap和detailmap
首先从MIPMAP的原理说起,它是把一张贴图按照2的倍数进行缩小。直到1X1。把缩小的图都存储起来。在渲染时,根据一个像素离眼睛为之的距离,来判断从一个合适的图层中取出***l颜色赋值给像素。在D3D和OGL都有相对应的API控制接口。


  
透过它的工作原理我们可以发现,硬件总是根据眼睛到目标的距离,来玄奇最适合当前屏幕像素分辨率的图层。假设一张32768x32768的mipmap贴图,当前屏幕分辨率为1024*1024。眼睛距离物体比较近时,mipmap最大也只可能从1024*1024的Mipmap图层选取***l。再次,当使用三线性过滤(trilinear)时,最大也只能访问2048*2048的图层选取***l,来和1024*1024图层中的像素进行线性插值。


  
detailMAP
顾名思义,就是细节的贴图,我这里有一个例子,
使用前:



  
使用后:


  
使用的着色器:


  
原理上不用赘述,其实就是图层的叠加与混合。在这里有几个关键词,一个是Detail的Tiling值,一个是这个Detailmap需要在导入的时候设置为Mipmap,里面的参数大家可以试着调一下,至于Mipmap的原理,已经在上面介绍了。

unity法线贴图原理

阅读数 1310