前言:本篇博客只是一个简单的了解法线纹理创建和采样的例子,主要是当做笔记使用。
凹凸映射:由于场景中的模型会存在凹凸不平的效果,如果把这个凹凸效果交给建模师来构建模型的话,这样模型的三角面片将会十分的巨大,从而造成gpu的渲染压力。这时就可以将模型贴图的每个像素点的高度信息转换成法线信息并以像素的形式存储在法线纹理中。当渲染模型时就会从法线纹理中采样获取像素信息,并将该像素信息转换成法线信息,然后与光照进行计算,从而得到凹凸不平的效果。
法线纹理获取方式:如下所示:
1.使用第三方工具(CrazyBumpSetup或者ShaderMap Pro等)将源贴图生成一张法线纹理贴图。
2.使用unity设置面板来获取。将类型设置成法线纹理类型,如果源纹理中不是存储的法线数据的话,可以从源纹理灰度中获取法线数据。如图所示:
3.自己编写法线纹理生成算法。以unity中从源纹理的灰度值获取法线纹理算法为例:
核心流程如下:
1>.将源纹理中每个像素的右边和左边深度差组成水平方向的差分向量,然后将像素的下边和上边深度差组成的垂直方向的差分向量。
2>.将水平和垂直差分向量进行叉乘来获取法线向量,并将该法线向量转换为值域空间在0~1的像素数据。
3>.由于windows上用的法线解析函数是UnpackNormalDXT5nm,这个函数中法线x分量取颜色w分量,法线y分量取颜色y分量,法线z分量是在法线x和y分量上进行计算后获取。所以将2步骤中获取的像素中的r分量存在w位置,颜色中的g分量还是存在y位置。
4>.从法线纹理中进行采样来获取颜色数据,然后将颜色数据按照3步骤中的算法进行解码来获取到值域在-1~1之间的法线数据。
5>.将获取到的法线数据和光照进行处理,从而得到凹凸效果。
核心代码如下:
using UnityEngine;
using System.Collections;
public class CreateNormalMap : MonoBehaviour {
// 原始纹理
public Texture2D srcTex;
// 法线纹理
public Texture2D normalTex;
// Use this for initialization
void Start () {
for (int h = 1; h < srcTex.height - 1; ++h)
{
for (int w = 1; w < srcTex.width - 1; ++w)
{
// 获取当前像素位置的前和后对应的高度差值,此处高度值取像素中的r分量
float uLeft = srcTex.GetPixel(w - 1, h).r;
float uRight = srcTex.GetPixel(w +1, h).r;
float u = uRight - uLeft;
// 获取当前像素水平位置的高度差分向量:u代表水平方向,x为1,y为0,z轴存放高度差值
Vector3 vec_u = new Vector3(1, 0, u);
// 获取当前像素位置上和下对应的高度差值,此处高度值取像素中的r分量
float vTop = srcTex.GetPixel(w, h - 1).r;
float vBottom = srcTex.GetPixel(w, h + 1).r;
float v = vBottom - vTop;
// 获取当前像素垂直位置的高度差分向量:v代表垂直方向,x为0,y为1,z轴存放高度差值
Vector3 vec_v = new Vector3(0, 1, v);
// 当前像素位置的水平和垂直高度差分向量进行叉乘得到法线向量
Vector3 N = Vector3.Cross(vec_u, vec_v);
// 将法线纹理映射到0~1的法线纹理像素中
N.x = N.x * 0.5f + 0.5f;
N.y = N.y * 0.5f + 0.5f;
N.z = N.z * 0.5f + 0.5f;
// 由于windows上用的法线解析函数是UnpackNormalDXT5nm,这个函数中法线x分量取颜色w分量,法线y分量取
// 颜色y分量,法线z分量是在法线x和y分量上进行计算后获取,所以此处代码如下:
normalTex.SetPixel(w, h, new Color(0, N.y, 0, N.x));
}
}
// 同步更新法线纹理修改
normalTex.Apply();
}
// Update is called once per frame
void Update () {
}
}
使用效果如图所示:移动点光源到u字母附近会发现凹凸处高亮和变暗等效果。
法线纹理与光照交互:获取到法线纹理中的法线数据后,一般需要与光源进行交互。我推荐使用内建的着色器,因为它做了很多处理才能达到逼真的凹凸效果。但是这里我也简单表达下法线纹理与光照进行交互的基础细节。
核心要点如下:
1.法线纹理当中记录的法线数据在模型不同面上可能存在相同的采样坐标。当光源固定在某个位置时就会出现某些面的光照强度错误,以及背面也被渲染出来的错误。如图所示:
2.顶点纹理坐标系指的是一个垂直于顶点所在面的法线向量代表z轴,一个经过顶点并垂直于法线向量的切线向量代表x轴,一个垂直于法线向量和切线向量的次法线向量代表的y轴。如图所示:
3.顶点的纹理坐标系就是用来解决1中所提到的问题。它可以将光照向量转换到顶点的纹理坐标系中,从而保证每个顶点的光照是不同的。如图所示:
4.一个pass中使用ForwardBase光照模型来处理平行光,一个pass使用ForwardAdd光照模型来处理点光源。然后使用blend one one将两个pass中光照处理后的颜色进行混合输出。
5.法线纹理采样后需要使用UnpackNormal来转换到-1~1之间的法线向量,然后将光照向量转换到纹理坐标系空间中并和获取的法线向量求点积来获取光照强度。由于平行光任何方向上强度一样,点光源随着距离物体距离越远强度会越弱,此时可以通过光照向量w分量是否不为0来判断是点光源,然后使用光照向量长度来粗略的当做衰减系数使用。
核心代码如下:
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "Custom/Bumped" {
Properties {
_NormalMap("Normal Map", 2D) = "white"{}
}
SubShader {
// 法线纹理和平行光交互
pass {
// ForwardBase光照模型用来将平行光作为重要光源进行处理
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "unitycg.cginc"
#include "lighting.cginc"
struct v2f {
float4 pos:POSITION0;
float2 uv:POSITION1;
float3 lightDir:POSITION2;
};
sampler2D _NormalMap;
v2f vert(appdata_tan v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// 获取纹理采样坐标:不考虑偏移和缩放
o.uv = v.texcoord.xy;
// 将顶点的切线向量和法线向量进行叉乘来获取一个垂直于顶点切线向量和顶点法线向量的顶点次法线向量
float3 binormal = cross(v.tangent.xyz, v.normal);
// 获取顶点的纹理坐标系变换矩阵:顶点切线向量,顶点法线向量以及顶点次法线向量组成的三维坐标系
float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
// 将光照向量转换到纹理坐标系空间中:目的是为了避免不同面有相同采样坐标时造成的光照方向和背面被光照的问题
o.lightDir = mul(rotation, _WorldSpaceLightPos0.xyz);
return o;
}
fixed4 frag(v2f IN):COLOR0
{
// 获取归一化的光照向量
float3 L = normalize(IN.lightDir);
// 对法线纹理进行采样来获取像素,然后将该像素转换成归一化的法线信息(由原始纹理的高度值按照指定算法获取)
float3 N = normalize(UnpackNormal(tex2D(_NormalMap, IN.uv)));
// 获取光照强度,由于负值代表光照在物体背面,此时可以不做光照处理,所以值要限定在0~1之间
float ndotl = saturate(dot(N, L));
// 获取平行光颜色
fixed4 col1 = _LightColor0 * ndotl;
// 获取环境光
fixed4 col2 = UNITY_LIGHTMODEL_AMBIENT;
return col1 + col2;
}
ENDCG
}
// 法线纹理和点光源交互
pass {
// ForwardAdd光照模型用来将点光源作为重要光源进行处理
Tags { "LightMode" = "ForwardAdd" }
// 混合上面pass对平行光和处理
blend one one
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "unitycg.cginc"
#include "lighting.cginc"
struct v2f {
float4 pos:POSITION0;
float2 uv:POSITION1;
float3 lightDir:POSITION2;
};
sampler2D _NormalMap;
v2f vert(appdata_tan v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// 获取纹理采样坐标:不考虑偏移和缩放
o.uv = v.texcoord.xy;
// 将顶点的切线向量和法线向量进行叉乘来获取一个垂直于顶点切线向量和顶点法线向量的顶点次法线向量
float3 binormal = cross(v.tangent.xyz, v.normal);
// 获取顶点的纹理坐标系变换矩阵:顶点切线向量,顶点法线向量以及顶点次法线向量组成的三维坐标系
float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
// 将光照向量转换到纹理坐标系空间中:目的是为了避免不同面有相同采样坐标时造成的光照方向和背面被光照的问题
o.lightDir = mul(rotation, _WorldSpaceLightPos0.xyz);
return o;
}
fixed4 frag(v2f IN):COLOR0
{
// 获取归一化的光照向量
float3 L = normalize(IN.lightDir);
// 对法线纹理进行采样来获取像素,然后将该像素转换成归一化的法线信息(由原始纹理的高度值按照指定算法获取)
float3 N = normalize(UnpackNormal(tex2D(_NormalMap, IN.uv)));
// 获取光照强度,由于负值代表光照在物体背面,此时可以不做光照处理,所以值要限定在0~1之间
float ndotl = saturate(dot(N, L));
// 获取衰减系数:距离物体越远,点光源强度越弱
float atten = 1.0;
if (_WorldSpaceLightPos0.w != 0)
{
atten = 1.0 / length(IN.lightDir);
}
// 获取点光源颜色
fixed4 col = _LightColor0 * ndotl * atten;
return col;
}
ENDCG
}
}
}
使用效果如图所示:点光源距离物体越远光照越暗直到不光照导致物体变黑。平行光夹角越偏离物体物体变黑,否则物体变量。