一. 前言

对于卡通渲染而言,描边是一个非常重要的环节,非常影响游戏的品质,如果描边没做好,会大大降低游戏的美术水准。 看了下网上关于用UE4描边的案例,发现几乎都是基于屏幕空间后处理通过Slobe算法做描边的案例(也可以理解,毕竟在UE中写Shader这件事相对Unity还是有点复杂),这种描边方案不太好控制,所以一开始就毙掉了。 还有一种是修改引擎管线,加入自己的描边Pass,当然我也是这么做的,但是这种方案网络上的材质连线也都很粗糙,并没有详细说明引发的问题,以及解决方案,本身传承知识的习惯,经过了一段时间的攀爬,在此做个总结分享给大家,希望一起进步,撰写仓促,文章内容难免有错误纰漏之处,如若读者能不吝告知,则不胜感激。

二.问题提出

新增Pass的描边方法很常见原理也很简单,分两个Pass,第一个描边Pass中渲染背面,根据法线向外offset外扩,第二个Pass中正常渲染即可。这种方案对于软模型而言效果不错,而且高效。但是如果不做特殊处理,和在Unity引擎中做描边一样,也都会遇到以下两个问题:

1,随着相机距离拉远拉近,由于透视原因,描边的粗细会发生变化,而我们需要一种不论距离变化多大,描边始终是一个宽度的效果。

2,关于硬表面物体的描边,如下图的Cube这种硬表面模型而言,在法线断开处outline自然会出现断裂。还有游戏中比如武器,硬转折的发片等,这些物体由于法线顶点的不统一都会引发断裂问题。我们需要使其平滑且连续。

三.思路介绍与梳理

1,关于上文的问题1提出两种思路来解决。由于UE本身只提供了世界空间的偏移,所以:

(1). 想办法在世界空间中加入和摄像机相关的因子,具体实现见下文.

(2). 修改MobileBasePassVertexShader.usf顶点着色器文件,在其他空间做描边,如下图在Clip空间做偏移,外部传递进去WorldPositionOffset(在材质中需要变换到Clip空间)仅当作Clip空间的偏移参数。

2,关于上文的问题2也提出两种思路来解决(~~).

(1). 法线断裂其实是因为在三维软件中比如Max同一个顶点上光滑组不统一,每个顶点的法线不止一条,如下图,这样的话到了引擎中其实是会分开3个顶点的,每个顶点按照自己的法线方向往外扩,可不就出现断裂了嘛,所以方案也出来了,在美术制作模型的时候统一光滑组,即不给物体分光滑组,这样的话所有的物体由于顶点处的法线方向统一,所以也就不会发生断裂现象。但是这样的话没了光滑组,很多软边不太好处理,也只能退而求其次强行加倒角,在边需要柔化的地方倒角(maya中也可以加入循环边),用更多的面去卡线,这是一种简单而粗暴的做法。

(2),通过一种方法将平均后的法线写入FBX数据中,然后在引擎材质中访问修正后的数据当作外扩的法线使用. 这里需要考虑3个小问题,依次展开:

a,存在哪个数据通道。既然要保存数据到模型中去,要么保存到顶点色,要么切线中,要么UV中。 思来想去考虑到顶点色做描边的颜色和粗细,切线通道在头发各向异性高光中可能会用到,所以最后只剩下UV通道中,对于场景静态物体,第2套UV肯定不能占用,UE引擎需要第2套UV作为烘培需要用。对于角色动态物体,考虑到现在流行的次世代卡通着色,对于一些布料材质有可能需要细节法线纹理,而且会出现在某些特殊的位置,这个时候如果完全铺设在正常的第一套UV中,那么布料占据的UV可能会非常小,细节会损失,这时候就需要保存到第2套UV中, 所以最后我们选择把平均化后的法线数据保存到了模型的第3套UV中,第1套正常UV数据,第2套对于衣服细节纹理UV占用,第3套一般不会用到,所以正好适合放描边的数据,当然你也可以放到其他的4-8套的任意一套中。 想明白这一点后就可以开始计算法线平均值然后写入数据了。

b,写入哪个空间。因为我们在Shader中拿到的模型空间,已经是跟着骨骼运动后的模型空间了。 所以我们需要把数据存在切线空间里面或者直接储存在切线,然后转到模型,世界空间,就是骨骼运动后的数据了,也可以简单理解为运动数据存储在切线空间的变换矩阵里面。

c,使用什么工具写入。写入数据有几种方式,要么在三维软件Max或者Maya中计算顶点的平均法线写入,要么在其他可以操作模型的引擎中写入。 由于Unity原生读取Mesh的方便之处,再加上提供了FBX Export Package,使其变得相当方便,所以这里选用了Unity引擎。

方案选择:考虑到描边参数的可控性以及尽量少改动UE的原则的情况下,描边采用了不修改引擎,依然在世界空间下,不过把和相机因素考虑进去解决描边随相机变化粗细变化问题。为了得到最好的效果而且省去美术工作,选择修改法线数据,将平均后的法线数据保存在蒙皮网格的切线空间下的第3套UV中上

4.实践

1,通过修改引擎,加入自定义描边Pass,实现思路基本完全按照这个系列文章操作,没什么太多要说的。

白昼行姜暗夜摸王:尝试在UE4.22中实现罪恶装备Xrd的卡通渲染​zhuanlan.zhihu.com

2,不考虑模型描边断裂的情况,解决随相机距离远近变化的问题。最简单的如下图1所示,直接在世界空间挤出,肯定会引发描边的粗细会随相机的远近存在一个近大远小的问题。按照上述文章里的材质连线其实可以解决一部分问题,但是还是不是太好控制,于是自己改写了描边材质如下图2所示。这里获取模型和相机的距离对OutlineWidth进行补充,距离的越远补充的越大,越近补充的越小,而且加入了Power值控制强度而非仅仅线性,使其达到越远补充的效果越强的一种效果。同时将顶点色考虑进去,控制其描边颜色和粗细。

图1
图2

3,模型描边断裂的情况。打开Unity,导入FBX,编写获取平均法线,然后写入的功能逻辑。写入完以后,需要在Shader中读取获得并利用,为了确保正确这里也写一套进行验证。代码如下,当写入完成后可以看到下方效果对比图,Box的硬边变得连续了,所以证明数据已经成功写入。

(1),正常描边Pass

                 Pass{Cull FrontCGPROGRAM#include "UnityCG.cginc"  #pragma vertex vert  #pragma fragment frag  fixed4 _OutlineCol;float _OutlineFactor;struct v2f{float4 pos : SV_POSITION;};v2f vert(appdata_full v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);float3 vnormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);//正常做法,视空间法线外扩,没考虑到相机的透视问题//float2 offset = TransformViewToProjection(vnormal.xy);//o.pos.xy += offset * _OutlineFactor / 1000;//把clip.w考虑进去,解决相机拉远拉近导致描边殂谢改变的透视问题,以及屏幕宽高比描边不均衡问题(ScreenParam.xy).float2 offset = normalize(clipNormal.xy) / _ScreenParams.xy * _OutlineWidth * o.pos.w * 2;o.pos.xy += offset;return o;}fixed4 frag(v2f i) : SV_Target{return _OutlineCol;}ENDCG  }

(2),使用修正过的法线数据的描边Shader

         v2f vert(appdata v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);float4 worldPos = mul(unity_ObjectToWorld, v.vertex);float3 worldNormal = UnityObjectToWorldNormal(v.normal);float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);float3 worldBitangent = normalize(cross(worldNormal, worldTangent) * v.tangent.w);float3x3 tangentTransform = float3x3(worldTangent, worldBitangent, worldNormal);float3 fixedNormal = UnpackNormalRG(float3(v.uv2, 1.0));fixedNormal = normalize(fixedNormal);float3 worldSpaceFixedNormal = normalize(mul(fixedNormal, tangentTransform));float3 localSpaceNormal = mul((float3x3)unity_WorldToObject, worldSpaceFixedNormal);float3 viewSpaceNormal = mul((float3x3)UNITY_MATRIX_IT_MV, localSpaceNormal);float2 ndcnormal = normalize(mul((float3x3)UNITY_MATRIX_P, viewSpaceNormal).xy) * o.pos.w;o.pos.xy += ndcnormal / _ScreenParams.xy * _OutlineFactor * 2;o.uv = TRANSFORM_TEX(v.uv, _MainTex);return o;}fixed4 frag(v2f i) : SV_Target{return _OutlineCol;}

(3),保存模型法线数据的C#部分

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;public class PluginMeshTools
{[MenuItem("MeshTools/模型平均法线写入UV2数据")]public static void WirteAverageNormalToTangentToos(){MeshFilter[] meshFilters = Selection.activeGameObject.GetComponentsInChildren<MeshFilter>();foreach (var meshFilter in meshFilters){Mesh mesh = meshFilter.sharedMesh;WirteAverageNormalToTangent(mesh);}SkinnedMeshRenderer[] skinMeshRenders = Selection.activeGameObject.GetComponentsInChildren<SkinnedMeshRenderer>();foreach (var skinMeshRender in skinMeshRenders){Mesh mesh = skinMeshRender.sharedMesh;WirteAverageNormalToTangent(mesh);}}private static void WirteAverageNormalToTangent(Mesh mesh){var averageNormalHash = new Dictionary<Vector3, Vector3>();for (var j = 0; j < mesh.vertexCount; j++){if (!averageNormalHash.ContainsKey(mesh.vertices[j])){averageNormalHash.Add(mesh.vertices[j], mesh.normals[j]);}else{averageNormalHash[mesh.vertices[j]] =(averageNormalHash[mesh.vertices[j]] + mesh.normals[j]).normalized;}}var averageNormals = new Vector3[mesh.vertexCount];for (var j = 0; j < mesh.vertexCount; j++){averageNormals[j] = averageNormalHash[mesh.vertices[j]];}var uv2 = new Vector2[mesh.vertexCount];var oriTangents = mesh.tangents;var oriNormals = mesh.normals;var oriBitangent = Vector3.one;for (var j = 0; j < mesh.vertexCount; j++){oriBitangent = (Vector3.Cross(oriNormals[j], oriTangents[j]) * oriTangents[j].w).normalized;//构建tbn矩阵, 默认tbn->切线到模型var tbn = new Matrix4x4( oriTangents[j], oriBitangent, oriNormals[j], Vector4.zero);//旋转矩阵的逆矩阵等于转置矩阵,所以转置tbn,获得其逆矩阵=>模型到切线空间tbn = tbn.transpose;var bakeNormal = tbn.MultiplyVector(averageNormals[j]).normalized;//归一化后写入uv2[j] = new Vector2(bakeNormal.x * 0.5f + 0.5f, bakeNormal.y * 0.5f + 0.5f);}//uv从0开始mesh.SetUVs(2, uv2);}
}

(4),效果对比,可以看出修正过的法线描边连续了

(5),通过PakageManager下载FBX Exporter包,下载完以后将修改后的Mesh数写入到FBX中并导出,特别的当导出蒙皮骨骼网格的时候要勾选带有AnimSkinned Mesh选项。

4,导入UE4中修改描边材质,取第3套UV数据(Texcoord[2]),观看效果,描边不再断裂了。

效果不明显

四.总结

上述通过借助Unity引擎来修改FBX数据的方式是一种新思路,并不算最优解,最优解是导入FBX后在UE引擎中直接修改SkeletalMesh的数据并保存(无奈太菜,改了几次都失败了,如果有大牛成功修改了,希望能够写一下文档让我等菜鸟观摩一下。。)。 同样也希望能有更多人将技术分享出来一起学习,借用隔壁yiyi大佬 @flashyiyi 的经典独白结语,他最近关于蓝色协议的分析也非常好,建议品读。

flashyiyi:蓝色协议技术分享解读​zhuanlan.zhihu.com

reference:

喵刀Hime:【Job/Toon Shading Workflow】自动生成硬表面模型Outline Normal​zhuanlan.zhihu.com

大胖:硬边外描边断边问题​zhuanlan.zhihu.com

https://www.videopoetics.com/tutorials/pixel-perfect-outline-shaders-unity/​www.videopoetics.com

---------------------------------------2019.6.15修改-----------------------------------------

----------------------------------

五 补充

经过yiyi大佬的指点,并参考了 @Oldside 的文章,修改了Fbx导入模块,主要是FbxSkeletalMeshImport.cpp文件,在这个文件中我们找到处理Fbx Mesh数据的方法 FillSkeletalMeshImportData,然后这个方法会调用FillSkelMeshImporterFromFbx这个方法,然后在这个方法里能看到具体如何将FBX数据转换成UE自己的数据,其实就是调用FbxSDK里的方法转成自己的形式而已。 所以在这之前我们只要将数据提前处理然后保存就可以了,效果其实是一样的, 如下

void UnFbx::FFbxImporter::StoreNormalsToVertColor(FbxMesh* mesh)
{//获取layerFbxLayer* layer0 = mesh->GetLayer(0);//依次获取layer中的顶点色、2uv、法线、切线、副法线FbxLayerElementVertexColor* VertColor = layer0->GetVertexColors();FbxLayerElementUV* UV2 = mesh->GetLayer(1)->GetUVs();FbxLayerElementNormal* VertNormal = layer0->GetNormals();FbxLayerElementTangent* VertTangent = layer0->GetTangents();FbxLayerElementBinormal* VertBinomral = layer0->GetBinormals();//逐顶点遍历操作for (int j = 0; j < mesh->GetPolygonVertexCount(); j++){FbxArray<int> SameControlPointsIndex;for (int k = 0; k < mesh->GetPolygonVertexCount(); k++){//将重复的保存下来,代表是同一个顶点if (mesh->GetPolygonVertices()[k] == mesh->GetPolygonVertices()[j]){SameControlPointsIndex.Add(k);}}//去除重复数据FbxArray<FbxVector4> Normals;for (int x = 0; x < SameControlPointsIndex.Size(); x++){FbxVector4 Normal = VertNormal->GetDirectArray()[SameControlPointsIndex[x]];Normals.AddUnique(Normal);}//将所有不同方向的法线加在一起并归一化获得光滑法线FbxVector4 SmoothNormal;for (int n = 0; n < Normals.Size(); n++){SmoothNormal += Normals[n];}SmoothNormal.Normalize();//构建tbn矩阵FbxVector4 Tangent = VertTangent->GetDirectArray()[j];FbxVector4 Normal = VertNormal->GetDirectArray()[j];FbxVector4 Bitangent = VertBinomral->GetDirectArray()[j];//将法线从模型空间转为切线空间FbxVector4 tmpVector;tmpVector = SmoothNormal;tmpVector[0] = Tangent.DotProduct(SmoothNormal);tmpVector[1] = Bitangent.DotProduct(SmoothNormal);tmpVector[2] = Normal.DotProduct(SmoothNormal);tmpVector[3] = 0;SmoothNormal = tmpVector;//找到定点色对应的颜色索引int VertColorIndex = VertColor->GetIndexArray()[j];//将法线数值范围从-1~1处理为0~1后存入RGB通道中,A通道保持//不变,因为其中存放着轮廓线大小信息FbxColor Color;Color.mRed = SmoothNormal[0] * 0.5f + 0.5f;Color.mGreen = SmoothNormal[1] * 0.5f + 0.5f;Color.mBlue = SmoothNormal[2] * 0.5f + 0.5f;Color.mAlpha = VertColor->GetDirectArray()[VertColorIndex].mAlpha;//将颜色写入顶点颜色layer中VertColor->GetDirectArray().SetAt(VertColorIndex, Color);//第二种方案:尝试将数据写入第2套UV,想办法把顶点色留出来用来设置描边的颜色,可惜失败了~~~//具体原因不明,不知道UE在处理2UV的时候做了什么操作,但方法写出来供大家参考if (UV2){int UV2Index = UV2->GetIndexArray()[j];FbxVector2 v2;v2[0] = SmoothNormal[0];v2[1] = SmoothNormal[1];UV2->GetDirectArray().SetAt(UV2Index, v2);}}
}

修改过程中遇到了一点很诡异的问题,将平均化的法线保存到顶点色是可以的,保存到第2套UV中取出来的数据就坏了(具体原因不明,可能是在后续UE转化成自己数据的时候又进行了拆分?,所以最好在UE处理完FbxMesh数据后即FileSkelMeshImporterFromFbx方法后边修改,太长懒得改了~)。由于将平均化的法线保存到了顶点色中,所以描边颜色暂时能想到的方法是美术制作SkelMesh的第2套UV(也很简单,直接将UV1复制copy到UV2中就行),然后采样一张代表不同区域描边颜色的贴图。只不过从性能角度考虑,需要多采样一张代表不同区域描边颜色的贴图,这张图由于只是代表区域的描边颜色,纯色块,所以可以设置贴图大小非常小,应该也还好。 好了这样的化就实现了在UE里边自动化导入,自动化设置法线的流程了,比最初的方法更方便了一些。

完结

referrence:

Oldside:Unity硬表面模型描边断裂问题解决过程记录​zhuanlan.zhihu.com

ue4 改变枢轴位置_在UE4引擎中做卡通描边的一点心得相关推荐

  1. ue4 改变枢轴位置_【UE4地形】轻松实现UE4自动地貌和自动植被分布

    今天是2020年11月09日  星期一 正文共:2624字 48图  预计阅读时间7分 首先介绍下我们制作自动地貌所需要的节点顺序不分先后 Break MaterialAttributes(中断材质属 ...

  2. ue4 改变枢轴位置_[UE4]偏门实用技巧合集

    前言: 不论是玩游戏,还是玩软件,我对操作和技巧都有着痴迷的追求.而且是个快捷键狂魔. 从这个工具就能看出来,我对快捷键的执着,对工作效率极致提升的吹毛求疵. 戴巍:[SD Plugin] 快捷键创建 ...

  3. ue4 改变枢轴位置_[UE4蓝图][Materials]虚幻4中可互动的雪地材质完整实现(一)

    不说废话,先上个演示图 最终成果(脚印,雪地可慢慢恢复,地形可控制) 主要原理(白话文): 假如你头上是块白色并且可以透视的平地,来了个非洲兄弟踩上面,你拿起单反对着上面拍了一张,照片如下 把脚印稍作 ...

  4. ue4 改变枢轴位置_[UE4蓝图][Materials]虚幻4中可互动的雪地材质完整实现(二)

    上一篇: [UE4蓝图][Materials]虚幻4中可互动的雪地材质完整实现(一)​zhuanlan.zhihu.com 10.新建一个MaterialParameterCollection(材质参 ...

  5. ue4 改变枢轴位置_UE4-构建更好的静态网格体

    在本文中,我们会讨论包括系统单位.三角面数量.材质ID.枢轴点,我们会学习什么是光照贴图,然后介绍如何创建光照贴图.我们将学习碰撞网格体的创建和使用,细节层级或 LOD,最后介绍如何限制过度绘制. 1 ...

  6. ue4 改变枢轴位置_UE4虚幻引擎学习云笔记(五)-静态网格体编辑器

    [五.静态网格体编辑器(Static Mesh Editor)] 19-09-26 静态网格体编辑器一般用以分配材质至静态网格体,每个LOD可以有不同材质. 静态网格体编辑器(Static Mesh ...

  7. ue4 改变枢轴位置_UE4实时渲染深入探究----学习总结【上篇】

    写在前边 周末学习了下UE4实时渲染的视频,看完后觉得收获颇多,为了以后自己复习方便,记录于此,如果能帮助到大家,则再好不过了.为了以后复习的准确性和深刻,在这里我力求写的尽可能的详细,同时尽可能的保 ...

  8. ue4 改变枢轴位置_System Era Softworks如何利用UE4创作Astroneer的精彩宇宙

    太空沙盒游戏<Astroneer>是一款开发多年的高人气独立作品.这款游戏首次公布于2015年10月,并在次年发行了抢先体验版.凭借System Era Softworks在这个项目中注入 ...

  9. ue4 改变枢轴位置_UE4渲染模块概述(四)---反射

    在前一文中介绍了像素着色器与material,大概知道了UE4材质的生产管线: Jerry:UE4渲染模块概述(三)---Pixel Shader & Material Rendering​z ...

  10. ue4 改变枢轴位置_houdini+ue4道路(2):思路

    最近在搞道路生成,终于完成了一版demo,主参考了一些3A大厂的分享,自己补全了一些细节. 由于本人贴图制作水平所限,效果还远没有达到最优. 后面计划做个分享,先打个草稿. 一,背景 道路生成,目前最 ...

最新文章

  1. 你想知道的“ROC曲线”
  2. python3 ipaddress模块 创建 检查 操作ip地址 简介
  3. python在化学方面的应用-学材料、化学的要不要担心人工智能抢了自己的饭碗?...
  4. ICH10R服务器主板是什么芯片,Intel ICH10R 芯片组 RAID配置
  5. MSTP协议介绍和堆叠技术介绍
  6. 程序员职业发展路线规划,快来康康你“修炼”到哪个段位了?
  7. 计算机如何模拟人类说话,七十、计算机如何模拟痛觉
  8. PHP搭建留言板,PHP搭建简易留言板
  9. Spring学习资料
  10. 生成领料单(编号:20110704A1153)
  11. PCB各层的含义(讲的非常易懂清晰)
  12. 饥荒服务器不显示管理员,饥荒联机版管理员怎么添加_饥荒联机版管理员介绍与添加方法详解_玩游戏网...
  13. CentOS安装XenServer Tools
  14. 权限管理系统数据库设计的简单构思
  15. isam2 优化pose graph
  16. LeetCode知识点总结 - 977
  17. Oracle ClusterwarePRCT-1011 : Failed to run oifcfg. Detailed error: null
  18. Django urls 下划线的坑-Using the URLconf defined in xxx, Django tried these URL patterns, in thi
  19. 美女导游孙洁到欧洲推销中国旅游,携程缘何成了名片?
  20. 计算机仿真技术教学大纲,《电子电路计算机仿真综合训练》教学大纲

热门文章

  1. SQLite数据库中的.db-shm文件和.db-wal文件
  2. Android 10 SystemUI 如何隐藏状态栏耳机图标和定位图标
  3. 【听说隔壁老王开始学编程了?】
  4. 上升时间测量与示波器带宽
  5. 如何在PPT中设置选择题
  6. python3识别图中的文字_Python3调用百度AI识别图片中的文字功能示例【测试可用】...
  7. alert获取输入框内容_Alert弹出框处理
  8. 【树莓派】从零搭建DAS服务器,挂载扩容硬盘,实现文件存储与自动下载
  9. linux系统文件夹
  10. 寒风里的凌厉香气,令人沉醉的男士魅力