概要

本为主要讲解生成法线贴图的基本方法,并在 unity 中进行实现和测试。

预备知识

法线贴图和基本的图形学知识,基本的向量和极限的知识。

高度图或灰度图

一张二维纹理有两个维度 u 和 v,但其实,高度(h)可以算第三个维度。有了高度,一张二维纹理就可以想象成一个三维的物体了。

先来考虑只有 u 方向的情况,如图所示, A 和 B 是纹理中的两个点, uv 坐标分别是 (0, 0) 和 (1, 0),上方黑线表示点对应的高度,那么显然,只要求出 u 方向上的高度函数在某一点的切线,就能求出垂直于他的法线了。同理, v 方向也是如此。也就是说,如果有纹理的高度信息,那么就能计算出纹理中每一个像素的法线了。

所以计算法线需要一张高度图,它表示纹理中每一个点对应的高度。

但其实并不需要求出每个纹理像素上 uv 方向各自的法线,只需要求出 uv 方向上高度函数的切线,再做一个叉积,即可计算出对应的法线了。

如果没有高度图,也可以用灰度图代替,灰度图就是把 rgb 三个颜色分量做一个加权平均,有很多种算法提取灰度值,这里用一个比较常用的基于人眼感知的灰度值提取公式。

color.r * 0.2126 + color.g * 0.7152 + color.b * 0.0722

这个公式是由人眼对不同颜色敏感度不同得来的,这里无需过多计较,直接把提取出来的灰度值作为高度值即可。

计算方法

当需要求一个点的函数图像切线的时候,只要求出该点的函数斜率即可,即是导数,这需要和它相临的点进行计算。显然,两个点越接近,结果越精确。所以有如下公式:

求出切线后,就得到了两个方向上的切线向量

。之所以是这种形式的二维向量,是因为这里是按照 uoh 平面和 voh 平面分别计算的,具体的向量形式需要根据实际情况去组合。这里可以做一个优化,在求导数的时候公式里做了一个除法,因为法线最终会归一化,切线向量长度不影响叉积后的结果向量方向,所以其实可以直接把求导数时候的除法去掉,即直接将切线向量乘以

,变为

。如果你觉得乱,没关系,后面看具体的代码就明白了。

接下来是将两个向量做叉积,叉积的顺序会影响计算出的法线的方向,这个要根据实际情况去决定。

实例

这个例子使用 unity shader 去动态的生成一张纹理中每一个像素的法线,并当作颜色输出出来,最终在屏幕上会看到一张动态生成的法线贴图。将纹理放置成平行于屏幕的方向,如下图所示:

整张纹理处于世界空间 XOY 平面,并且朝向 -Z 轴(unity 使用左手坐标系,且 Z 轴朝向屏幕里)。

由于没有高度图,所以提取出灰度值来当作高度图,二手手机号码交易平台算法根上面描述的一样,函数名为 GetGrayColor。

float GetGrayColor(float3 color){return color.r * 0.2126 + color.g * 0.7152 + color.b * 0.0722;}

然后可根据高度图的值来计算 uv 两个方向的高度函数切线。

float3 GetNormalByGray(float2 uv){   // 代码后有详细的讲解float2 deltaU = float2(_MainTex_TexelSize.x * _DeltaScale, 0);float h1_u = GetGrayColor(tex2D(_MainTex, uv - deltaU).rgb);float h2_u = GetGrayColor(tex2D(_MainTex, uv + deltaU).rgb);// float3 tangent_u = float3(1, 0, (h2_u - h1_u) / deltaU.x);float3 tangent_u = float3(deltaU.x, 0, (h2_u - h1_u));float2 deltaV = float2(0, _MainTex_TexelSize.y * _DeltaScale);float h1_v = GetGrayColor(tex2D(_MainTex, uv - deltaV).rgb);float h2_v = GetGrayColor(tex2D(_MainTex, uv + deltaV).rgb);// float3 tangent_v = float3(0, 1, (h2_v - h1_v) / deltaV.y);float3 tangent_v = float3(0, deltaV.y, (h2_v - h1_v));float3 normal = normalize(cross(tangent_v, tangent_u));return normal;}

上面代码分为 3 段,前两段为计算 uv 各自方向的高度函数切线,最后一段计算最终法线。

先看第一段,计算 u 方向的高度函数切线。首先,确定步长

的大小。MainTexTexelSize 是 unity shader 内置的一个变量,保存着纹理大小相关的信息,是一个 float4 类型的值,具体为 (1 / width, 1 / height, width, height)。_DeltaScale 是一个控制步长缩放的变量,在这个例子中为 0.5,乘以 _DeltaScale 是用来控制法线生成的精确度的,就如之前所说,

越小,生成的法线就越精确。通常我们会向当前采样点两侧去采样,以获得更精准的结果,这个方法叫做中心差分法。然后可以根据步长分别取当前像素左右两侧的高度值(在这个例子里就是灰度值),在按照上面提到的计算方法计算切线即可。注释掉的代码是原始代码,下面没注释的是优化后的代码,这个也是上面提到的。

有一个问题是,为什么计算出来的切线向量是 (x, 0, z) 的形式,而不是其他?这是因为前面提到整张纹理是处于 XOY 平面的,而高度是第三个维度,因为 u 和 v 自然是按照 x 和 y 轴处理方便,所以高度 h 就按照 z 轴来处理了。

还有一个可能的疑问是,当 _DeltaScale 特别小的时候,取两侧的像素实际上都是单前像素,则高度差都是 0 了。但实际上这个情况只有在采样过滤方式为 point 采样时才会出现,具体采样过滤方式是如何处理的可以查阅其他资料。

同理,第二段可以计算出 v 方向的高度函数切线,两个切线向量,做叉积,再归一化,即可获得当前像素点表面的法线向量。叉积的顺序很重要,因为纹理是朝向 -z 轴的,所以一般来说会让法线也顺着表面所在的朝向,这就是为什么是 cross(tangentv, tangentu) 而不是 cross(tangentu, tangentv) 的原因。

现在将法线当作颜色输出出来看一下,当然不能直接输出,因为法线向量可能包含着负值,可能看到的都是黑色,所以需要转换一下,这个转换对于了解过法线贴图的读者应该很熟悉了。

fixed4 color = normal * 0.5 + 0.5

直接输出这个 color,如下图所示:

看起来跟常见的法线贴图有些不一样,常见的是偏蓝色的那种。为什么是偏蓝色的呢,因为常见的法线贴图都是切线空间的。

基于切线空间的法线贴图,z 也就是 b 通道的值都是 0.5 到 1,而 x 和 y 也就是 r 和 g 通道都是 0 到 1,所以看起来会偏蓝一些,当然不是绝对。而上面计算出来的法线贴图,由于叉积的顺序,z 分量是朝向 -z 轴的,所以 b 通道都是 0 到 0.5,不信可以用截屏工具看下颜色值。在这个例子里,想要变成切线空间下的法线贴图是非常简单的,只需要将 z 分量乘以 -1 即可,

normal.z *= -1;fixed4 color = normal * 0.5 + 0.5

结果如下图:

根上一张图比,确实偏蓝一些了,但是依然不够蓝。这并不是因为这张纹理特殊,而是还有一些校正的步骤没有做。

在计算切线向量的时候,是直接用高度差和

值做计算的,这其实是不合理的,因为

是非常非常小的,一张 1024 * 1024 大小的图,

只有 1 / 1024 = 0.00097656,但是高度差却是 0 到 1 之间某两个数的差,例如高度为 0.6 和高度为 0.2,正常来说是远大于

的,这就导致了切线向量很接近 -z 轴,计算出的法线就很接近于 xoy 平面了,这样就看起来有很多红色和绿色,因为 x 和 y 的分量更大。为了解决这个问题,需要引入一个 _HeightScale 变量,来控制高度差的比例。

float3 GetNormalByGray(float2 uv){...float3 tangent_u = float3(deltaU.x, 0, _HeightScale * (h2_u - h1_u));...float3 tangent_v = float3(0, deltaV.y, _HeightScale * (h2_v - h1_v));...}

当这个值为 _HeightScale 值为 0.01 时,法线贴图结果如下:

这张法线贴图看起来正常了,而且仔细观察可以发现,每一个砖块的上侧是偏绿的,因为 y 对应于 g,右侧是偏红的,因为 x 对应于 r。

可以不用中心差分法吗

可以使用有限差分法,即不取像素两边相邻的点,而是只取一个方向上相邻的点与当前像素比较,这种方法想想也知道效果一般不如中心差分法的好。

除了高度差缩放,还有别的参数可以调节吗

有,这里简单列举两个,因为修改都很简单,而且效果不适合这里讲的例子,所以不在本文实现了。

凹凸值

图中每一个砖块,是凹进去的还是突出来的呢?要改变这个属性,只需要调整法线 xy 的正负即可,就会改变原有的凹凸方向,稍微想象一下应该就能想出来。

粗糙度

可以在原来的法线题图基础上,进一步修改法线贴图的粗糙度。其实之前的高度差缩放,也是处理粗糙度,但是当你有一张已经生成好的法线贴图时,想修改就需要做额外的处理了。也很简单,对法线的 xy 分量进行缩放,然后再重新计算

即可。

加上光照

法线是为了光照服务的,所以这里再演试一下加上一个平行光之后的漫反射的效果,并与没加法线贴图的效果做一下对比(默认法线为 -z 轴方向)。

首先是没有法线贴图的情况。

fixed4 frag (v2f i) : SV_Target{float3 normal = float3(0, 0, -1);fixed4 texColor = tex2D(_MainTex, i.uv);float diffuse = saturate(dot(normal, normalize(_WorldSpaceLightPos0.xyz)));fixed4 color;color.rgb = texColor.rgb * diffuse *_LightColor0.rgb;return color;}

最终的结果如下图所示:

这是将光源绕 x 轴和 y 轴都旋转了 60 度并且使用默认法线得到的 diffuse 结果,和原来没有光照的原图比较,有了明暗的变化,但依然只是一张平坦的图。

接下来是使用了上面算法动态生成法线贴图的情况。

fixed4 frag (v2f i) : SV_Target{float3 normal = GetNormalByGray(i.uv);// normal.z *= -1;fixed4 color;fixed4 texColor = tex2D(_MainTex, i.uv);float diffuse = saturate(dot(normal, normalize(_WorldSpaceLightPos0.xyz)));color.rgb = texColor.rgb * diffuse *_LightColor0.rgb;return color;}

注意这里的 normal.z 不再乘以 -1 了,因为这个例子一切都是在世界空间下计算的,正常情况下可能在切线空间算效率会更高一些,但这并不是本篇文章的内容。最终输出的结果如下图所示:

可以看到,整张图有了明显的立体感,砖块也显得粗糙了,与之前有了极大的效果提升。再仔细观察可以发现,每个砖块左边和上边都被照亮,右边和下边都变暗了,这正符合平行光的旋转角度,所以光照结果是正确的。

最后的工作

最后的工作就是把生成的法线贴图保存到硬盘上,这一步只需要调用引擎的相关 API 把渲染出来的法线贴图保存为资源即可,也可以直接在 cpu 上操作去生成一张,但这么做就不方便用实时光照去查看效果了。

从纹理中生成法线贴图相关推荐

  1. 如何在Unity实现从纹理中生成法线贴图?

    本文主要讲解从纹理中生成法线贴图的基本方法,并在 Unity 中进行实现和测试. 预备知识 法线贴图和基本的图形学知识,基本的向量和极限的知识. 高度图或灰度图 一张二维纹理有两个维度 u 和 v,但 ...

  2. 3D游戏建模入门初级教学:制作纹理逼真的法线贴图

    下图是一只恐龙的低模布线,细心的朋友估计会看到恐龙头部的布线密度是要远远高于身体和四肢的,这种布线的好处就是可以在你需要着重刻画的部位经过细分后生成的模型面数会远远高于那些次要部分,会使你的细节刻画更 ...

  3. OpenGL通过原图自动生成法线贴图

    这种生成法线贴图的效果并不是很好,最新的思路是使用基于cGANs的方法来生成法线贴图. glsl比较简单的算法,思想有点类似于人工智能中的梯度下降,步骤为: 将像素看作向量,计算出模长,代表为像素的高 ...

  4. 在 iPad 上试验从用算法生成法线贴图-到法线映射光照效果

    2019独角兽企业重金招聘Python工程师标准>>> 在 iPad 上试验从用算法生成法线贴图-到法线映射光照效果 目录 概述 一般来说, 法线贴图是用高模的法线图, 低模的纹理图 ...

  5. 【Android 安装包优化】Android 中使用 SVG 图片 ( SVG 矢量图简介 | Android 中生成 Vector 矢量图资源 )

    文章目录 一.SVG 矢量图简介 二.Android 中生成 Vector 矢量图资源 三.参考资料 一.SVG 矢量图简介 Android SVG 参考文档 : https://developer. ...

  6. Unity中的法线贴图、漫反射及高光

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

  7. 【Unity Shaders】Reflecting Your World —— Unity3D中的法线贴图和反射

    本系列主要参考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同时会加上一点个人理解或拓展. 这里是本书所有的插图.这里是本书所需的代码和资源 ...

  8. JFreeChart框架中生成饼状图上怎样显示数据 [问题点数:40分,结帖人GreenLawn]

    我用JFreeChart框架生成饼状图,但想把数据信息在饼图上显示,是在饼图内部(即圆内)显示!怎样实现啊?? 去掉lable pieplot.setLabelGenerator(null); 去掉线 ...

  9. 在vscode中生成java类图

    1.在vscode中下载插件plantuml, 安装后java文件右键菜单多了Export workspace diagrams. 2.在java项目目录,编辑sh文件保存为aa.sh,用来生成aa. ...

  10. 生成法线贴图的几款软件和轻量法线效果查看小工具

    1.CrazyBump http://crazybump.com/mac/ 2.SpriteIlluminator https://www.snakehillgames.com/spritelamp/ ...

最新文章

  1. 连接php的作用是什么,什么是超链接,有什么作用
  2. CodeForces 901C Bipartite Segments
  3. java程序员第二语言_惊呆了!Java程序员最常犯的错竟然是这10个
  4. 如何预防光纤光缆布线中的雷击伤害
  5. “约见”面试官系列之常见面试题第十一篇之canvas(建议收藏)
  6. 一会404一会500_没网络就是404?这锅可不能乱背!
  7. SQLite | Join 语句
  8. ios更改UITabBarController背景以及选中背景图片的方法
  9. java简易扑克牌_简易扑克牌游戏(java)
  10. GoldenGate中使用FILTER,COMPUTE 和SQLEXEC命令
  11. assertion: 18 { code: 18, ok: 0.0, errmsg: auth fails }
  12. C/C++ 实现模拟键盘鼠标
  13. BZOJ1299 巧克力棒
  14. android 调用百度翻译API 实现在线翻译
  15. 瑞利-贝纳尔对流(Rayleigh–Bénard convection)
  16. Airflow Timezone
  17. 中国房价必跌的40个理由
  18. 从画笔到像素:一文读懂AI绘画的前世与今生
  19. vue点击实现箭头的向上与向下
  20. 专访宜信AI中台团队负责人王东:智慧金融时代,大数据和AI如何为业务赋能

热门文章

  1. 删除后别人的微信号变成wxid_微信偷偷更新:终于能改微信号,每年改一次
  2. 【每日英文】2021.8.5
  3. Eclipse同屏显示两个代码编辑窗口
  4. php是什么水处理药剂,国内目前最主要水处理药剂分类及特点
  5. Java入门学习笔记
  6. kali无法连接网络(网络不通)
  7. 对称、群论与魔术(三)——常见的几何对称性简介
  8. SpringBoot海景房出租管理系统+代码讲解
  9. 我在上海奋斗的五年---从月薪3500到700万(读后感:一个真汉子的人生)
  10. 修改Worldpress主题的Footer/Header部分