shader 获取法线_Unity Shader 入门到改行5——法线贴图
the best of blur
1. 法线贴图理论
1.1 什么是法线贴图
一般的贴图中存储的是表面颜色值(RGBA),而法线贴图存放的则是法线信息(xyzw),假设某顶点处的 uv 坐标为 (u,v), 那么在法线贴图 (u,v)处纹素的值表示该顶点的“法线”方向。通常法线贴图中存储的并不是这个顶点的真实法线信息。
1.2 法线贴图的作用
想象一下,如果我们想要表现一个凹凸不平的模型表面(想象一个橙子的表面),有哪些办法呢?
直接把模型做成凹凸不平。这种方法最理想,效果也最好。但是模型需要太多顶点了,例如橙子表面的一个“坑”,需要增加额外的若干个顶点。
做一个一定精度的平滑模型(例如把橙子做成一个球体模型),把表面的”坑“或”凸点“信息,也就是某一点的”海拔“记录下来,渲染的时候根据这些信息动态生成顶点信息,得到凹凸不平的模型。不用说,这种方法需要单独的存储空间来记录凹凸信息,而且顶点动态生成将会非常消耗。
和第二种方法一样,做一个平滑模型,同样记录表面的“海拔”,渲染时不是动态生成顶点,而是根据“海拔”信息反推顶点的法线信息,通过光照效果来表现表面的”凹凸“。这种方法在计算光照时需要先进行表面法线的计算,比较消耗。
同样做一个光滑模型,不是记录表面的凹凸信息本身,而是记录”假定的凹凸情形下的法线信息“,渲染时根据“有偏差”的法线信息来进行光照计算,使得渲染出来的画面看起来凹凸不平。
上面第三种方法称为基于“高度纹理”的凹凸表现。而第四种方法就是基于“法线纹理”的凹凸表现。
注意:高度贴图和法线贴图用来表现“凹凸”,在模型轮廓的边缘会穿帮。比如你可以用这两种方法使一个平滑的橙子模型表面看起来凹凸不平,但是在橙子的边缘总是平滑的。
1.3 法线贴图纹素取值范围
通常贴图纹素用来表示 RGBA,那么每个分量的取值范围是[0,1],而法线的每个分量取值范围为[-1,1],所以用贴图纹素表示一个法线时,需要针对每一个分量做映射
pixel = (normal + 1) / 2;
在针对法线贴图采样后,进行逆运算
normal = 2 * pixel - 1;
得到实际的法线分量值。
1.4 法线贴图基于什么坐标系
法线贴图储存了表面法线,而法线是一个方向,那么这个方向是基于什么坐标系?通常跟随顶点数据一起传输到 顶点着色器中的法线,由 NORMAL 语义指定,是基于模型坐标系的。所以我们可以将法线在模型坐标中的值存储到法线贴图中,得到模型空间的法线贴图,而在实际制作中,应用更多的是顶点切线空间的法线贴图。
对于每个顶点,以顶点自身作为原点,顶点切线方向为x轴,法线方向为z轴,切线和法线方向叉乘得到 y 轴(副法线方向),得到这个顶点的 切线坐标空间,基于这个空间的法线记录下来得到 顶点切线空间的法线贴图。
左:模型空间的法线贴图 右:切线空间的法线贴图
模型空间法线贴图的优点
(1)实现简单,直观
(2)更平滑的缝合和边界处的表现。
切线空间法线贴图的优点
(1)可重用,记录的是“相对法线信息”,而模型空间的法线贴图记录的是“绝对法线信息”。
(2)可以做 UV 动画来实现凹凸移动效果。
(3)可压缩。z分量永远是正方向,可以只存储xy分量。
1.5 为什么切线空间的法线贴图看起来都是偏蓝色的?
切线空间的法线贴图保存的是基于顶点的切线空间中的法线数值,而在顶点的切线空间中,真实法线的反向永远是(0,0,1),经过上述的计算公式得到法线贴图中存储的值为 (0.5,0.5, 1),偏蓝色。而修改后的法线通常也是 z 值最大,因为你不太可能有90度以上的法线修改,整体还是偏蓝。
通常使用顶点切线空间的法线贴图,而顶点空间中的修改后的法线值,z分量最大,换算成颜色就是 b 分量最大,所以法线贴图通常看起来偏蓝色。
2. 如何在 Shader 中应用法线贴图
我们使用在切线空间下的法线贴图,先上完整 shader 代码,然后逐步分析,代码如下:
Shader "Shader_Examples/04_NormalTexture_TangentSpace"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_SpecularColor ("SpecularColor", Color) = (1,1,1,1)
_Gloss ("Gloss", Range(8, 256)) = 20
_BumpTex ("BumpTex", 2D) = "bump" {}
_BumpScale ("BumpScale", Float) = 1.0
}
SubShader
{
Tags { "RenderType"="Opaque" "LightMode" = "ForwardBase" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _SpecularColor;
float _BumpScale;
sampler2D _BumpTex;
float4 _BumpTex_ST;
float _Gloss;
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 tangent : TANGENT;
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 lightDir : TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// 模型空间副法线
fixed3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
float3 lightDir = ObjSpaceLightDir(v.vertex);
float3 viewDir = ObjSpaceViewDir(v.vertex);
o.lightDir = mul(rotation, lightDir);
o.viewDir = mul(rotation, viewDir);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 lightDir = normalize(i.lightDir);
float3 viewDir = normalize(i.viewDir);
float3 halfDir = normalize(lightDir + viewDir);
float4 packedNormal = tex2D(_BumpTex, i.uv);
float3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
fixed3 albedo = tex2D(_MainTex, i.uv).rgb;
fixed3 diffuse = _LightColor0.rgb * albedo.rgb * saturate(dot(tangentNormal, lightDir));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;
fixed3 specular = _SpecularColor * _LightColor0 * pow(saturate(dot(halfDir, tangentNormal)), _Gloss);
return fixed4(diffuse + ambient + specular, 1.0);
}
ENDCG
}
}
}
渲染效果如图:
法线贴图效果
2.1 shader 属性与对应的变量
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_SpecularColor ("SpecularColor", Color) = (1,1,1,1)
_Gloss ("Gloss", Range(8, 256)) = 20
_BumpTex ("BumpTex", 2D) = "bump" {}
_BumpScale ("BumpScale", Float) = 1.0
}
漫反射纹理 _MainTex, 高光颜色 _SpecularColor 和高光系数 _Gloss 没什么好说的,新增的纹理 _BumpTex 为法线贴图,默认值为 unity 内置法线贴图 "bump",_BumpScale 用来控制表面的“凹凸”程度,后面会分析它是怎么起作用的。对应的变量声明:
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _SpecularColor;
float _BumpScale;
sampler2D _BumpTex;
float4 _BumpTex_ST;
float _Gloss;
2.2 着色器输入结构
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 tangent : TANGENT;
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 lightDir : TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
语义TANGENT指定的切线是一个 float4 类型的变量,而语义NORMAL指定的法线是 float3 类型,因为 TANGENT 的z分量需要用来确定 副法线 的方向,下一个段落会介绍如何计算副法线
因为使用了顶点切线空间下的法线贴图,我们需要把所有的光照计算都变换到顶点切线空间下,在顶点着色器中将光线方向lightDir和视线方向viewDir变换到顶点切线空间,再输入到片元着色器中。
因为我们这里没有涉及到纹理的 ST 变化,所以 _MainTex 和 _BumpTex 功用纹理坐标
v2f 中并没有定义法线,因为我们这里使用的是发现贴图中的法线,而不直接使用顶点法线了
2.3 顶点着色器
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// 模型空间副法线
fixed3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
// 模型空间到顶点切线空间的变换矩阵
float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
// 光线方向和视线防线变换到顶点切线空间
float3 lightDir = ObjSpaceLightDir(v.vertex);
float3 viewDir = ObjSpaceViewDir(v.vertex);
o.lightDir = mul(rotation, lightDir);
o.viewDir = mul(rotation, viewDir);
o.uv = v.uv;
return o;
}
顶点的法线:顶点所在的所有平面的法线加权平均,得到顶点法线
顶点的切线:我们都知道顶点切线与顶点法线垂直、但与顶点法线垂直的方向有很多?哪一条是顶点切线呢?约定俗成 切线最终规定为顶点 uv 坐标中的 u 方向,可以参考文末的参考文章1。
顶点的副法线:由法线和切线叉乘得到,方向性由顶点切线的z分量确定。
如何计算模型空间到顶点切线空间的变换矩阵:参考我的推导过程模型空间到顶点切线空间变换矩阵的推导。结论就是:将模型空间下的切线、副法线、法线按行排列得到变换矩阵。
在顶点着色器中将光线方向和视线方向变换到顶点的切线空间并传递给片元着色器。
2.4 片元着色器
fixed4 frag (v2f i) : SV_Target
{
float3 lightDir = normalize(i.lightDir);
float3 viewDir = normalize(i.viewDir);
float3 halfDir = normalize(lightDir + viewDir);
float4 packedNormal = tex2D(_BumpTex, i.uv);
float3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
fixed3 albedo = tex2D(_MainTex, i.uv).rgb;
fixed3 diffuse = _LightColor0.rgb * albedo.rgb * saturate(dot(tangentNormal, lightDir));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;
fixed3 specular = _SpecularColor * _LightColor0 * pow(saturate(dot(halfDir, tangentNormal)), _Gloss);
return fixed4(diffuse + ambient + specular, 1.0);
}
如何从法线贴图中得到法线:tex2D采样 _BumpTex 得到该点的法线像素值,需要计算出对应的xyz值,因我们已经在 Unity 编辑器中将 _BumpTex 设置为 "Normal Map" ,所以内置方法 UnpackNormal 已经执行了这个计算
albedo,diffuse,ambient,specular 的计算不用多说了
_BumpScale 的作用:用来控制“凹凸程度”,当 _BumpScale 为0时,表示该点的顶点法线和法线贴图中采样出的法线重合,说明该点没有“凹凸”,_BumpScale 绝对值越大,表示该点的顶点法线和贴图中的法线偏差越远,说明“凹凸感”越明显。
下面5个胶囊体的 _BumpScale 取值分别为 2/1/0/-1/-2
不同的_BumpScale凹凸效果
3. Unity中的法线贴图类型设置
在上面的片元着色器中,我们从法线贴图中采样出纹素后,使用了 Unity 内置函数 UnpackNormal 来计算最终的法线值。只有正确的设置图片的类型为 "Normal Map" 时,使用这个内置函数才能得到正确结果,在 Unity 中的设置面板如下:
法线贴图设置
Create from Grayscale 表示是否“高度图”生成的纹理贴图。当我们在贴图中记录的是相对高度(黑色表示更低,白色表示更高)时,除了要设置类型为“Normal Map”之外,还要勾选这个选项,这个贴图就会被当成纹理贴图使用了。
勾选了 Create from Grayscale 之后,有两个选项:bumpness表示凹凸程度,filtering 决定了如何生成纹理贴图,smooth 表示生成的法线过渡比较平滑,而sharp 则表示法线过渡比较锋利。
shader 获取法线_Unity Shader 入门到改行5——法线贴图相关推荐
- Unity 屏幕特效 之 简单地使用 Shader 获取深度,实现景深效果
Unity 屏幕特效 之 简单地使用 Shader 获取深度,实现景深效果 目录
- unity 给图片边缘_Unity Shader 屏幕后效果——边缘检测
关于屏幕后效果的控制类详细见之前写的另一篇博客: 这篇主要是基于之前的控制类,实现另一种常见的屏幕后效果--边缘检测. 概念和原理部分: 首先,我们需要知道在图形学中经常处理像素的一种操作--卷积. ...
- 使用opengl编程实现一个三维渲染实体_Unity Shader学习随记_01_渲染流水线
什么是Shader?它和Material(材质)的关系 Shader,中文翻译:着色器,是可编程图形管线的算法片段 Shader实际上就是一小段程序,它负责将输入的顶点数据以指定的方式和输入的贴图或者 ...
- 【shader】UE4 Subsurface Profile shader提取
尝试把UE4里面的人像提取出来(因为无法直接获得UE4使用的shader代码),这个文章是基于UE4使用的sss.此外还有其他的sss呈现方式. 原作连接https://docs.unrealengi ...
- 【Unity3D Shader编程】之十三 单色透明Shader 标准镜面高光Shader
本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接: http://blog.csdn.net/poem_qianmo/article/details/50878538 作者:毛星云(浅 ...
- 【Unity3D Shader编程】之十三 单色透明Shader 标准镜面高光Shader
分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 本系列文 ...
- UnityShader之Shader格式篇【Shader资料1】
关于Shader,在Unity里面我们一般叫做ShaderLab,只要你的职业是与渲染搭边,Unity就与ShaderLab有着直接的关联,你都应该试着去学会它,其实我们在新手未有入门的时候,我们总是 ...
- 深入URP之Shader篇5: SimpleLit Shader分析(1)
SimpleLit.shader 本篇开始分析simple lit shader.我们通过分析unlit shader了解了URP shader的结构,以及一些基础功能.而simple lit sha ...
- 深入URP之Shader篇3: Unlit Shader分析[下]
Unlit shader 上篇中我们分析了Unlit shader的Properties在ShaderGUI中的处理,接下来看Sub Shader. SubShader unlit shader以及其 ...
最新文章
- json loads No JSON object could be decoded 问题解决
- UDF、UDAF、UDTF函数编写
- 模型学习 - SVM
- iPhone 12 Max电池容量曝光:老扎心了
- 持续集成部署Jenkins工作笔记0005---应用服务器设置账号密码说明
- C#中创建线程的四种方式
- Disruptor 极速体验
- eval函数pythonmopn_pytorch:model.train和model.eval用法及区别详解
- MikroTik RouterOS 3.30 安装+免SSH全自动算号+自动注册L6图文全过程
- 勒索病毒最新变种for linux,Satan勒索病毒新变种卷土重来 安全狗提醒您注意
- 关于AD9371调试笔记
- 斐讯盒子N1_YYF_刷机ROM_讯飞语音助手实用版固件及教程分享
- Vocaloid简介
- Edwin 的基本使用
- 2021年中国吉他和低音放大器市场趋势报告、技术动态创新及2027年市场预测
- h3c trunk口改access_H3C交换机端口链路类型Trunk 端口配置指导
- 基于Java+Springmvc+vue+element实现高校心理健康系统详细设计和实现
- PF_RING 6.0.2在Redhat 6.3 x86_64上编译和安装
- Python 绘制狄拉克 delta 函数(完美实现)
- Oracle VM VirtualBox安装Win10系统
热门文章
- 如何利用 Webshell 诊断 EDAS Serverless 应用
- 十余位权威专家深度解读,达摩院2019十大科技趋势点燃科技热情
- 阿里云李刚:下一代低延时的直播CDN
- 你只差这两步 | 将Sentinel 控制台应用于生产环境
- 一次搞定各种数据库SQL执行计划
- neon浮点运算_ARM 浮点运算详解
- idea 安装php插件_免费版的 IDEA 为啥不能使用 Tomcat ?
- python统计字符在文件中出现的次数_一文搞定统计字符串中某字符出现的频次
- python均分纸牌_Python实现比较扑克牌大小程序代码示例
- Mysql 8.0 安装教程 Linux Centos7