找回密码
 立即注册
查看: 657|回复: 10

[简易教程] 【原神】Unity默认管线尝试还原角色渲染

[复制链接]
发表于 2021-12-18 05:39 | 显示全部楼层 |阅读模式
渲染小菜鸡一枚,这个渲染还原是很久之前就想做了,这次回到学校正好就大概参考着大佬们的文章摸出了一个大致外形。如果有做的不对的地方欢迎指正。另外:大佬们的贴图都是在哪里找的,可以指路吗
描边

在Toon Shading中十分重要的一环是针对物体进行描边,我的代码里面用的是《GUILTY GEAR Xrd》中的Back Facing描边法。Back Facing描边法的基本思路是:第一次绘制角色,第二次绘制描边。在绘制描边的时候,在顶点着色器里面将顶点方向沿着法线方向推出一段距离,使得物体的轮廓放大渲染作为描边。同时在描边绘制时选择Cull Front。这样描边和角色重合的部分会因为不能够通过深度测试而被Cut掉,从而保证描边不会遮挡角色。两次绘制的顺序颠倒也是可以的。不过后绘制描边的话,可以通过深度检测过滤掉很多描边绘制的像素,效率会更好。
但是这样做会产生下述的问题,就是如果法线变化太过剧烈的地方,描边轮廓会断开。


对于这种情况,一般的解决方法是需要对模型外扩使用的法线数据进行修改,需要对模型顶点的法线做平均运算然后写入切线数据之中。至于为什么要写到切线数据里,这是因为只有法线和切线数据会随着骨骼动画而改变。所以如果渲染的是有骨骼动画的角色,写入切线数据里就不用做额外处理,计算上简单一些。如果碰到了角色使用法线贴图或者各项异性材质这种需要原始切线数据的情况,那么可以先把平均法线转换到切线空间,再保存到UV或者顶点颜色上。
这一部分详细可以参考2173大佬的描边文章,大佬写了一个专门更改法线数据写入切线数据的编辑器
但是我这里使用的方法比较偷懒,直接使用了顶点方向和法线方向进行差值,并且多做一次模型轮廓朝向的判断,让轮廓可以被推向正确的方向


大致效果:(我把所有描边放宽了方便大家观察描边效果)


同时,关于顶点色控制描边颜色,可以通过上文中的 o.vertColor = v.vertColor.rgb来得到。但是由于MHY自己公开出的模型好像没有这一部分数据,所以我使用了一个近似的方法,即直接对漫反射纹理采样,并且与轮廓线预设的底色来进行混合,得到轮廓线的颜色。这样看起来不会太突兀


大致效果如下,我在这里选择的是灰色作为轮廓线底色


其实就这样直观来看,描边存在两个问题,第一个就是描边断裂,虽然我使用了顶点和法线方向的差值,但是在甘雨袖口这一块法线极端变化的位置,还是肉眼可见描边线断裂开来,所以使用编辑器工具去修改法线数据写入切线中还是十分重要的。
这里我使用一下大佬写的编辑器工具得出这个效果:可以看出描边确实比我偷懒的方法贴合


另一个问题是,随着摄像机的拉大,描边的宽度貌似也在变大,在原神里面,描边宽度是不随着摄像机的缩放而变化的。但是我这里其实不太能理解为什么要修复,因为近大远小我觉得没有毛病,2333333。所以我在自己的代码里面就把这个过程去掉了,如果想修复的话,还是可以参考上面2173大佬的文章进行更改,或者参照下方在默认管线中的用法


还有一些基于屏幕后处理的描边方法,这就放到以后接触的时候再说了!
NPR基础着色

要对角色和场景进行卡通渲染着色,就要先清楚怎么样才能让角色和场景看起来卡通,主要有以下三点:
图文摘自:
从零开始的卡通渲染

  • 减少色阶的数量


2. 冷暖色调的分离


在美术上根据颜色区分为暖色调和冷色调。在偏真实的光照计算中,往往只计算一个明暗关系,然后由光和物体的颜色决定最终效果。而卡通渲染则会根据明暗关系,为明面和暗面分配不同色调的颜色。比如一个暖色调的明面,配合一个冷色调的暗面。将色调拉开之后,显得更为卡通(在罪恶装备中会通过单独的贴图定义暗面色调,和明面色调做一些区分)
3. 对明暗区域的手绘控制


在手绘动画中为了好的画面效果,往往其明暗的分布并不是完全正确的。而在罪恶装备的处理中,是通过灯光方向,Threshold贴图和法线方向对光照进行手绘风格的控制的。
灯光方向控制:卡通渲染的角色在部分灯光方向下,可以有最佳的画面表现。有些时候这个灯光方向和场景灯光或者其他角色的灯光方向不一致。为了让每个角色都有最佳表现,最好每个角色都有一盏自己的灯光方向。甚至当角色转向时,这个灯光也跟着角色做一定程度的转向,来让角色有一个更好的光影表现
Threshold贴图控制:在《GUILTY GEAR Xrd》中将这张贴图称作ilmTexture


这张贴图有点类似于AO贴图,不过它是对光照计算的结果进行一些倾向性的修正。让一部分区域,比如角色脖子的部分更容易产生阴影来达到手绘风格的阴影效果。
法线方向控制:法线控制有两种方法,一般是直接编辑法线,达到想要的光照结果。一种是创建一个平滑的简单模型,然后将其法线传递到复杂的物体上,达到优化阴影的效果。
那么接下来我们一步步开始:
冷暖色调控制

首先,我们先简单的使用两个色阶,一个暖色,一个冷色,暖色部分着色光照面,冷色部分着色阴影面




可以得到一个最直接的着色结果


之后,我们使用一个平滑过渡函数函数SmoothStep对交界处做一个平滑:




平滑之后的大致结果如下:


当然,现在最常见的做法是使用一个Ramp贴图来对色阶进行控制,Ramp贴图记录了冷暖色调的过渡值,在MHY的官方模型里面它长这样


那么我可以简单的对Ramp贴图进行采样,这里需要注意的是,RampMap更像是一个索引贴图,所以是需要使用半兰伯特光照计算的结果去进行采样的


可以得到一个还算不错的结果


接下来简单加上一个高光:


接着,将这个材质和贴图直接融合到一起,(这里使用的均是MHY官方模型以及自带的贴图),直接贴出来的效果是下面这个样子的


ilmTexture(风格控制)

现在这个渲染的结果还有很多问题,首先,肌肤的部分是全黑色,因为我们还缺少肌肤贴图。其次,模型的任意地方都有高光,这样会显得我们的模型特别油腻,因此需要一张高光贴图来进行调整。在《GUILTY GEAR Xrd》中,使用的是成为ilmTexture的贴图对橘色明暗区域实现手绘风格的控制。其中,红色通道控制高光强度,蓝色通道控制高光范围,绿色通道控制漫反射的阴影阈值。但是,米哈游给出的官模中是没有对应的贴图的!所以,可能还原不出真实的效果,但是我们也有一些自己的方法去模拟一下ilmTexture
首先,使用MHY自带的肌肤贴图,并且结合自带的Skin-Ramp做一个肌肤着色。我这里将其当作了漫反射采样,Alpha通道为0的地方直接裁剪


对于模型油腻的问题,很大程度上是因为光照的反射计算出现在了一些不该出现的区域(比如皮肤上,或者不反光的衣服材质上面)。按理论上来说,我们需要一张高光贴图将高光控制在这些应该出现的区域,但是MHY开放出的模型里面我好像没有找到这一项,所以我只能从网上大佬们的资源中扒个有相对完整贴图的模型。
所以有请咱们的王小美同学!
我这里只让王小美同学的金属部分产生高光,高光金属贴图大概长下图这个样子


对这个贴图进行采样控制,如果没有采样到高光的区域,则简单的舍弃掉这一部分高光。
注:由于我这里高光贴图是另外找的,比对之后发现它的漫反射比原本衣服金属部分的漫反射颜色要深一些,所以我这里又做了一次颜色叠加混合


我们先将高光的GLOSS调整到0,并且去掉漫反射,让所有高光反射的区域显示出来,位置差不多就是这些


区域看着还行~,那么把漫反射啥的都加上


稍微有点感觉了
边缘光计算

在Toon Shading中,边缘光计算也是一个重点。因为边缘光可以更加凸显角色的轮廓部分,让角色的边缘更加明显,同时凸显手绘的效果,我搜集到的边缘光有两种做法。
普通的N/H dotV边缘光





在计算中我们可以看出,边缘光计算使用的是1减去法线点乘视线,这样在视角较大的大的边缘,就会获得比较大的光强度。同时需要注意的是,边缘光属于一种自发光,所以要和其它光照计算结果做相加。
这种边缘光可以看出一些缺点,首先是数值上特别难以调整,因为它和模型表面的法线相关,如果在一些比较平坦的硬表面上,就会出现一些错误的计算结果(比如甘雨腿部的边缘光,看起来就特别割裂)。而在罪恶装备中,使用了一张Mask去做边缘光修正。


基于屏幕空间的深度边缘光

以下内容部分来源于参考:
既然,常规的N/H/ DotV会出现一些问题,比如在平面表现下,边缘光区域会扩散出来造成不正确的边缘结果。因此,可以采用另一个相对稳定的方案,即使用基于屏幕空间深度的边缘光。
这个方案说起来和Back Facing描边法其实非常相似:我们把每一个点的位置向着法线方向位移一段距离,再根据位移后的坐标采样相机深度图,把深度和自己的深度做一个对比,如果差值大于一个阈值,那么这个地方就是边缘
另外,这里需要注意两个坑:
首先对 _ CameraDepthTexture做一些额外说明:这个变量是unity为我们自动返回的一个深度值,_CameraDepthTexture 在延迟渲染之中是可以直接得到的,而在前向渲染中我们需要给摄像机加一个脚本允许Unity获取深度值才可以使用


其次,渲染的目标必须要有ShadowCaster这个通道,目标的深度值才可以被写入深度缓冲区之中,因此这里我们使用一个最简单的ShadowCaster通道


至于为什么一定需要一个ShadowCaster的通道,我找到的比较好的解释在这里:
那么万事具备,我们来动手实现一下:


首先,我们需要得到表面法线,因为需要根据这个将采样深度图的位置向外推。然后这里我们需要得到屏幕空间下,也就是ViewPort下的采样点。这里的转换函数如下:


·有了采样点之后,我们就可以根据采样点和原本的深度进行一个比较,最后比较差异是否有超过我们预设的一个阈值


注:LinearEyeDepth 负责把深度纹理的采样结果转换到视角空间下的深度值,而另一个与它相似的函数Linear01Depth 则会返回一个范围在[0, 1]的线性深度值。这两个函数内部使用了内置的_ZBufferParams变量来得到远近裁剪平面的距离。


这个边缘光效果看上去就已经好很多了
面部阴影矫正

上方贴出的效果中,可以看到面部的光照结果是十分不理想的


因为面部模型法线分布的原因,当光照和面部形成一定角度的时候,会形成十分难看的光照。所以这个是需要我们进行调整的。
面部阴影的调整参考自:
这里我也使用了一张面部阈值贴图,让模型的脸部可以有区域光而不是单独的方向高光,使用的面部贴图如下


这里的大致计算流程和大佬做的差不多,需要注意的是传入模型方向我这里偷懒了,直接调整了王小美同学的朝向,但实际应该在C#中实时传入才对。
我们需要得到模型和光照的方向,判断光照和阴影的区域,同时面部阴影进行采样,计算得到应该的衰减结果


优化后的面部阴影如下,看上去就已经比较丝滑了




还有一个小问题是,你会发现头发的阴影和面部阴影是交错的,所以我们需要对阴影部分也做出修正,让面部阴影和头发阴影一起被改变。这里采用的是雪羽大佬的方案,使用一个旋转矩阵去偏移光照,人为的将阴影修正到同步,同时对我们采样的结果做一个平滑


于是就有了如下效果:


Bloom效果

众所周知!卡通渲染的画面一大风格就是明亮!所以Bloom效果是不可缺少的一部分。由于网上大部分都有关于Bloom实现的教程,Unity自己也有插件。但是我这里选用了冯乐乐女神《Unity Shader入门精要》里面的做法。(因为之前写过,所以这里基本上是照搬)
Bloom效果的实现步骤如下:


BloomShader中有四个Pass:第一个Pass用于提取图像中比较亮的区域,第二个第三个分别用于高斯模糊的水平和垂直卷积,第四个Pass对两张图进行采样进行融合。
(具体的看后面的代码吧,或者请见冯乐乐大佬的书)
我这里把数值稍微拉大了一些,确认效果。


其它细节

衣服阴影

在大佬做的配布中还发现了这个东西:一张衣服阴影的贴图


我这里选择雪羽大佬使用的阴影方案,对衣服的阴影做固定处理


实当调整一下阴影参数,可以观察到绘制出来的阴影,在光照下会被淡化






最终效果:



https://www.zhihu.com/video/1450931356221620224
一些感悟

首先呢,这个还原我本人其实没有特别满意,只能算是1.0版本吧,因为很多贴图都是从网上扒的,再加上自己的审美啊,技术局限等因素还是差了点味道。不过也算是了结了自己之前的一个小小心愿
做的过程中感触比较大的是,虽然Shader里面有很多的小Trick,但很多很多的工作还是美术的活,真正需要程序的部分其实并不多。但是看着从一个完全没法看的模型一步步还原到有点那个感觉,还是挺兴奋的
接下来的话估计这边的事情要放一放了,毕竟要毕业了,毕设和公司的MINI在即,得抓紧时间造轮子才行。废话就到这里吧,后面的部分是一些核心代码
代码

核心NPRshader

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

// Upgrade NOTE: replaced 'defined _FACESHADOW_MAP' with 'defined (_FACESHADOW_MAP)'

Shader "Custom/NPRTest"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {} //主贴图
        _RampTex ("Ramp Texture",2D) = "white"{}//Ramp贴图
        _MetalTex("Metal Texture",2D) = "white"{} //金属高光贴图
        _LightShadowMap("Shadow Texture",2D) = "white"{}//阴影纹理贴图

        _MainColor("Main Color",Color) = (1,1,1) //主色调
        _ShadowColor("Shadow Color",Color) = (0.7,0.7,0.8) //冷色调
        _ShadowRange("Shadow Range",Range(0,1)) = 0.5 //占比控制
        _ShadowSmooth("Shadow Smooth",Range(0,1)) = 0.2 //交接平滑度

        [Space(10)]
        _FaceShadowMap("Face ShadowMap",2D) = "white"{}//面部阴影贴图
        _FaceShadowMapPow("Face ShadowMap pow",Range(0.15,0.3)) = 0.15//阴影变化权重
        _FaceShadowOffset("Face ShadowOffset",Range(0,1)) = 0//面部阴影偏移,防止产生跳变
        [Toggle]_IgnoreLightY("WhetherFixLightY",float) = 0

        [Space(10)]
        _SpecularGloss("Specular Gloss",Range(0,128)) = 32
        _SpecularColor("Speuclar Color",Color) = (0.7,0.7,0.8)

        [Space(10)]
        _RimColor("Rim Color",Color) = (1,1,1,1) //边缘光
        _RimMax("Rim Max",Range(0,1)) = 0.5//控制字段
        _RimMin("Rim Min",Range(0,1)) = 0.5
        _RimSmooth("Rim Smooth",Range(0,1)) = 0.2 //边缘光平滑度

        [Space(10)]
        _OutLineWidth("Outline Width",Range(0,2)) = 0.24
        _OutLineColor("Outline Color",Color) = (0.5,0.5,0.5,1)
        _Factor("Outline Factor",Range(0,1)) = 0.5

        [Space(10)]
        _OffsetMul("_RimWidth",Range(0,0.1)) = 0.012
        _Threshold("_Threshold",Range(0,1)) = 0.09

    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            Tags {"LightMode" = "ForwardBase"}

            Cull Back
        
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #pragma target 3.5

            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            #pragma shader_feature _FACESHADOW_MAP

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _RampTex;
            float4 _RampTex_ST;
            sampler2D _MetalTex;
            float4 _MetalTex_ST;
            sampler2D _FaceShadowMap;
            float4 _FaceShadowMap_ST;
            sampler2D _LightShadowMap;

            sampler2D _CameraDepthTexture;

            // UNITY_DECLARE_TEX2D(_FaceShadowMap);
            half _FaceShadowOffset;
            half _FaceShadowMapPow;
            float _IgnoreLightY;
            
            half3 _MainColor;
            half3 _ShadowColor;
            half4 _RimColor;

            half _RimMax;
            half _RimMin;
            half _RimSmooth;
            half _ShadowRange;
            half _ShadowSmooth;

            half _OffsetMul;
            half _Threshold;

            half _SpecularGloss;
            half4 _SpecularColor;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
                float4 vertexColor : Color;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                float3 positionVS : TEXCOORD3;
            };

            float4 TransformClipToViewPortPos(float4 positionCS)
            {
                float4 o = positionCS * 0.5f;
                o.xy = float2(o.x,o.y*_ProjectionParams.x) + o.w;
                o.zw = positionCS.zw;
                return o/o.w;
            }

            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv,_MainTex);
                o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.positionVS = UnityObjectToViewPos(v.vertex);
                return o;
            }

            half4 frag(v2f i) : SV_TARGET
            {
                half4 col = 1;
                half4 mainTex = tex2D(_MainTex,i.uv);
                half4 metalTex = tex2D(_MetalTex,i.uv);
                half4 LightMapShadow = tex2D(_LightShadowMap,i.uv);

                half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                half3 worldNormal = normalize(i.worldNormal);
                half3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                half halfLambert = dot(worldNormal,worldLightDir)*0.5 + 0.5;

                //漫反射计算部分
                // float3 ramp = tex2D(_RampTex, float2(halfLambert, halfLambert)).rgb;//上面这样采样会出现不正常的高光区域
                float3 ramp = tex2D(_RampTex,float2(saturate(halfLambert-_ShadowRange),0.5));
                float3 rampStart = tex2D(_RampTex, float2(0,0)).rgb;
                half rampNum = smoothstep(0,_ShadowSmooth,halfLambert - _ShadowRange);
                half3 RampColor = lerp(rampStart,ramp,rampNum);

                //阴影层级计算
                //如果SFactor = 0,ShallowShadowColor为一级阴影色,否则为BaseColor
                float SWeight = (LightMapShadow.g * RampColor.r + halfLambert) * 0.5 + 1.125;
                float SFactor = floor(SWeight - _ShadowRange);
                half3 ShallowShadowColor = SFactor * _MainColor.rgb + (1-SFactor) * _ShadowColor.rgb;
                half rampS = smoothstep(0,_ShadowSmooth,halfLambert - _ShadowRange);
                ShallowShadowColor = lerp(_ShadowColor,_MainColor,rampS);

                half3 diffuse = RampColor;
                diffuse *= LightMapShadow.a ==0 ? 1 : ShallowShadowColor;
                diffuse *= mainTex.a == 0 ? 1 : mainTex.rgb;//用三目运算符替代IF执行裁剪操作
                // half ramp = smoothstep(0,_ShadowSmooth,halfLambert - _ShadowRange);
                // half3 diffuse = halfLambert > _ShadowRange ? _MainColor : _ShadowColor;//在这里抉择冷色调还是暖色调
               
                //高光计算部分
                half3 halfDir = normalize(worldLightDir + viewDir);
                fixed3 specularColor = _SpecularColor.rgb*pow(max(0,dot(worldNormal,halfDir)),_SpecularGloss);
                fixed3 specular = metalTex.a == 0 ? 0 : specularColor * metalTex.rgb;

                // 边缘光计算(视向量做法)
                // half f = 1.0 - saturate(dot(viewDir,worldNormal));
                // half rim = smoothstep(_RimMin,_RimMax,f);
                // rim = smoothstep(0,_RimSmooth,rim);
                // half3 rimColor = rim * _RimColor.rgb * _RimColor.a;

                //边缘光计算部分(屏幕空间深度边缘光)
                float3 normalWS = i.worldNormal;
                float3 normalVS = UnityWorldToViewPos(normalWS);
                float3 positionVS = i.positionVS;
                float3 samplePositionVS = float3(positionVS.xy + normalVS.xy*_OffsetMul,positionVS.z);
                float4 samplePositionCS = UnityViewToClipPos(samplePositionVS);
                float4 samplePositionVP = TransformClipToViewPortPos(samplePositionCS);

                float depth = i.pos.z /i.pos.w;
                float linearEyeDepth = LinearEyeDepth(depth);
                float offsetDepth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture,samplePositionVP));
                float linearEyeOffsetDepth = LinearEyeDepth(offsetDepth);
                float depthDiff = linearEyeOffsetDepth - linearEyeDepth;
                float rimIntensity = step(_Threshold,depthDiff);
                half3 rimColor = rimIntensity * _RimColor.rgb * _RimColor.a;

                #if defined (_FACESHADOW_MAP) //面部阴影修正
                    float lightDataLeft = tex2D(_FaceShadowMap,i.uv);
                    float lightDataRight = tex2D(_FaceShadowMap,float2(1 - i.uv.x,i.uv.y));
                    float2 lightData = float2(lightDataRight,lightDataLeft);
                    float3 Fronts = float3(0,0,1);
                    float3 Right = float3(1,0,0);

                    float sinx = sin(_FaceShadowOffset);
                    float cosx = cos(_FaceShadowOffset);
                    float2x2 rotationOffset = float2x2(cosx,-sinx,sinx,cosx);
                    float2 lightDir = mul(rotationOffset,worldLightDir.xz);
                    lightData = pow(abs(lightData), _FaceShadowMapPow);

                    float FrontL = dot(normalize(Fronts.xz), normalize(lightDir));
                    float RightL = dot(normalize(Right.xz), normalize(lightDir));
                    RightL = -(acos(RightL)/3.14159265 - 0.5)*2;
                    float lightAttenuation = (FrontL > 0) * min(
                        (lightData.r > RightL),
                        (lightData.g > -RightL)
                    );


                    half3 FaceColor = lerp(mainTex.rgb*rampStart.rgb*_ShadowColor.rgb,  mainTex.rgb*ramp.rgb*_SpecularColor, lightAttenuation);
                    col.rgb = FaceColor;
                #else //非面部阴影的计算区域
                    col.rgb = (diffuse + specular + rimColor) * _LightColor0.rgb;
                    col.a = mainTex.a;
                #endif
                // half4 test = half4(0,rampTex.g,0,255);
                return col;

            }


            ENDCG
        }

        Pass
        {
            Tags {"LightMode"="ForwardBase"}

            //开启正向剔除
            Cull Front

            CGPROGRAM
        
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;//尝试用贴图采样轮廓线颜色

            half _OutLineWidth;
            half4 _OutLineColor;
            float _Factor;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
                float4 vertColor : COLOR;
                float4 tangent : TANGENT;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 vertColor : TEXCOORD1;
            };

            v2f vert(a2v v)
            {
                v2f o;
                float3 pos = normalize(v.vertex.xyz);
                // float3 normal = normalize(v.normal);
                float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.tangent.xyz);

                //点积是为了确定顶点对于几何中心的指向,判断此处的顶点是位于模型的凹处还是凸处
                float D = dot(pos,normal);
                //矫正顶点的方向值,判定是否是轮廓线
                pos *= sign(D);
                //描边的朝向插值,决定是偏向法线方向还是顶点方向
                pos = lerp(normal,pos,_Factor);
                //将顶点往指定方向挤出
                v.vertex.xyz += pos*_OutLineWidth;

                o.pos = UnityObjectToClipPos(v.vertex);
                o.vertColor = v.vertColor.rgb;//如果有顶点色才使用
                o.uv = TRANSFORM_TEX(v.uv,_MainTex);

                return o;

            }

            fixed4 frag(v2f i) : SV_TARGET
            {
                i.vertColor = tex2D(_MainTex,i.uv).rgb;
                return fixed4(_OutLineColor*i.vertColor,0);
            }

            ENDCG
        }

        Pass
        {
            Tags {"LightMode" = "ShadowCaster"}

            CGPROGRAM

            #pragma target 3.0

            #pragma vertex Shadowvert
            #pragma fragment ShadowFrag

            #include "UnityCG.cginc"

            struct VertexData{
                float4 position : POSITION;
            };

            float4 Shadowvert(VertexData v) : SV_POSITION{
                return UnityObjectToClipPos(v.position);
            }

            half4 ShadowFrag() : SV_TARGET{
                return 0;
            }

            ENDCG

        }
    }

    CustomEditor "NPRShaderGUI"
}
Shader的UI扩展

using UnityEngine;
using UnityEngine.Rendering;
using UnityEditor;

public class NPRShaderGUI : ShaderGUI
{
    Material target;

    //让所有方法都能访问这两个属性
    MaterialEditor materialEditor;
    MaterialProperty[] properties;

    //第一个参数是对MaterialEditor的引用。该对象管理当前选定材质的检查器。第二个属性是包含该材质属性的数组
    public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
    {
        //给这些属性赋值
        this.target = materialEditor.target as Material;
        this.materialEditor = materialEditor;
        this.properties = properties;
        DoMain();//主帖图部分
    }

    void DoMain()
    {
        GUILayout.Label("Main Maps",EditorStyles.boldLabel);
        MaterialProperty mainTex = FindProperty("_MainTex");
        materialEditor.TexturePropertySingleLine(MakeLabel(mainTex,"Albedo(RGB)"),mainTex,FindProperty("_MainColor"));//将这组件绑定到可视化编辑器上面
        materialEditor.TextureScaleOffsetProperty(mainTex);//这个函数让编辑器显示Tiling和Offset属性
        DoRamp();
        DoMetal();
        DoLightMapShadow();
        DoFaceShadow();

        DoSpecular();
        DoShadow();
        DoRimColor();
        DoOutLine();
    }

    void DoRamp()
    {
        GUILayout.Space(10);
        GUILayout.Label("Ramp Maps",EditorStyles.boldLabel);
        MaterialProperty RampMap = FindProperty("_RampTex");
        materialEditor.TexturePropertySingleLine(MakeLabel(RampMap,"RampMap"),RampMap);
    }

    void DoMetal()
    {
        GUILayout.Space(10);
        GUILayout.Label("Metal Maps",EditorStyles.boldLabel);
        MaterialProperty MetalMaps = FindProperty("_MetalTex");
        materialEditor.TexturePropertySingleLine(MakeLabel(MetalMaps,"MetalMap"),MetalMaps);
    }

    void DoRimColor()
    {
        GUILayout.Space(10);
        GUILayout.Label("Rim Control",EditorStyles.boldLabel);
        // MaterialProperty RimColor = FindProperty("_RimColor");
        // materialEditor.ColorProperty(RimColor,"RimColor");

        // MaterialProperty RimMax = FindProperty("_RimMax");
        // materialEditor.RangeProperty(RimMax,"RimMax");

        // MaterialProperty RimMin = FindProperty("_RimMin");
        // materialEditor.RangeProperty(RimMin,"RimMin");

        MaterialProperty RimColor = FindProperty("_RimColor");
        materialEditor.ColorProperty(RimColor,"RimColor");

        MaterialProperty OffsetMul = FindProperty("_OffsetMul");
        materialEditor.RangeProperty(OffsetMul,"_RimWidth");

        MaterialProperty Threshold = FindProperty("_Threshold");
        materialEditor.RangeProperty(Threshold,"_Threshold");
    }

    void DoOutLine()
    {
        GUILayout.Space(10);
        GUILayout.Label("OutLine Control",EditorStyles.boldLabel);
        
        MaterialProperty OutLineWidth = FindProperty("_OutLineWidth");
        materialEditor.RangeProperty(OutLineWidth,"OutLineWidth");

        MaterialProperty OutLineColor = FindProperty("_OutLineColor");
        materialEditor.ColorProperty(OutLineColor,"OutLineColor");

        MaterialProperty Factor = FindProperty("_Factor");
        materialEditor.RangeProperty(Factor,"Factor");
    }

    void DoSpecular()
    {
        GUILayout.Space(10);
        GUILayout.Label("Specular Control",EditorStyles.boldLabel);

        MaterialProperty SpecualrColor = FindProperty("_SpecularColor");
        materialEditor.ColorProperty(SpecualrColor,"SpecualrColor");

        MaterialProperty SpecularGloss = FindProperty("_SpecularGloss");
        materialEditor.RangeProperty(SpecularGloss,"SpecularGloss");

    }

    void DoShadow()
    {
        GUILayout.Space(10);
        GUILayout.Label("Shadow Control",EditorStyles.boldLabel);

        MaterialProperty ShadowColor = FindProperty("_ShadowColor");
        materialEditor.ColorProperty(ShadowColor,"ShadowColor");

        MaterialProperty ShadowRange = FindProperty("_ShadowRange");
        materialEditor.RangeProperty(ShadowRange,"ShadowRange");

        MaterialProperty ShadowSmooth = FindProperty("_ShadowSmooth");
        materialEditor.RangeProperty(ShadowSmooth,"ShadowSmooth");
    }

    void DoFaceShadow()
    {
        GUILayout.Space(10);
        GUILayout.Label("FaceShadowMap",EditorStyles.boldLabel);
        MaterialProperty map = FindProperty("_FaceShadowMap");
        EditorGUI.BeginChangeCheck();//检查方法是否有更改
        materialEditor.TexturePropertySingleLine(
            MakeLabel(map,"FaceShadow"),map,
            map.textureValue? FindProperty("_FaceShadowOffset") : null//有贴图的时候隐藏滑块
            );
        if(map.textureValue)
        {
            MaterialProperty FaceShadowMapPow = FindProperty("_FaceShadowMapPow");
            materialEditor.RangeProperty(FaceShadowMapPow,"_FaceShadowMapPow");

            MaterialProperty WhetherFixLight = FindProperty("_IgnoreLightY");
            materialEditor.FloatProperty(WhetherFixLight,"_IgnoreLightY");
        }
        if(EditorGUI.EndChangeCheck()){
            SetKeyword("_FACESHADOW_MAP", map.textureValue);//将面部阴影定义为关键字
        }
    }
   
    void DoLightMapShadow()
    {
        GUILayout.Space(10);
        GUILayout.Label("LightShadow",EditorStyles.boldLabel);
        MaterialProperty map = FindProperty("_LightShadowMap");
        materialEditor.TexturePropertySingleLine(MakeLabel(map,"LightShadow"),map);
    }


    //工具函数部分
    MaterialProperty FindProperty(string name)
    {
        return FindProperty(name,properties);
    }

    static GUIContent staticLabel = new GUIContent();//替换文本和工具提示函数
    static GUIContent MakeLabel (string text,string tooltip = null){
        staticLabel.text = text;
        staticLabel.tooltip = tooltip;
        return staticLabel;
    }

    static GUIContent MakeLabel(MaterialProperty property,string tooltip = null)
    {//直接从属性中得到文本和提示
        staticLabel.text = property.displayName;
        staticLabel.tooltip = tooltip;
        return staticLabel;
    }

    void SetKeyword (string keyword,bool state){
        if(state){//foreach遍历所有的目标材质,不然只会有第一个材质有相应的操作
            foreach(Material m in materialEditor.targets)
            {
                m.EnableKeyword(keyword);//使用该方法将关键字添加到着色器中
            }
        }
        else{
            foreach(Material m in materialEditor.targets)
            {
                m.DisableKeyword(keyword);//使用该方法将关键字添加到着色器中
            }
        }
    }

    bool IsKeywordEnabled(string keyword){
        return target.IsKeywordEnabled(keyword);
    }

    void RecordAction(string label){
        materialEditor.RegisterPropertyChangeUndo(label);//支持撤销操作
    }
}
BloomShader

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/Bloom"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Bloom ("Bloom(RGB)",2D) = "black" {}
        _LuminanceThreshold("Luminance Threshold",Float) = 0.5
        _BlurSize("Blur Size",Float) = 1.0
    }
    SubShader
    {
        CGINCLUDE

        sampler2D _MainTex;
        float4 _MainTex_ST;
        float4 _MainTex_TexelSize;
        sampler2D _Bloom;
        float _LuminanceThreshold;
        float _BlurSize;

        ENDCG

        ZTest Always Cull Off ZWrite Off
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass //第一个Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed luminance(fixed4 color){//采样亮度值
                return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                fixed val = clamp(luminance(col) - _LuminanceThreshold,0.0,1.0);

                return col * val;
            }
            ENDCG
        }

        Pass //第二个Pass
        {
            CGPROGRAM

            #pragma vertex vertBlurVertical
            #pragma fragment fragBlur

            #include "UnityCG.cginc"

            struct v2f{
                float4 pos : SV_POSITION;
                half2 uv[5] : TEXCOORD0;
            };

            v2f vertBlurVertical(appdata_img v){
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                half2 uv = v.texcoord;

                o.uv[0] = uv;
                o.uv[1] = uv + float2(0.0,_MainTex_TexelSize.y * 1.0) * _BlurSize;
                o.uv[2] = uv - float2(0.0,_MainTex_TexelSize.y * 1.0) * _BlurSize;
                o.uv[3] = uv + float2(0.0,_MainTex_TexelSize.y * 2.0) * _BlurSize;
                o.uv[4] = uv - float2(0.0,_MainTex_TexelSize.y * 2.0) * _BlurSize;

                return o;
            }

            fixed4 fragBlur(v2f i) : SV_Target{
                float weight[3] = {0.4026,0.2442,0.0545};

                fixed3 sum = tex2D(_MainTex,i.uv[0]).rgb * weight[0];

                for(int it = 1;it < 3;it++){
                    sum += tex2D(_MainTex,i.uv[it*2 - 1]).rgb * weight[it];
                    sum += tex2D(_MainTex,i.uv[it*2]).rgb * weight[it];
                }

                return fixed4(sum,1.0);
            }

            ENDCG
        }

        Pass //第三个Pass
        {
            CGPROGRAM

            #pragma vertex vertBlurHorizontal
            #pragma fragment fragBlur

            struct v2f{
                float4 pos : SV_POSITION;
                half2 uv[5] : TEXCOORD0;
            };

            #include "UnityCG.cginc"

            v2f vertBlurHorizontal(appdata_img v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
               
                half2 uv = v.texcoord;
               
                o.uv[0] = uv;
                o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
                o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
                o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
                o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
                        
                return o;
            }

            fixed4 fragBlur(v2f i) : SV_Target {
                float weight[3] = {0.4026, 0.2442, 0.0545};
               
                fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
               
                for (int it = 1; it < 3; it++) {
                    sum += tex2D(_MainTex, i.uv[it*2-1]).rgb * weight[it];
                    sum += tex2D(_MainTex, i.uv[it*2]).rgb * weight[it];
                }
               
                return fixed4(sum, 1.0);
            }

            ENDCG
        }  

        Pass //第四个Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragBloom
            
            #include "UnityCG.cginc"


            struct v2f
            {
                float4 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
            };

            v2f vert(appdata_img v){
                v2f o;

                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv.xy = v.texcoord;
                o.uv.zw = v.texcoord;

                // #if UNITY_UV_STARTS_AT_TOP
                //     o.uv.w = 1.0 - o.uv.w;
                // #endif

                return o;
            }

            fixed4 fragBloom(v2f i) : SV_Target{
                return tex2D(_MainTex,i.uv.xy) + tex2D(_Bloom,i.uv.zw);
            }

            ENDCG
        }
    }
    Fallback "Diffuse"
}
Bloom后处理脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode,ImageEffectAllowedInSceneView]
public class Bloom : MonoBehaviour
{
    [SerializeField] Material material;
    [SerializeField] Shader bloomShader;   

    [Range(0,4)]
    public int iterations = 3;
   
    [Range(0.2f,3.0f)]
    public float blurSpread = 0.6f;

    [Range(1,8)]
    public int downSample = 2;

    [Range(0.0f,4.0f)]
    public float luminanceThreshold = 0.6f;//提取区域的阈值大小,为了HDR所以范围是0-4

    const string m_bloomShaderID = "Custom/Bloom";

    private void Awake()
    {
        bloomShader = Shader.Find(m_bloomShaderID);
        material = new Material(bloomShader);
    }
    private void Start() {
        Camera.main.depthTextureMode = DepthTextureMode.Depth;//打开当前摄像机的深度图
    }
    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if(material != null){
            material.SetFloat("_LuminanceThreshold",luminanceThreshold);

            int rtW = source.width / downSample;
            int rtH = source.height / downSample;

            RenderTexture buffer0 = RenderTexture.GetTemporary(rtW,rtH,0);//从显存拿一个缓存区
            buffer0.filterMode = FilterMode.Bilinear;

            Graphics.Blit(source,buffer0,material,0);

            for(int i=0;i<iterations;i++){//迭代次数
                material.SetFloat("_BlurSize",1.0f + i*blurSpread);

                RenderTexture buffer1 = RenderTexture.GetTemporary(rtW,rtH,0);

                Graphics.Blit(buffer0,buffer1,material,1);

                RenderTexture.ReleaseTemporary(buffer0);//释放显存
                buffer0 = buffer1;
                buffer1 = RenderTexture.GetTemporary(rtW,rtH,0);

                Graphics.Blit(buffer0,buffer1,material,2);

                RenderTexture.ReleaseTemporary(buffer0);
                buffer0 = buffer1;
            }

            material.SetTexture("_Bloom",buffer0);
            Graphics.Blit(source,destination,material,3);

            RenderTexture.ReleaseTemporary(buffer0);
        }else{
            Graphics.Blit(source,destination);
        }
    }

}

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2021-12-18 05:41 | 显示全部楼层
大佬,牛逼
发表于 2021-12-18 05:45 | 显示全部楼层
虽然看不懂,但大受震撼
发表于 2021-12-18 05:52 | 显示全部楼层
虽然我看不懂,但我大受震撼
发表于 2021-12-18 05:56 | 显示全部楼层
最终输出的时候考虑下加入抗锯齿呢?对整体感受影响挺大的。
发表于 2021-12-18 05:58 | 显示全部楼层
我也看不懂,同样大受震撼。请问是需要先下载mmd模型然后导入unity之后开始的么?
发表于 2021-12-18 05:58 | 显示全部楼层
改天我试试!其实这个问题我之前想过,但是感觉会和描边功能冲突 暂时还没有进行深入研究
发表于 2021-12-18 06:06 | 显示全部楼层
对的,中间需要借助blender的MMD TOOLS转一次文件格式,然后重新校准材质贴图
发表于 2021-12-18 06:16 | 显示全部楼层
谢谢大佬,待我尝试学习一下
发表于 2021-12-18 06:24 | 显示全部楼层
虽然我看不懂 但是不妨碍我喜欢你捏
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-6 08:36 , Processed in 0.103513 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表