跳转到主要内容
Computer Graphics Jul 13, 2022 2 tags

第14章 非真实渲染

第14章 非真实渲染

cover

概要:

卡渲是在一般的光照模型上,如Lambert、Phong…进行改造,记录这些光照模型的结果,作为uv的一个坐标采样RampTex;或使用 色调映射技术(Tonal Art Map,TAM),作为不同风格、不同灰度的纹理的权值…以此来体现有风格的明暗面

  • 一般卡渲:
  • 素描效果:

第十四章 · 卡渲:

是在一般的光照模型上,如Lambert、Phong…进行改造,记录这些光照模型的结果,作为uv的一个坐标采样RampTex;或使用 色调映射技术(Tonal Art Map,TAM),作为不同风格、不同灰度的纹理的权值…以此来体现有风格的明暗面

14.1卡通风格的渲染

实现卡通渲染的方式之一:基于色调着色技术(tone-based shading)

  • 使用漫反射系数对一张一维纹理RampTex进行采样,以控制漫反射的色调
  • 对模型进行描边

14.1.1 渲染轮廓线

《Real Time Rendering, third edition》将描边方法分为5种类型:

  1. 基于观察角度表面法线渲染(类似于Fresnel):使用视角方向和表面法线的点乘来得到轮廓信息特点:快速(一个Pass即可),但效果不好
  2. **过程式几何轮廓渲染:**第一个Pass渲染背面,并将顶点放大,让其轮廓显示;第二个Pass渲染正面的面片。优点:快速有效、适用于绝大多数表面平滑的模型缺点:不适合类似于立方体这样平整的模型
  3. 基于图像处理的轮廓线渲染(第12、13章的边缘检测就属于这一类别)优点:适用于任何种类的模型缺点:一些深度和法线变化很小的轮廓无法被检测,如桌子上的纸张
  4. 基于轮廓边检测的轮廓线渲染:检测一条边为轮廓边的公式:检测 这条边相邻的两个三角形面片是否一个朝正面,一个朝背面即(n_0 \cdot v >0) ≠ (n_1 \cdot v >0)  ,其中n_0 和n_1  是2相邻三角形面片的法向,  v是视角到改边任意顶点的方向检测过程由 **几何着色器(Geometry Shader)**完成优点:可以渲染出独特的轮廓线,找到精确的轮廓边缺点:逐帧单独提取轮廓,在帧与帧之间会有跳跃性
  5. 混合上边的方法: 本节使用法2(过程式几何轮廓渲染):使用两个Pass
  • Pass1:使用轮廓线颜色渲染整个背面面片,并在视角空间下把模型顶点朝法向方向向外扩张一段距离,以此让背部轮廓线可见。代码如下: viewPos = viewPos + viewNormal * _Outline;

  • **Pass1改进:**直接使用法线对顶点扩展,对于内凹的模型,可能出现背面面片遮住正面面片的情况。为了尽可能防止出现这种情况,进行以下改进: 这样的好处:扩展的背面更加扁平化,从而降低了挡住正面面片的可能性

viewNormal.z = -0.5; viewNormal = normalize(viewNormal); viewPos = viewPos + viewNormal * _Outline;

14.1.2 添加高光

卡渲的高光往往是一块分界明显的色块

将之前的 Blinn-Phong模型进行改造,将值和一个阈值进行比较,如果小于阈值,则高光反射系数为0,反之为1(和阈值的做比较可以用 step函数)

1.step函数: 参数2 > 参数1 时,输出1,反之输出0

float spec = dot(worldNormal , worldHalfDir);	//法向点乘半角方向
spec = step(threshold , spec);		//和阈值做比较

此方法的缺点,从0到1会有很大锯齿

2.方法改进:lerp+smoothstep

**smoothstep(a , b , x)函数:**当 a<x<b时,输出值在(0,1)之间平滑曲线过渡;其他情况输出0或者1

本例代码:

w取很小的值,这里用相邻像素之间的近似导数值算出w,用 fwidth函数 实现

**fwidth函数 :**计算屏幕空间相邻两像素之间某一值的插值

**当年清风:常用Shader内置函数及使用总结(持续更新)**51 赞同 · 4 评论文章

关于fwidth函数的示例

当spec-threshold 很小时,即处于轮廓边缘,则进行平滑过渡

float spec = dot(worldNormal , worldHalfDir);	//法向点乘半角方向
spec = lerp(0 , 1 , smoothstep(-w, w, spec - threshold));	//其实把lerp去了也可以

14.1.3 实现

Pass1:渲染背面,将模型顶点沿着法线放大,渲染出描边

Pass2:正常的卡渲

  • **Diff:**用RampTex
  • Spec:用色块,smoothstep函数作用 bulling-Phong模型,smoothstep函数的w值,用fwidth(bulling) 计算出
  • 本次是对之前光照模型的回顾,尤其是shadow的实现不要忘了:头文件、Tags…
Shader "Unlit/C14_ToonShading"
{
    Properties
    {
        _Color ("Color Tint" ,Color) = (1.0, 1.0, 1.0, 1.0)
        _MainTex ("MainTex", 2D) = "white" {}
        _RampTex ("RampTex", 2D) = "white" {}
        _OutLine ("OutLine", Range(0.0, 1.0)) = 0.1
        _OutLineCol ("OutLineCol", Color) = (0.0, 0.0, 0.0, 1.0)
        _SpecularCol ("SpecularCol", Color) = (1.0, 1.0, 1.0, 1.0)
        _SpecularScale ("SpecularScale", Range(0, 0.1)) = 0.01      //高光反射时的阈值
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        //【Pass1 渲染背面,得到轮廓】
        Pass
        {
            NAME "OUTLINE"
            Cull Front      //关闭正面渲染

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            half _OutLine;
            half4 _OutLineCol;

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                float4 posVS = float4(UnityObjectToViewPos(v.vertex) , 1.0);      // 将顶点变换到视角空间
                float3 nDirVS = mul((float3x3)UNITY_MATRIX_IT_MV , v.normal);   // 将法线变换到视角空间
                nDirVS.z = -0.5;    // 法线z值 设为常数
                posVS = posVS + float4(normalize(nDirVS) , 0.0) * _OutLine;     // 改变顶点位置
                o.pos = UnityViewToClipPos(posVS);      // 将顶点变换到裁剪空间
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                return _OutLineCol;
            }
            ENDCG
        }


        //【Pass2 卡渲正面】
        Pass
        {
            Tags {"LightMode" = "ForwardBase"}  // 使用Unity的光照信息
            Cull Back  

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"       //阴影所需
            #include "AutoLight.cginc"      //阴影所需
            #pragma multi_compile_fwdbase_fullshadows    //让Shader的光照变量可以被正确赋值,并能产生阴影

            half4 _Color;
            sampler2D _MainTex;
            sampler2D _RampTex;
            half4 _SpecularCol;
            half _SpecularScale;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv0    : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv0 : TEXCOORD0;
                float3 nDirWS : TEXCOORD1;
                float3 posWS  : TEXCOORD2;
                LIGHTING_COORDS(3, 4)        //阴影
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv0 = v.uv0;
                o.nDirWS = UnityObjectToWorldNormal(v.normal);
                o.posWS = mul(unity_ObjectToWorld, v.vertex).xyz;
                TRANSFER_VERTEX_TO_FRAGMENT(o)  //阴影
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                //【贴图采样】
                half4 var_MainTex = tex2D(_MainTex, i.uv0);
                //【向量准备】
                half3 nDirWS = normalize(i.nDirWS);
                half3 lDirWS = normalize(UnityWorldSpaceLightDir(i.posWS));
                half3 vDirWS = normalize(UnityWorldSpaceViewDir(i.posWS));
                half3 hDirWS = normalize(vDirWS + lDirWS);
                //【Ambient】
                half3 albedo = (_Color * var_MainTex).rgb;
                half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;      // 环境光
                //【Shadow】
                half shadow = LIGHT_ATTENUATION(i);
                //【Diff】
                half halfLambert = dot(nDirWS, lDirWS) * 0.5 + 0.5;
                half4 var_RampTex = tex2D(_RampTex, float2(halfLambert * shadow, 0.2));     // shadow * halflambert 再采样,使得阴影是RampTex的暗面
                half3 diffuse = var_RampTex.rgb * albedo * _LightColor0.rgb;
                //【Spec】
                half bulling_withoutMax0 = dot(nDirWS , lDirWS);
                half w = fwidth(bulling_withoutMax0) * 2.0;     // 高光边缘处,值越大
                half3 specular = _SpecularCol.rgb  * smoothstep(-w, w, bulling_withoutMax0 + _SpecularScale -1) ;        //-1是为了缩小高光点
                //【最终输出】
                half3 finalCol = diffuse + specular + ambient;
                return half4(finalCol, 1.0);
            }
            ENDCG
        }

    }
    Fallback "Diffuse"
}

最终效果

14.2 素描风格的渲染

微软研究院发表了一篇关于素描风格的渲染文章,通过提前生成的素描纹理实现素描风格渲染,这些素描纹理组成了一个 色调艺术映射(Tonal Art Map,TAM)

素描纹理:

  • 从左到右,纹理的笔触逐渐增多,用于模拟不同光照下的漫反射
  • 从上到下,每张纹理的多级渐变纹理(mipmaps),此mipmaps不是简单对上一级纹理进行降采样,而是需要保持笔触之间的间隔,以便得到更真实的素描效果 本节课,我们只使用6张素描纹理,不考虑mipmaps

14.2.1实现:

  • Properties:
  • 用UsePass调用边缘线Pass
  • 顶点着色器:
  • 片元着色器:
Shader "Unlit/C14_Hatching"
{
    Properties
    {
        _Color ("Color Tint", Color) = (1.0, 1.0, 1.0, 1.0)
        _MainTex ("Texture", 2D) = "white" {}
        _TileFactor ("平铺系数", Float) = 1.0       // 值越大,素描线条越密;_TileFactor缩放uv坐标,控制纹理大小
        _OutLine ("OutLine", Range(0.0, 1.0)) = 0.1     // 边缘线,颜色就不追加了,默认黑色
        _HatchMap0 ("HatchMap 0", 2D) = "white" {}
        _HatchMap1 ("HatchMap 1", 2D) = "white" {}
        _HatchMap2 ("HatchMap 2", 2D) = "white" {}
        _HatchMap3 ("HatchMap 3", 2D) = "white" {}
        _HatchMap4 ("HatchMap 4", 2D) = "white" {}
        _HatchMap5 ("HatchMap 5", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        //【调用14.1节的描边Pass】
        UsePass "Unlit/C14_ToonShading/OUTLINE"

        //【素描Pass】
        Pass
        {
            Tags {"LightMode" = "ForwardBase"}
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"               //阴影所需
            #include "AutoLight.cginc"              //阴影所需
            #pragma multi_compile_fwdbase_fullshadows       //阴影所需

            half4 _Color;
            sampler2D _MainTex;
            float _TileFactor;
            half _OutLine;
            sampler2D _HatchMap0;
            sampler2D _HatchMap1;
            sampler2D _HatchMap2;
            sampler2D _HatchMap3;
            sampler2D _HatchMap4;
            sampler2D _HatchMap5;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv0 : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv0 : TEXCOORD0;
                float3 posWS : TEXCOORD1;
                half3  hatchWeights0 : TEXCOORD2;   //纹理012的权值————权值当作亮度
                half3  hatchWeights1 : TEXCOORD3;   //纹理345的权值————权值当作亮度
                LIGHTING_COORDS(4, 5)

            };

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv0 = v.uv0 * _TileFactor;        // _TileFactor缩放uv坐标,控制纹理大小
                o.posWS = mul(unity_ObjectToWorld , v.vertex).xyz;
                float3 nDirWS = normalize(UnityObjectToWorldNormal(v.normal));
                float3 lDirWS = normalize(UnityWorldSpaceLightDir(o.posWS));

                //【光照模型:Lambert】
                half lambert = max(0.0, dot(nDirWS, lDirWS));

                o.hatchWeights0 = half3(0.0, 0.0, 0.0); 
                o.hatchWeights1 = half3(0.0, 0.0, 0.0);
                float hatchFactor = lambert * 7.0;      // 通过Lambert决定不同纹理的权值

                //【权值给予,对于每个顶点】
                if (hatchFactor > 6.0) {        // 白色权值 = 1 - 所有纹理的权值之和,这里最亮,所以白色权值为1.
                    // pure white ,do nothing
                }
                else if (hatchFactor > 5.0) {   // 纹理0权值;此时所有权值相加不为1,所以是白色和纹理0混合
                    o.hatchWeights0.x = 6.0 - hatchFactor;
                }
                else if (hatchFactor > 4.0) {   // 纹理0权值, 纹理1权值。  纹理0权值+ 纹理1权值=1 ,所以是纹理0和纹理1混合
                    o.hatchWeights0.x = hatchFactor - 4.0;
                    o.hatchWeights0.y = 1 - o.hatchWeights0.x;
                }
                else if (hatchFactor > 3.0) {
                    o.hatchWeights0.y = hatchFactor - 3.0;
                    o.hatchWeights0.z = 1 - o.hatchWeights0.y;
                }
                else if (hatchFactor > 2.0) {
                    o.hatchWeights0.z = hatchFactor - 2.0;
                    o.hatchWeights1.x = 1 - o.hatchWeights0.z;
                }
                else if (hatchFactor > 1.0) {
                    o.hatchWeights1.x = hatchFactor - 1.0;
                    o.hatchWeights1.y = 1 - o.hatchWeights1.x;
                }
                else {
                    o.hatchWeights1.y = hatchFactor - 0.0;
                    o.hatchWeights1.z = 1 - o.hatchWeights1.y;
                }

                TRANSFER_VERTEX_TO_FRAGMENT(o)  //阴影
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                //【采样纹理,获得每个像素对应的颜色】
                half4 hatchMap0 = tex2D(_HatchMap0, i.uv0) * i.hatchWeights0.x;     //纹理0的值
                half4 hatchMap1 = tex2D(_HatchMap1, i.uv0) * i.hatchWeights0.y;     //纹理1的值
                half4 hatchMap2 = tex2D(_HatchMap2, i.uv0) * i.hatchWeights0.z;     //纹理2的值
                half4 hatchMap3 = tex2D(_HatchMap3, i.uv0) * i.hatchWeights1.x;     //纹理3的值
                half4 hatchMap4 = tex2D(_HatchMap4, i.uv0) * i.hatchWeights1.y;     //纹理4的值
                half4 hatchMap5 = tex2D(_HatchMap5, i.uv0) * i.hatchWeights1.z;     //纹理5的值
                half4 sumAllHatchWeights = i.hatchWeights0.x + i.hatchWeights0.y + i.hatchWeights0.z +
                                           i.hatchWeights1.x + i.hatchWeights1.y + i.hatchWeights1.z;       //得到所有权值之和,用于计算纯白色
                half4 whiteCol = half4(1.0, 1.0, 1.0, 1.0) * (1.0 - sumAllHatchWeights);        //计算白色

                half4 hatchCol = whiteCol + hatchMap0 + hatchMap1 + hatchMap2 + hatchMap3 + hatchMap4 + hatchMap5;  //素描颜色
                //【shadow】
                half shadow = LIGHT_ATTENUATION(i);
                //【finalCol】
                half3 finalCol = (hatchCol * _Color ).rgb;
                return half4(finalCol, 1.0);
            }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

最终效果

Related Articles

继续阅读