上一章,我们主要聚焦皮肤渲染与次表面散射。今天这一章节,我们来聊一聊头发

相较于皮肤,头发可能就更加令人挠头了:在美术层面,我们需要以发束为单位处理大量的多边形并堆砌成各式各样发型的形态;在渲染层面,我们需要让着色器使用 alpha 通道,剔除多边形上不需要的像素,呈现出单独纤细,整体密集的发丝形态。头发模型的形态和细节的刻画,会由美术的同学帮助我们完成,如果你也有兴趣了解个中一二,可以参考大神 Adam Skutt 的角色头发建模行业标杆教程。

Adam Skutt 头发建模教程:

https://www.artstation.com/artwork/02anK

确立目标

我们的目标是将一个标准的 PBR 角色头发美术资源导入 Cocos Creator 渲染呈现。美术资源包含了固有色贴图(包含 alpha 通道)、法线贴图和 AO 贴图。与皮肤篇一样,我们会基于 Cocos Creator 内置的标准 PBR 着色器制作自定义头发着色器。

注意,我们需要对美术资源做出一点小小的要求:固有色贴图(包含 alpha 通道)必须以单独 alpha 通道的 .tga 格式存储。我们将会编写的着色器并不能正确渲染带有像素剔除的 .png 格式图像。

奠定理论

首先我们仍然需要解答一个问题:什么样的效果能够让头发更真实?

观察上图,首先抓住人眼球的无疑是她头发上长条状的高光,进一步细看,我们可以注意到长条状的高光有两种颜色变化:一种是我们熟悉的偏白色高光颜色,强度也略高;另一种则是比头发固有色明度和饱和度略高的高光颜色。

在上图中我们可以看到两条看似相交的高光,这是他发型的梳理方式而决定的。这告诉我们头发的高光基本遵循每一根单独发丝的走向。在同一束头发中所有的发丝走向基本一致,所以他们的高光聚集在一起形成了条状。

在这个例子中,我们同样可以看到遵循发丝走向的两种颜色的高光,而且高光的位置似乎集中在发型弯曲的位置上。

综合起来,我们可以观察到的规律是:

  • 头发的高光呈带状,并遵循发丝的走向;

  • 头发有两条高光带,一条较强的偏白色高光带,一条偏向头发固有色的高光带;

  • 头发的高光带通常会出现在弯曲的部分。

我们知道,Specular 表述的是材质的反射光线,无论材质表面的粗糙度如何,Specular 光线传播的方向都可以从宏观上看作一个锥形,在这个锥形范围内光线的传播是平均的,这也是为什么我们在材质表面观察到的高光通常是一个圆形。这种光线传播的特性,物理上称之为各向同性(Isotropy),其涵义正如其字面意思:“在各个方向上一致的”。

然而在参考图中,头发上的高光并不是圆形的。高光只有在单个发丝上出现,而从发丝之间的横向来看,并没有产生高光的条件。整体来看,头发是一种只有垂直方向会产生高光,水平方向没有高光,垂直方向的高光密集排列在一起成带状的材质。这种在各个方向上不统一的特性,称之为各向异性(Anisotropy)

除了头发之外,任何物理上由无数会产生高光的细丝密集组合成的材质,都会表现各向异性的高光特性,比如丝绸、大多数的晶体、抛光的木材、拉丝抛光处理的金属等。

实现各向异性

目前我们在游戏中看到的大多数头发各向异性渲染效果,都基于早在1989年发表的 Kajiya-Kay 模型。那么,什么是 Kajiya-Kay 模型?

既然头发的各向异性特征表现在高光上,那么,我们应该从 Specular 入手。

我们在皮肤篇中已经聊过的“N·L”方法,利用物体法线方向和光照方向可以让我们快速得出光照明暗关系的数值。既然高光同样和物体的表面特征和光照方向相关,那么我们是否可以使用同样的方法得出 Specular 呢?

当然,我们不能机械地把 Specular 理解为强度极高、范围极小的 Diffuse。高光除了受到光照方向的影响之外,还与我们观察的角度相关。因此我们引入一个光照方向(L)和观察方向(V)之和的半向量 H,套用“N·L”的方法用它与法线 N 求点积。高光的强度远高于 Diffuse 的强度,所以我们把强度参数作为点积的指数输出。由此,我们就得到了一个基本的计算 Specular 的公式:

然而,这个公式仅适用于各向同性的情况,对于头发的各向异性特征并没有考虑进去。

如下图所示,在一般情况下,我们的 Specular 公式求的是法线和半向量的点积。但是我们通过观察得知:头发的各向异性与发丝的走向相关,与头发整体的结构关系不大。因此,N 对我们来说失去了意义,我们需要的是表达发丝走向的向量 T。

得到了 T 向量后,我们如法炮制继续套用“N·L”方法,这需要得到 T 在半向量上的投影。这个投影实际是 T 和 N 夹角的正弦,而点积只能获得两者夹角的余弦。所幸的是,我们可以通过正余弦定理,通过换算得到正弦:

vec4 worldViewDir = cc_matView * vec4(0.0, 0.0, 1.0, 0.0) - vec4(v_position, 0.0);
vec4 worldHalfDir = normalize(normalize(cc_mainLitDir) + normalize(worldViewDir));
float THdot = dot(normalize(T), normalize(worldHalfDir.xyz));
float sinTH = sqrt(1.0 - pow(NHdot, 2.0));

在上面的代码中,cc_matView 和 cc_mainLitDir 都已经在皮肤篇中出现过了,分别返回的是 View Matrix 和光源方向。

这些看上去都比较简单。那么问题是:我们如何得到向量 T?

我们知道,物体的顶点存储了切线空间数据:以顶点法线方向为一轴,顶点切线(与顶点法线垂直,与表面平行)为另一轴,与顶点法线和切线都垂直的第三个向量为第三轴。其中顶点法线和顶点切线已经包含在网格的顶点数据当中了,模型的同学已经帮我们处理好了光滑组或软硬边(取决于美术同学用的是 3ds max 还是 Maya),并按照需求提供了法线贴图。第三个向量,通常被称为副切线向量(Bi-tangent 或 Bi-normal),我们可以根据它垂直于顶点法线和顶点切线的特性,用叉积计算得到它:

v_tangent = normalize((matWorld * vec4(In.tangent.xyz, 0.0)).xyz);
v_bitangent = cross(v_normal, v_tangent) * In.tangent.w;

其实在 Cocos Creator 的 PBR 着色器中,副切线向量已经为我们计算好了,我们可以通过 v_bitangent 使用它。

目前为止,我们已经把 Specular 与发丝的走向建立了联系,但是我们的 Specular 依然是模型的高光,看上去并不像头发。我们需要使用一张发丝的灰度图,作为一个数值权重来偏移副切线向量的方向,使我们的高光向发丝的方向拉伸,从而更像头发的形态。

首先我们需要知道的是:切线空间的数据是基于物体表面的切面的,而物体的表面又由法线方向决定。因此我们对副切线向量的偏移,一定是朝着法线的方向偏移。

接下来,我们需要一张发丝的灰度图作为拉伸的权重。这张灰度图可以是一张使用头发模型 UV 的贴图,或者一张四方连续的贴图。如果是后者,我们要为它写入相应的 UV Tiling 的功能。

vec2 anisotropyUV = v_uv * anisotropyTile.xy + anisotropyTile.zw;
vec4 jitterMap = texture(jitterTex, anisotropyUV);

我们声明了一个 vec4 参数 anisotropyTile,用它来实现 UV 的控制功能。v_uv 我们已经在皮肤篇中使用过了,返回的是顶点着色器传递的 UV 数据。

我们先把法线数据与权重灰度图相乘,将法线方向加以扰动,除此之外,我们还需要相加一个位移权重的自定义参数,这个参数的意义在后面会体现出来。让后将其与副切线向量相加。这与我们求得 H 向量的方法是一样的,归一后我们得到的就是副切线向量与扰动后法线方向向量的半向量。

求得我们的 T 向量之后,对其进行点积计算,换算为正弦,代入我们的简单 Specular 公式,各向异性的高光就出来了。在这里我们还使用了 GLSL 的 smoothstep 函数,类似于 mix 函数它会把输入的参数投射到定义的最小值和最大值的区间中,并在两个极值之间生成一条平滑的过度曲线。

float anisotropyIndex( float offset, float factor, float amt ) {vec3 jitterT = v_bitangent + (v_normal * (offset + factor));float THdot = dot(normalize(jitterT), normalize(worldHalfDir.xyz));float sinTH = sqrt(1.0 - pow(NHdot, 2.0));float atten = smoothstep(-1.0, 0.0, NHdot);
return pow(sinNH, amt) * atten;
}

现在,Specular 已经基本遵循发丝的走向了,但是我们的高光似乎太强烈了,这是因为我们并没有考虑头发自身相互遮蔽的问题。解决它非常容易,只要把头发的 AO 叠加到高光上即可。

float aoFactor = mix(1.0, 0.0, pbr.x);

最后就是联动的环节了。回想一下我们观察的参考图,我们需要使用我们编写的各向异性函数生成两道高光带,还记得我们在扰动副切线向量时写入的位移参数吗?给两条高光带分别带入不同的位移参数值(hairSpecMOffset, hairSpecAOffset),他们就不会重叠在一起,并且你可以把高光移动到模型弯曲度较高的地方,获得更真实的效果。除此之外,他们分别有各自的强度参数(hairSpecMAmt, hairSpecAAmt)和颜色参数(hairSpecColor01, hairSpecColor02),我们还可以给一个总强度进行整体协调(hairSpecIntensity)。

vec4 hairSpec = clamp((anisotropyIndex(hairSpecMOffset, jitterMap, hairSpecMAmt) * hairSpecColor01 * hairSpecIntensity + anisotropyIndex(hairSpecAOffset, jitterMap, hairSpecAAmt) * hairSpecColor02 * s.albedo * hairSpecIntensity) * aoFactor), 0.0, 1.51);

做到这一步,我们的各向异性高光的函数已经基本就绪了,然而我们又遇到了与皮肤篇中相似的问题:我们在哪个通道输出高光呢?

你可能已经想到可以利用我们的各向异性高光的函数调整 roughness 通道以达到控制 Specular 输出的目的,但这个结果并不是我们想要的。我们的函数返回的并不是高光的遮罩,而且我们并不希望在高光的部分看到如同镜面一般的反射。更何况,标准 PBR 的高光仍然存在,我们更加不希望看到各向同性和各向异性的高光同时出现。既然如此,最简单的办法是:把 roughness 设为常量1,消除所有的各向同性高光,然后把各向异性高光的输出叠加在 albedo 通道上。

头发的呈现

我们的着色器已经编写完成了,但我们的工作还没有完成。如何使用这个着色器才能呈现最好的效果呢?

新建一个材质,赋予我们的各向异性着色器。将 Technique 设为 1-transparent。

首先将各个贴图赋予到相应的通道上。法线贴图对应法线通道,AO 贴图对应 Occlusion 通道,固有色贴图对应 Albedo 通道。

开启 USE ALPHA TEST,使用 alpha 通道剔除不需要的像素。这里可以使用红通道或 alpha 通道调整剔除的阈值,令“抠图”更干净。

做到这一步,头发该有的样子应该有了。然而你会发现,头发的前后关系似乎有点奇怪。你需要展开编辑器最下方的 PipelineStates 标签,在 DepthStencilState 标签下开启 DepthWrite,确认 DepthFunc 设为 Less。

当然,在默认情况下,模型的背面是不会被渲染的。如果需要实现双面材质的效果,在 RasterizeState 下的 CullMode 设为 None 即可。

做到这里,我们在 Cocos Creator 中重现的经典 Kajiya-Kay 模型头发着色器就基本完成了。

下一章,我们来给人物一对灵动的眼睛。

往期精彩

真的有那么闪耀吗?让你的人物拥有真实轻盈秀发相关推荐

  1. wlop2020全奖励包_真的要来?网友发现新更新包拥有“夜魇暗潮2020”信息

    最近有关于2020年夜魇暗潮活动的数据被网友们找了出来.这意味着这个曾经非常流行的万圣节特供游戏模式即将回归.夜魇暗潮是Dota 2历史上最早的,也是最具标志性的节日性自定义游戏模式之一. 在今天早些 ...

  2. 小i机器人闪耀首届长三角科交会,展示真实智能生活

    11月28日,2018年首届长三角科技交易博览会(以下简称"科交会")在上海汽车会展中心拉开大幕,在为期三天的时间里,来自长三角的各类科技类企业同场展示.此次首届科交会由嘉定区主办 ...

  3. android软件是真的吗,安卓手机拍照太假,苹果才够真实,真是这样吗

    原标题:安卓手机拍照太假,苹果才够真实,真是这样吗 随着科技的不断进步,现在手机的功能也随之越来越强大,而手机相机更是从最早的30万像素的摄像头,已经跃升到了现在的4800万像素,提升了160倍,其历 ...

  4. 真的有能开光追的手游了!自带实机演示的那种,OPPO这次玩“大”了

    杨净 萧箫 发自 凹非寺 量子位 报道 | 公众号 QbitAI 手机上能玩到光追游戏了! 桥豆麻袋,你确定吗? 众所周知,光追渲染效果非常溜,哪怕镜面反射.水面倒影以及阳光阴影都非常真实- 但算力要 ...

  5. ICLR 2020 | GAN是否真的判断出了数据的真假?

    ©PaperWeekly 原创 · 作者|武广 学校|合肥工业大学硕士生 研究方向|图像生成 GAN 自提出以来就以生成对抗为目标进行模型优化,这种对抗真的区分了真实数据和生成数据了吗?ICLR 20 ...

  6. 计算机科学与技术分数高吗,现在学计算机都是傻子?千万别学计算机科学与技术是真的吗?...

    选择科目 测一测我能上哪些大学 选择科目 领取你的专属报告 > 选择省份 关闭 请选择科目 确定 v> 近些年来,关于计算机类专业人才严重饱和.千万别学计算机科学与技术专业的声音越来越多, ...

  7. 我喜欢你......第二种喜欢

    阿俗 偶然寂寞太繁重身旁似乎只是观众你的感触感染没有人懂 难过谁毛遂自荐体恤让人非分特别打动爱上他前后用不到一分钟 嘿回忆爱情的内容有谁念过好头不如好尾不外是一时软弱让人纵容 没有谁面前鼓动不应爱又爱 ...

  8. Maya和Arnold的高级照明实践

    Maya和Arnold的高级照明实践 时长6小时20分钟 包括项目文件 1920X1080 MP4 语言:英语+机译中文字幕 大小:14.8G 题目:FXPHD - MYA312 - Maya &am ...

  9. 打开,保存文件框的文本溢出排查

    工作中遇到的这个问题还是很有意思的.其中嵌套了很多奇葩性的问题. (转载请指明出于breaksoftware的csdn博客) 我们来看下故事的发生过程,QA同学发现我们存在如下的bug 看到如此多的串 ...

最新文章

  1. 使用Xftp实现Windows与Linux服务器实现快速传输文件
  2. boost::spirit模块实现罗马数字解析器(演示符号表)的测试程序
  3. 【SRX】RE与PFE策略不同步,导致Commit失败-----案例分析
  4. C语言字符串压缩显示
  5. 【转】在ASP.NET Web API 2中使用Owin基于Token令牌的身份验证
  6. 打开计算机属性的命令,电脑双击打开的是属性怎么办
  7. 解决 windows npm ERR! asyncWrite is not a function 问题
  8. OpenShift 4 - DevSecOps Workshop (8) - 为Pipeline增加生成Image任务
  9. eclipse 安装egit插件
  10. python从入门到精通-Python从入门到精通
  11. list 集合 分页 三种实现方式,include jdk8 --stream
  12. linux 能降低内核,Linux 内核移除并整改了导致性能下降 50% 的 STIBP
  13. 动作捕捉系统用于模仿学习
  14. android联想搜索不到wifi,联想笔记本ThinkPad E430 无法搜索到无线网络的解决办法...
  15. 网红品牌终将祛魅,而伊利、康师傅这些老司机们却仍然历久弥新
  16. Transact -SQL 语句
  17. golang 依赖管理_简介:如何管理Golang项目依赖项
  18. Unity UGUI 背景图片自适应文字内容大小
  19. C语言实现银行ATM存取款系统 | 附源码
  20. 词霸天下---202 词根【-tribut- = -tribu-给 】

热门文章

  1. 惨遭全国大厂封杀的百分百进大厂的面试题,确定不来看看。
  2. [UE4入门笔记(13)] 40.准星 41.射线检测(续第12篇) 42.行为状态机 --梁迪老师UE4纯C++Slate开发沙盒游戏
  3. 历时四年,顺丰终于拿到了无人机物流飞行权
  4. 《JavaScript高级程序设计 (第3版)》学习笔记42:chapter_11-3 HTML5
  5. 3M推出全新的3M清洁与保护徽标项目
  6. 宇信易诚Liana银行系统软件界面设计
  7. java计算机毕业设计家庭食谱管理系统2021(附源码、数据库)
  8. 前端面试题(市场上流传最广的)
  9. dpdk l3fwd/l2fwd实验
  10. zzw_rsync命令中的/的作用