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就相当于一层蛋糕,而你已经学了很久做蛋糕了你做两层
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学习#
查看21道真题和解析