在这篇技术指导中,我们将介绍一种节省性能且简单的方式来实现卡通网点效果。然而,这种方法在模型表面特别平直时效果可能不佳。
实现原理
如下图所示,我们使用本地坐标或世界坐标作为距离来计算一个点阵,并利用模型的表面与这些点阵相切,再将相切的结果绘制到模型表面上。具体步骤如下:
- 计算波点位置:通过模型空间坐标或世界空间坐标计算波点的位置。
- 确定波点范围:根据光照方向和表面法线,计算波点的范围。
- 应用颜色贴图:使用 ramp texture 对计算好的波点和光方向进行着色。
代码片段
代码片段
我们这里使用的是模型空间坐标来计算波点。
float vertexPoint = length(frac(i.positionOS.xyz * _DotSize) - (0.5).xxx );
i.positionOS
是顶点在模型空间中的位置。_DotSize
控制波点的大小。frac
函数返回输入值的小数部分,用于生成重复的网格图案。length
函数计算从中心到当前顶点的小数部分距离,形成圆形波点。
接下来,我们计算需要波点的范围:
float halfLambert = dot(_MainLightPosition.xyz,i.normalWS) * 0.5 + 0.5;
float externalScale = 1.0 - smoothstep(_ExternalOffset, _ExternalOffset + _ExternalSoft, halfLambert);
float centerScale = smoothstep(_CenterOffset, _CenterOffset + _CenterSoft, halfLambert);
float pointPower = externalScale * centerScale;
float pointMask = round(pow(vertexPoint, pointPower));
halfLambert
是基于光照方向和表面法线计算出的半 Lambert 光照值,用于模拟光照强度。_MainLightPosition
是主光源的位置。smoothstep
函数用于平滑过渡,使得外部和中心区域有柔和的渐变效果。externalScale
和centerScale
控制波点外部和中心区域的范围及过渡。pointPower
是外部和中心范围的乘积,用于调节波点强度。pointMask
使用round(pow(vertexPoint, pointPower))
来生成最终的波点遮罩。
最后,使用 ramp texture 对计算好的波点和光方向进行着色:
float2 colorUV = lerp( float2(_PointColorRange, 0.1), float2(halfLambert,0.1 ), pointMask);
float3 finalColor = tex2D(_RampTex, colorUV);
lerp
函数用于插值,根据pointMask
在两个颜色之间进行线性插值。_PointColorRange
控制波点颜色调节范围。tex2D
函数从贴图中采样颜色。
完整代码(HLSL)
Shader "Unlit/PointArt_Code"
{
Properties
{
[SingleLineTexture]_RampTex ("颜色贴图", 2d) = "white" {}
_PointColorRange ("波点颜色调节", range(0.0, 1.0)) = 0.1
_DotSize ("波点大小" , float) = 1
_ExternalOffset("波点外部范围调整", Range(0.0, 1.0)) = 0.3
_ExternalSoft("波点外部范围过度", Range(0.0, 1.0)) = 0.2
_CenterOffset ("波点中间范围调整", Range(0.0, 1.0)) = 0.1
_CenterSoft ("波点中间范围过度", Range(0.0, 1.0)) = 0.1
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
Name "Forward"
Tags {"LightMode" = "UniversalForwardOnly" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
struct appdata
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
float4 positionOS : TEXCOORD0 ;
float3 normalWS : TEXCOORD1 ;
};
CBUFFER_START(UnityPerMaterial)
float _DotSize;
float _ExternalOffset;
float _ExternalSoft;
float _CenterOffset;
float _CenterSoft;
float _PointColorRange;
CBUFFER_END
sampler2D _RampTex;
v2f vert (appdata v)
{
v2f o;
o.vertex = TransformObjectToHClip(v.positionOS);
o.positionOS = v.positionOS;
o.normalWS = TransformObjectToWorldNormal(v.normalOS) ;
return o;
}
float4 frag (v2f i) : SV_Target
{
//使用模型空间坐标计算波点,根据需要是可以使用世界空间的坐标。
float vertexPoint = length(frac(i.positionOS.xyz * _DotSize) - (0.5).xxx );
//计算圆点的范围
float halfLambert = dot(_MainLightPosition.xyz,i.normalWS) * 0.5 + 0.5;
float externalScale = 1.0 - smoothstep(_ExternalOffset, _ExternalOffset + _ExternalSoft, halfLambert);
float centerScale = smoothstep(_CenterOffset, _CenterOffset + _CenterSoft, halfLambert);
float pointPower = externalScale * centerScale;
float pointMask = round(pow(vertexPoint, pointPower));
//采样贴图颜色
float2 colorUV = lerp( float2(_PointColorRange, 0.1), float2(halfLambert,0.1 ), pointMask);
float3 finalColor = tex2D(_RampTex, colorUV);
return float4(finalColor,1);
}
ENDHLSL
}
}
CustomEditor "UnityEditor.ShaderGraphUnlitGUI"
FallBack "Hidden/Shader Graph/FallbackError"
}
总结
以上代码展示了如何通过简单且高效的方法实现卡通网点效果。这种方法利用了模型空间或世界空间的坐标,通过对光照方向和表面法线的计算,将网点效果映射到模型表面。尽管在模型特别平直时效果可能不佳,但总体上是一种性能友好的实现方式。