效果图
实现大致思路
(1)几张铅笔画程度不同的素描原线图
(2)根据模型顶点法线与场景中光照方向的夹角算点积。
(3)点积越大,颜色越前,素描程度越低,点积越小,颜色越深,素描程度越大。
(4)将点积分为七个不同的范围,其中最大的范围为纯白色
(5)其余六个范围,按点积大小从上方素描原线图由浅至深进行采样
(6)增加一个纯白贡献度,将不采样的部分及单次采样的部分向白色翻转,形成留白的效果
详细代码解释
(1) 定义属性 Properties{
//物体本身颜色
_Color("Main Color",COLOR) = (1,1,1,1)
//采样纹理平铺程度,越大则采样越细
_TileFactor("TileFactor",Float) = 8
//六张素描原线图纹理,按颜色深浅赋值
_Hatch0("Hatch0",2D) = "white" {}
_Hatch1("Hatch1",2D) = "white" {}
_Hatch2("Hatch2",2D) = "white" {}
_Hatch3("Hatch3",2D) = "white" {}
_Hatch4("Hatch4",2D) = "white" {}
_Hatch5("Hatch5",2D) = "white" {}
}
(2)顶点及片段结构体数据
struct a2v {
//模型空间下的顶点坐标
float4 vertex : POSITION;
//模型空间下的法线坐标
float4 normal : NORMAL;
//模型采样纹理坐标
float2 texcoord : TEXCOORD0;
};
struct v2f {
//裁剪空间下的坐标
float4 pos : SV_POSITION;
//纹理采样坐标
float2 uv :TEXCOORD0;
//这两个变量是记录六张素描纹理的采样程度,两个float3的x,y,z分别记录不同纹理的采样程度
float3 hatchWeight0:TEXCOORD1;
float3 hatchWeight1:TEXCOORD2;
};
(3)实现顶点着色器
v2f vert(a2v i) {
v2f o;
o.pos = UnityObjectToClipPos(i.vertex);
o.uv = i.texcoord.xy * _TileFactor;
fixed3 worldLightDir = normalize(WorldSpaceLightDir(i.vertex));
fixed3 worldNormal = UnityObjectToWorldNormal(i.normal);
//计算世界光照方向和模型法线的点积,并且规范到0-1之间,
//夹角越小,点积越大,这个法线与世界光照方向重合程度越高,则越亮,素描程度越低
//夹角越大,点积越小,这个法线与世界光照方向重合程度越低,则越暗,素描程度越高
//当然这里你也可以使用其他的坐标,比如视线方向与法线,效果并不尽相同,根据需求来
float diff = max(0, dot(worldLightDir , worldNormal));
//我们有七个范围,最大的范围为白色,其他六个范围在不同的素描原线纹理中进行采样
float hatchFactor = diff * 7.0;
//初始化
o.hatchWeight0 = fixed3(0, 0, 0);
o.hatchWeight1 = fixed3(0, 0, 0);
if (hatchFactor > 6.0) {
//到达这个区间内的颜色为纯白
//最终要做一个转化,也就是利用纯白的贡献度
}
else if (hatchFactor > 5.0) {
//最浅的那张颜色
o.hatchWeight0.x = hatchFactor - 5.0;
}
else if (hatchFactor > 4.0) {
o.hatchWeight0.x = hatchFactor - 4.0;
o.hatchWeight0.y = 1 - o.hatchWeight0.x;
}
else if (hatchFactor > 3.0) {
//这里及后面都计算了两个值,同时采样上一张,和当前素描纹理,是为了做一个过度
o.hatchWeight0.y = hatchFactor - 3.0;
o.hatchWeight0.z = 1 - o.hatchWeight0.y;
}
else if (hatchFactor > 2.0) {
o.hatchWeight0.z = hatchFactor - 2.0;
o.hatchWeight1.x = 1 - o.hatchWeight0.z;
}
else if (hatchFactor > 1.0) {
o.hatchWeight1.x = hatchFactor - 1.0;
o.hatchWeight1.y = 1 - o.hatchWeight1.x;
}
else {
o.hatchWeight1.y = hatchFactor;
o.hatchWeight1.z = 1 - o.hatchWeight1.y;
}
return o;
}
(4)实现片元着色器
fixed4 frag(v2f o) : SV_Target{
//根据坐标,去纹理中采样,
//具体哪些纹理可以采样,采样程度及混合由o.hatchWeight0 及 o.hatchWeight1 共计六个变量进行控制
fixed4 HatchTex0 = tex2D(_Hatch0, o.uv) * o.hatchWeight0.x;
fixed4 HatchTex1 = tex2D(_Hatch1, o.uv) * o.hatchWeight0.y;
fixed4 HatchTex2 = tex2D(_Hatch2, o.uv) * o.hatchWeight0.z;
fixed4 HatchTex3 = tex2D(_Hatch3, o.uv) * o.hatchWeight1.x;
fixed4 HatchTex4 = tex2D(_Hatch4, o.uv) * o.hatchWeight1.y;
fixed4 HatchTex5 = tex2D(_Hatch5, o.uv) * o.hatchWeight1.z;
//计算纯白贡献度,最大范围中,o.hatchWeight0 及 o.hatchWeight16个分量均为0 ,不会采样任一纹理颜色
//第二大范围中,只采样一张纹理,没有颜色混色,夹角越大,纯白贡献度越高,形成一种从白色到最浅素描采样的过度
//后面5个范围,没有纯白贡献度,完全依赖纹理本身颜色
fixed4 whiteColor = fixed4(1, 1, 1, 1) * (1 - o.hatchWeight0.x - o.hatchWeight0.y -
o.hatchWeight0.z - o.hatchWeight1.x - o.hatchWeight1.y - o.hatchWeight1.z);
//最终颜色为这些颜色的和
fixed4 hatchColor = HatchTex0 + HatchTex1 + HatchTex2 + HatchTex3 + HatchTex4 + HatchTex5 + whiteColor;
//将得到的素描采样颜色与模型自身颜色进行混合
return fixed4(hatchColor.rgb * _Color.rgb , 1.0);
}
完整Shader代码
Shader "Custom/MyHatch"
{
Properties{
_Color("Main Color",COLOR) = (1,1,1,1)
_TileFactor("TileFactor",Float) = 8
_Outline("Outline", Range(0, 1)) = 0.1
_Hatch0("Hatch0",2D) = "white" {}
_Hatch1("Hatch1",2D) = "white" {}
_Hatch2("Hatch2",2D) = "white" {}
_Hatch3("Hatch3",2D) = "white" {}
_Hatch4("Hatch4",2D) = "white" {}
_Hatch5("Hatch5",2D) = "white" {}
}
SubShader{
Tags { "RenderType" = "Opaque" "Queue" = "Geometry"}
Pass{
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma fragment frag
#pragma vertex vert
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
#include "UnityShaderVariables.cginc"
fixed4 _Color;
float _TileFactor;
sampler2D _Hatch0;
sampler2D _Hatch1;
sampler2D _Hatch2;
sampler2D _Hatch3;
sampler2D _Hatch4;
sampler2D _Hatch5;
struct a2v {
float4 vertex : POSITION;
float4 normal : NORMAL;
float2 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv :TEXCOORD0;
float3 hatchWeight0:TEXCOORD1;
float3 hatchWeight1:TEXCOORD2;
float3 worldPos:TEXCOORD3;
};
v2f vert(a2v i) {
v2f o;
o.pos = UnityObjectToClipPos(i.vertex);
o.uv = i.texcoord.xy * _TileFactor;
fixed3 worldLightDir = normalize(WorldSpaceLightDir(i.vertex));
fixed3 worldNormal = UnityObjectToWorldNormal(i.normal);
float diff = max(0, dot(worldLightDir , worldNormal));
float hatchFactor = diff * 7.0;
o.hatchWeight0 = fixed3(0, 0, 0);
o.hatchWeight1 = fixed3(0, 0, 0);
if (hatchFactor > 6.0) {
//到达这个区间内的颜色为纯白
}
else if (hatchFactor > 5.0) {
//最浅的那张颜色
o.hatchWeight0.x = hatchFactor - 5.0;
}
else if (hatchFactor > 4.0) {
o.hatchWeight0.x = hatchFactor - 4.0;
o.hatchWeight0.y = 1 - o.hatchWeight0.x;
}
else if (hatchFactor > 3.0) {
o.hatchWeight0.y = hatchFactor - 3.0;
o.hatchWeight0.z = 1 - o.hatchWeight0.y;
}
else if (hatchFactor > 2.0) {
o.hatchWeight0.z = hatchFactor - 2.0;
o.hatchWeight1.x = 1 - o.hatchWeight0.z;
}
else if (hatchFactor > 1.0) {
o.hatchWeight1.x = hatchFactor - 1.0;
o.hatchWeight1.y = 1 - o.hatchWeight1.x;
}
else {
o.hatchWeight1.y = hatchFactor;
o.hatchWeight1.z = 1 - o.hatchWeight1.y;
}
return o;
}
fixed4 frag(v2f o) : SV_Target{
fixed4 HatchTex0 = tex2D(_Hatch0, o.uv) * o.hatchWeight0.x;
fixed4 HatchTex1 = tex2D(_Hatch1, o.uv) * o.hatchWeight0.y;
fixed4 HatchTex2 = tex2D(_Hatch2, o.uv) * o.hatchWeight0.z;
fixed4 HatchTex3 = tex2D(_Hatch3, o.uv) * o.hatchWeight1.x;
fixed4 HatchTex4 = tex2D(_Hatch4, o.uv) * o.hatchWeight1.y;
fixed4 HatchTex5 = tex2D(_Hatch5, o.uv) * o.hatchWeight1.z;
fixed4 whiteColor = fixed4(1, 1, 1, 1) * (1 - o.hatchWeight0.x - o.hatchWeight0.y -
o.hatchWeight0.z - o.hatchWeight1.x - o.hatchWeight1.y - o.hatchWeight1.z);
fixed4 hatchColor = HatchTex0 + HatchTex1 + HatchTex2 + HatchTex3 + HatchTex4 + HatchTex5 + whiteColor;
return fixed4(hatchColor.rgb * _Color.rgb , 1.0);
}
ENDCG
}
}
}