unityURP三渲二——国风水墨场景实现shader原理

一、什么是三渲二,为什么会用到三渲二

三渲二就是把三维渲成二维风格,专业上叫做NPR(Non-Photorealistic Rendering,非真实感渲染)

简单来说

就是用3D模型来做底层,用卡通化的渲染

特点是有明显的描边,色块化,光影边界清晰,没有写实级别的细腻过渡

典型的例子就有:《原神》《崩坏》《双城之战》等

为什么要用呢,肯定是为了方便哈哈

最最最简单的说法就是既有2D的风格又能有3D的制作效率和镜头发挥空间,要是2D的话有些复杂镜头的帧画面成本代价太高

关于性能

风格化渲染性能压力小,中低端配置也能实现

二、实际案例操作(网上找的素材,如有雷同,那是肯定的)

这是一套Shader Gragh双Pass三渲二描边材质的节点,风格参数为中国水墨风,有点千里江山图的味道,主要是原本的模型素材就有点画的感觉,所以具体你想做什么效果还是得自己调一下参数

1、实现方法(这只是我的,当然你有更好的也行)

整个实现的逻辑将其分为两个节点逻辑:(1)逻辑A:主体Lit材质(纹理+法线+菲涅尔)

(2)逻辑B:顶点外扩描边

这两个节点的背后shader代码都是用的HLSL语句和BRP的CG内置语句稍有区别但是不太大,整个语法还是按照ShaderLab的格式写的。

2、ShaderGraph开双pass通道(针对2020以后版本的unity版本)

(1)详细步骤:对对看,简单

  • 在Shader Gragh的Graph Setting面板里,新建一个pass,命名为Pass0_Outline(描边层)
  • 原来默认的Pass改名为Pass1_Main(主体层)
  • 调整Pass顺序:Pass0_Outline 放在最上面,Pass1_Main放在下面(Unity 会从上到下依次渲染)。

(2)设置描边节点

额外的设置:Fragment 节点的 Metallic 设为 0,Smoothness 设为 0。

描边 Pass 里不要连接法线、平滑度等其他通道,只输出 Position 和 Base Color

描边 Pass 的渲染设置:

在 Pass0_Outline 的 Graph Settings 里修改:

Render Face: Back(只渲染背面,避免正面描边被主体挡住)记住因为和另外一种方法稍有区别

ZWrite: On

ZTest: LEqual

Blend: Off

Cull Mode: Back

整个节点背后的原理:

(1)计算逻辑:

把Normal Vector和Float参数相乘,得到外扩向量

外扩向量=法线方向x描边强度

这个向量的方向和法线方向一致,长度由描边强度决定

把原始的position和上一步算出的外扩向量相加

新顶点位置=原始顶点位置+外扩向量

这一步就是让每个顶点都沿着自己的法线方向向外移动一段距离,把整个模型都放大了一圈

连接到Vertex-》Position,Unity 就会用这个外扩后的顶点位置来渲染这一层模型。

Color参数直接连接到Fragment-》BaseColor,这意味着这个描边层的模型会被填充为纯色,一般为黑灰色,没有任何纹理,只用来显示轮廓

// 属性声明(描边 Pass 专属参数)
//HLSL 固定语法,开启材质参数缓冲区
CBUFFER_START(UnityPerMaterial)
    float _OutlineWidth;
    float4 _OutlineColor;
CBUFFER_END
//简单理解就是告诉 Shader:我有两个可调参数 —— 描边宽度、描边颜色。



struct Attributes
{
    float3 positionOS   : POSITION;
    float3 normalOS     : NORMAL;
//顶点着色器的 “输入数据”,表示:从 CPU / 模型网格中,传给顶点着色器哪些信息。
//OS不是内心OS哦,是指模型空间语言Object Space
};



//顶点着色器 → 片元着色器 的 “传递数据”,也是 GPU 裁剪、屏幕映射必须的核心数据。
struct Varyings
{
    float4 positionHCS  : SV_POSITION;
};



// Vertex Shader(描边 Pass 核心:顶点外扩)
Varyings VertOutline(Attributes input)
{
    Varyings output;
    // 1. 法线方向外扩顶点
    float3 positionOS = input.positionOS + input.normalOS * _OutlineWidth;
    // 2. 转换到裁剪空间
    output.positionHCS = TransformObjectToHClip(positionOS);
//OS到HCS也就是模型空间坐标到裁剪空间坐标
    return output;
}



// Fragment Shader(描边 Pass:纯色输出)
half4 FragOutline(Varyings input) : SV_Target
{
    // 直接输出描边颜色,不需要任何纹理/光照计算
    return _OutlineColor;
}

(3)设置 主体 Pass(MainPass)节点

连接之前在 Shader Graph 顶部的 Pass 下拉菜单,选择 MainPass,切换到主体 Pass 的编辑界面。

主体 Pass 的渲染设置(在 MainPass 的 Graph Settings 里修改):

Material:保持 Lit

Render Face:Front(只渲染正面)

Depth Write:On

Depth Test:LEqual

Cull Mode:Back

Cast Shadows:勾选

注意注意:主体 Pass 的 Vertex.Position 直接用默认的 Position (Object Space),不要做任何偏移。

整个节点背后的逻辑(相信学计算机的同学对菲涅尔应该都不陌生啦):

// 属性声明(对应你 Graph 里的参数)
CBUFFER_START(UnityPerMaterial)
    sampler2D _BaseMap;
    float4 _BaseMap_ST;
    float4 _BaseMapColor;
    sampler2D _NormalMap;
    float _NormalIntensity;
    float _Smoothness;
    float _FresnelPower;
    float _FresnelIntensity;
    float4 _FresnelColor;
CBUFFER_END

struct Attributes
{
    float3 positionOS   : POSITION;
    float2 uv           : TEXCOORD0;
    float3 normalOS     : NORMAL;
    float4 tangentOS    : TANGENT;
};

struct Varyings
{
    float4 positionHCS  : SV_POSITION;
    float2 uv           : TEXCOORD0;
    float3 normalWS     : TEXCOORD1;
    float3 tangentWS    : TEXCOORD2;
    float3 bitangentWS  : TEXCOORD3;
    float3 positionWS   : TEXCOORD4;
};

// Vertex Shader(主体 Pass)
Varyings VertMain(Attributes input)
{
    Varyings output;
    // 顶点位置(不做外扩,保持原始)
    output.positionHCS = TransformObjectToHClip(input.positionOS);
    output.positionWS = TransformObjectToWorld(input.positionOS);
    
    // UV 平铺与偏移
    output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
    
    // 法线/切线转换
    output.normalWS = TransformObjectToWorldNormal(input.normalOS);
    output.tangentWS = TransformObjectToWorldDir(input.tangentOS.xyz);
    output.bitangentWS = cross(output.normalWS, output.tangentWS) * input.tangentOS.w * GetOddNegativeScale();
    
    return output;
}

// Fragment Shader(主体 Pass)
half4 FragMain(Varyings input) : SV_Target
{
    // 1. 基础纹理采样 + 颜色 tint
    half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv);
    half3 baseColor = baseMap.rgb * _BaseMapColor.rgb;
    
    // 2. 法线贴图处理
    half4 normalMap = SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, input.uv);
    half3 normalTS = UnpackNormal(normalMap);
    normalTS.xy *= _NormalIntensity;
    half3x3 TBN = half3x3(normalize(input.tangentWS), normalize(input.bitangentWS), normalize(input.normalWS));
    half3 normalWS = normalize(mul(normalTS, TBN));
    
    // 3. 菲涅尔效果计算
    float3 viewWS = GetWorldSpaceViewDir(input.positionWS);
    float NdotV = saturate(dot(normalize(normalWS), normalize(viewWS)));
    float fresnel = pow(1 - NdotV, _FresnelPower);
    float fresnelMask = 1 - step(_FresnelIntensity, fresnel); // 对应你 Step + OneMinus 逻辑
    half3 fresnelColor = lerp(0, _FresnelColor.rgb, fresnelMask);
    
    // 4. 颜色混合(Overlay 模式,对应你 Graph 的 Blend 节点)
    half3 finalColor = lerp(baseColor, 1 - 2 * (1 - baseColor) * (1 - fresnelColor), fresnelMask);
    
    // 输出 Lit 材质的各个通道
    half4 finalOutput = half4(finalColor, baseMap.a);
    // 这里的 Normal/Smoothness 会被 URP Lit 管线自动处理,Shader Graph 内部会自动生成对应输出
    
    return finalOutput;
}

核心算法就是:菲涅尔效应基础公式,来自物理光学,菲涅尔效应描述了光线在两种介质表面反射率随入射角变化的规律,在计算机图形学中,常用简化公式实现:

float fresnel = pow(1 - saturate(dot(normalize(N), normalize(V))), Power);

边缘提取(One Minus + Step)One Minus:将菲涅尔结果反转(中间亮→边缘亮)Step:通过阈值(Edge参数)将菲涅尔结果二值化,提取出清晰的边缘轮廓再经过一次One Minus,将轮廓区域转为白色,用于后续颜色叠加

边缘颜色混合(Blend + Multiply)将提取的边缘区域与Color参数混合,再与主纹理进行Overlay模式的 Blend,实现边缘发光 / 变色效果。

法线贴图模块:

float3 normalTS = tex2D(_NormalMap, uv).rgb * 2.0 - 1.0;
//法线贴图的 RGB 通道存储的是切线空间法线信息,需要从 [0,1] 范围映射到 [-1,1]:

法线强度控制:

normalTS.xy *= _NormalIntensity;
normalTS = normalize(normalTS);
//用于调整法线效果的强弱

Base Color

混合了边缘光的主纹理

模型的基础漫反射颜色

Normal

法线贴图采样结果

表面法线信息,影响光照反射

Smoothness

暴露的Smoothness属性

控制表面高光的锐利程度(0 = 粗糙,1 = 镜面)

Metallic

暴露的Metallic属性

控制金属度(0 = 非金属,1 = 金属)

Alpha

未额外连接,默认 1

透明度(此 Shader 为不透明材质)

Emission

未连接,默认 0

自发光(边缘光通过 Blend 叠加在 Base Color 实现)

(4)总结一下吧:

举个蛋糕的例子,一层pass就相当于一层蛋糕,而你已经学了很久做蛋糕了你做两层

  • 底层蛋糕 = 描边 Pass(放大一圈的纯色模型)
  • 上层蛋糕 = 主体 Pass(正常大小的带纹理模型)
  • Unity 会先烤底层,再烤上层,最后叠在一起,你看到的就是带描边的完整效果。
  • 3、如果unity版本过低没有双pass通道,那可以把这两部分节点放在两个Shader Graph上,把它的 Render Face 设为 Back,给模型挂两个材质球,Unity 会按顺序渲染:先渲染描边层,再渲染主体层,效果和多 Pass 完全一致。

    4、纯手工Shder代码(无节点)

    Shader "Custom/ToonLitWithOutline"
    {
        Properties
        {
            // 主体 Pass 参数
            _BaseMap ("Base Map", 2D) = "white" {}
            _BaseMapColor ("Base Color", Color) = (1,1,1,1)
            _NormalMap ("Normal Map", 2D) = "bump" {}
            _NormalIntensity ("Normal Intensity", Range(0, 2)) = 1
            _Smoothness ("Smoothness", Range(0,1)) = 0.5
            _FresnelPower ("Fresnel Power", Range(1, 10)) = 3
            _FresnelIntensity ("Fresnel Intensity", Range(0,1)) = 0.5
            _FresnelColor ("Fresnel Color", Color) = (1,1,1,1)
            
            // 描边 Pass 参数
            _OutlineWidth ("Outline Width", Range(0, 0.1)) = 0.02
            _OutlineColor ("Outline Color", Color) = (0,0,0,1)
        }
    
        SubShader
        {
            Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }
            
            // --------------------------
            // 描边 Pass(先渲染)
            // --------------------------
            Pass
            {
                Name "Outline"
                Cull Front  // 只渲染背面
                ZWrite On
                ZTest LEqual
                
                HLSLPROGRAM
                #pragma vertex VertOutline
                #pragma fragment FragOutline
                #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
                
    
                // 描边 Pass HLSL 代码
                // 属性声明(描边 Pass 专属参数)
    CBUFFER_START(UnityPerMaterial)
        float _OutlineWidth;
        float4 _OutlineColor;
    CBUFFER_END
    
    struct Attributes
    {
        float3 positionOS   : POSITION;
        float3 normalOS     : NORMAL;
    };
    
    struct Varyings
    {
        float4 positionHCS  : SV_POSITION;
    };
    
    // Vertex Shader(描边 Pass 核心:顶点外扩)
    Varyings VertOutline(Attributes input)
    {
        Varyings output;
        // 1. 法线方向外扩顶点
        float3 positionOS = input.positionOS + input.normalOS * _OutlineWidth;
        // 2. 转换到裁剪空间
        output.positionHCS = TransformObjectToHClip(positionOS);
        return output;
    }
    
    // Fragment Shader(描边 Pass:纯色输出)
    half4 FragOutline(Varyings input) : SV_Target
    {
        // 直接输出描边颜色,不需要任何纹理/光照计算
        return _OutlineColor;
    }
                ENDHLSL
            }
            
    
            // --------------------------
            // 主体 Pass(后渲染,Lit 光照)
            // --------------------------
            Pass
            {
                Name "Main"
                Cull Back
                ZWrite On
                ZTest LEqual
                
                Tags { "LightMode" = "UniversalForward" }
                
                HLSLPROGRAM
                #pragma vertex VertMain
                #pragma fragment FragMain
                #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
                #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
                
                // 主体 Pass HLSL 代码
               // 属性声明(对应你 Graph 里的参数)
               CBUFFER_START(UnityPerMaterial)
                 sampler2D _BaseMap;
                float4 _BaseMap_ST;
                float4 _BaseMapColor;
                 sampler2D _NormalMap;
                 float _NormalIntensity;
                float _Smoothness;
                float _FresnelPower;
                float _FresnelIntensity;
                 float4 _FresnelColor;
        CBUFFER_END
    
    struct Attributes
    {
        float3 positionOS   : POSITION;
        float2 uv           : TEXCOORD0;
        float3 normalOS     : NORMAL;
        float4 tangentOS    : TANGENT;
    };
    
    struct Varyings
    {
        float4 positionHCS  : SV_POSITION;
        float2 uv           : TEXCOORD0;
        float3 normalWS     : TEXCOORD1;
        float3 tangentWS    : TEXCOORD2;
        float3 bitangentWS  : TEXCOORD3;
        float3 positionWS   : TEXCOORD4;
    };
    
    // Vertex Shader(主体 Pass)
    Varyings VertMain(Attributes input)
    {
        Varyings output;
        // 顶点位置(不做外扩,保持原始)
        output.positionHCS = TransformObjectToHClip(input.positionOS);
        output.positionWS = TransformObjectToWorld(input.positionOS);
        
        // UV 平铺与偏移
        output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
        
        // 法线/切线转换
        output.normalWS = TransformObjectToWorldNormal(input.normalOS);
        output.tangentWS = TransformObjectToWorldDir(input.tangentOS.xyz);
        output.bitangentWS = cross(output.normalWS, output.tangentWS) * input.tangentOS.w * GetOddNegativeScale();
        
        return output;
    }
    
    // Fragment Shader(主体 Pass)
    half4 FragMain(Varyings input) : SV_Target
    {
        // 1. 基础纹理采样 + 颜色 tint
        half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv);
        half3 baseColor = baseMap.rgb * _BaseMapColor.rgb;
        
        // 2. 法线贴图处理
        half4 normalMap = SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, input.uv);
        half3 normalTS = UnpackNormal(normalMap);
        normalTS.xy *= _NormalIntensity;
        half3x3 TBN = half3x3(normalize(input.tangentWS), normalize(input.bitangentWS), normalize(input.normalWS));
        half3 normalWS = normalize(mul(normalTS, TBN));
        
        // 3. 菲涅尔效果计算
        float3 viewWS = GetWorldSpaceViewDir(input.positionWS);
        float NdotV = saturate(dot(normalize(normalWS), normalize(viewWS)));
        float fresnel = pow(1 - NdotV, _FresnelPower);
        float fresnelMask = 1 - step(_FresnelIntensity, fresnel); // 对应你 Step + OneMinus 逻辑
        half3 fresnelColor = lerp(0, _FresnelColor.rgb, fresnelMask);
        
        // 4. 颜色混合(Overlay 模式,对应你 Graph 的 Blend 节点)
        half3 finalColor = lerp(baseColor, 1 - 2 * (1 - baseColor) * (1 - fresnelColor), fresnelMask);
        
        // 输出 Lit 材质的各个通道
        half4 finalOutput = half4(finalColor, baseMap.a);
        // 这里的 Normal/Smoothness 会被 URP Lit 管线自动处理,Shader Graph 内部会自动生成对应输出
        
        return finalOutput;
    }
                
                ENDHLSL
            }
        }
        FallBack "Hidden/Universal Render Pipeline/FallbackError"
    }
    

    5、最后出效果之后看看好不好看,不好看接着调,哪里出错了问问AI

    要是有更好的方法,大家积极讨论,我还在学习阶段什么都愿意看看😆

    #unity学习#
    unityshader开发学习 文章被收录于专栏

    这份 Unity Shader 学习笔记,是自己从零开始摸索着色器的过程记录。从基础的顶点片元、坐标变换,到光照、贴图、透明度混合,再到后期效果与优化,一点点踩坑、调试、总结。不追求晦涩理论,只记录能看懂、能复现、能直接用的写法和思路。方便以后忘记某个效果怎么实现时快速查阅,也见证自己从看不懂语法到能写出可用 shader 的成长。不求多么专业高深,只做一份属于自己的、随手能用的技术备忘录。

    全部评论

    相关推荐

    三月 · 3.14 / 3.21 美团 – 笔试· 3.15 拼多多笔试挂· 3.17 沐瞳科技测开 一面挂· 3.19 腾讯wxg 一面挂· 3.22 柠檬微趣 笔试挂· 3.24 字节跳动测开 一面挂· 3.25 腾讯云 一面挂· 3.28 阿里集团ai应用笔试 挂· 3.30 美团全栈 一面挂· 3.30 收到腾讯ieg面邀4.1,当时有点崩溃,延迟了一周面试时间,三月惨淡收官四月· 4.3 米哈游一面过,两小时后约二面· 4.7 文远知行 一面挂· 4.7 腾讯ieg 一面过,次日约二面· 4.8 米哈游二面,横向九天挂· 4.8 字节跳动客户端 一面挂· 4.10 美团全栈复活面邀,不匹配拒了· 4.12 鹰角网络 笔试· 4.13 滴滴一面 十分钟后二面 ,临时推迟了影石的面试· 4.13 腾讯ieg二面,次日看官网过· 4.15 影石 移动端 一面· 4.17 美团全栈复活 一面挂· 4.21 腾讯ieg hr面,次日云证· 4.21 滴滴 oc· 4.22 影石 二面· 4.22 吉比特约面拒· 4.22 滴滴offer,腾讯录用评估中三月边实习边面试压力大到头疼,经常崩溃,笔试做的很差,手撕撕不出来,八股常常答不上 ,项目讲不明白。一焦虑就爱吃油炸食品,一周四顿麦当劳,有个周末崩溃的骑了十公里去商场里买舒芙蕾。四月份刚好实习期结束,过清明节调整了状态,给自己做好了心理准备,重新复盘了之前的面试。假期结束后房租也到期了,刚好7.8号有四场面试,就在环球影城旁边订了酒店面试,面完这四场第二天狠狠坐了三趟霸天虎过山车。常常觉得自己准备的不够充分,面经也没有看完,面试之前脑袋空空还很想上厕所,但是实际上一开始自我介绍就冷静了下来,感觉项目能讲的越来越清晰,面试的八股原来来来回回就那么几个常见的,可以熟练的讲清楚了,到后面发现自己就算是完全不会的领域也能冷静的分析并且扯上几句,手撕也能很快做出来了,其实一下场我都不清楚自己是怎么做到的。心态也逐渐从面试很重要一定要过呀转换到熬完这一次就算胜利,哄着自己继续往前走。两个月有种脱胎换骨的感觉,不仅仅在技术层面,这两个月想了很多,想清楚了自己未来想要什么。暑期结束,继续好好学习,好好工作。
    点赞 评论 收藏
    分享
    评论
    3
    2
    分享

    创作者周榜

    更多
    牛客网
    牛客网在线编程
    牛客网题解
    牛客企业服务