US4L7——素描风格渲染
US4L7——素描风格渲染
素描风格渲染
素描风格渲染(Hatching Style Rendering),是一种非真实感渲染(NPR),主要目的是使3D模型看起来像手绘素描的视觉效果。
这种风格的渲染常用于游戏、动画和电影中,用来创造一种独特的艺术风格
素描风格渲染基本原理
一句话总结素描风格渲染基本原理:
用漫反射系数决定采样权重,在多张具有不同密度和方向的素描纹理中,进行采样,并将采样结果进行叠加得到最终效果
关键点:
-
多张具有不同密度和方向的素描纹理
美术需要提供多张素描纹理,我们之后会根据不同位置的光照强度,决定从哪种纹理中进行采样
1
2
3
4
5
6
7
8
9
10
11
12
13Properties
{
_Color("Color", Color) = (1, 1, 1, 1) // 颜色叠加
_TileFactor("TileFactor", Float) = 1 // 纹理平铺密度
_OutLineColor("OutLineColor", Color) = (0, 0, 0, 1) // 边缘线颜色
_OutLineWidth("OutLineWidth", Range(0, 1)) = 0.05 // 边缘线宽度
_Sketch0("Sketch0", 2D) = {} // 第一张素描纹理
_Sketch1("Sketch1", 2D) = {} // 第二张素描纹理
_Sketch2("Sketch2", 2D) = {} // 第三张素描纹理
_Sketch3("Sketch3", 2D) = {} // 第四张素描纹理
_Sketch4("Sketch4", 2D) = {} // 第五张素描纹理
_Sketch5("Sketch5", 2D) = {} // 第六张素描纹理
} -
漫反射系数决定采样权重
我们通过兰伯特光照模型中的:
将漫反射光照强度 0~1 扩充到 0~N,如果是 6 张素描纹理,那么就是 0~7,
根据不同顶点的不同光照,决定在哪一张纹理中进行采样的权重更大,该权重决定最后的颜色叠加- 6~7:不在素描纹理中采样
- 5~6:第1张素描纹理中采样
- 3~4:第2、3张纹理中采样
- 4~5:第1、2素描张纹理中采样
- 2~3:第3、4张纹理中采样
- 1~2:第4、5张纹理中采样
- 0~1:第5、6张纹理中采样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord.xy * _TileFactor;
// 计算漫反射光照强度
fixed3 worldLightDir = normalize(WorldSpaceLightDir(v.vertex)); // 世界空间下的光照方向
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); // 世界空间法线方向转换
fixed diffuse = max(0, dot(worldLightDir, worldNormal)); // 漫反射光照强度计算
diffuse = diffuse * 7.0; // 将光照强度从0~1变化到0~7范围
// 用于记录6张纹理采样的权重,默认都是0,0意味着之后不会使用对应纹理颜色中采样的颜色
o.sketchWeights0 = fixed3(0, 0, 0);
o.sketchWeights1 = fixed3(0, 0, 0);
// 根据光照强度,决定各个图片的采样权重
if (diffuse > 6.0) {
// 认为是最亮的部分,不修改任何权重值,意味着不会使用任何素描纹理中采样的颜色
} else if (diffuse > 5.0) {
o.sketchWeights0.x = diffuse - 5.0; // 修改第1张素描纹理权重
} else if (diffuse > 4.0) {
o.sketchWeights0.x = diffuse - 4.0; // 修改第1张素描纹理权重
o.sketchWeights0.y = 1.0 - o.sketchWeights0.x; // 修改第2张素描纹理权重
} else if (diffuse > 3.0) {
o.sketchWeights0.y = diffuse - 3.0; // 修改第2张素描纹理权重
o.sketchWeights0.z = 1.0 - o.sketchWeights0.y; // 修改第3张素描纹理权重
} else if (diffuse > 2.0) {
o.sketchWeights0.z = diffuse - 2.0; // 修改第3张素描纹理权重
o.sketchWeights1.x = 1.0 - o.sketchWeights0.z; // 修改第4张素描纹理权重
} else if (diffuse > 1.0) {
o.sketchWeights1.x = diffuse - 1.0; // 修改第4张素描纹理权重
o.sketchWeights1.y = 1.0 - o.sketchWeights1.x; // 修改第5张素描纹理权重
} else {
o.sketchWeights1.y = diffuse; // 修改第5张素描纹理权重
o.sketchWeights1.z = 1.0 - o.sketchWeights1.y; // 修改第6张素描纹理权重
} -
采样结果进行叠加
根据之前的权重计算,越亮的地方、越趋近于白色,或使用的素描纹理中线条更少更稀疏,而越暗的地方使用的素描纹理中线条更密集。
因此我们只需要使用之前的权重值和纹理采样结果相乘,最后将纹理颜色进行叠加即可。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17fixed4 frag(v2f i) : SV_Target
{
// 如果之前的权重没有设置,默认为0,对应纹理颜色最终就是(0,0,0,0)
fixed4 hatchTex0 = tex2D(_Hatch0, i.uv) * i.hatchWeights0.x; // 在第1张纹理中采样,并乘以权重1
fixed4 hatchTex1 = tex2D(_Hatch1, i.uv) * i.hatchWeights0.y; // 在第2张纹理中采样,并乘以权重2
fixed4 hatchTex2 = tex2D(_Hatch2, i.uv) * i.hatchWeights0.z; // 在第3张纹理中采样,并乘以权重3
fixed4 hatchTex3 = tex2D(_Hatch3, i.uv) * i.hatchWeights1.x; // 在第4张纹理中采样,并乘以权重4
fixed4 hatchTex4 = tex2D(_Hatch4, i.uv) * i.hatchWeights1.y; // 在第5张纹理中采样,并乘以权重5
fixed4 hatchTex5 = tex2D(_Hatch5, i.uv) * i.hatchWeights1.z; // 在第6张纹理中采样,并乘以权重6
// 6~7区间 所有采样颜色都是(0,0,0,0),代表最亮的地方,计算白色叠加值
fixed4 whiteColor = fixed4(1, 1, 1, 1) *
(1 - i.hatchWeight0.x - i.hatchWeight0.y - i.hatchWeight0.z
- i.hatchWeight1.x - i.hatchWeight1.y - i.hatchWeight1.z);
// 最终将所有纹理颜色和白色叠加值相加,得到最终颜色
fixed4 hatchColor = hatchTex0 + hatchTex1 + hatchTex2 + hatchTex3 + hatchTex4 + hatchTex5 + whiteColor;
return hatchColor;
}
素描风格渲染具体实现
纹理资源导入、相关属性添加
-
使用如下的素描纹理资源
-
新建 Shader 命名为
Sketch
,删除无用代码 -
添加属性
- 颜色属性
Color
—— 用于颜色叠加 - 平铺系数
TileFactor
—— 用于平铺纹理,让采样细节更多 - 素描纹理
Sketch1~6
—— 用于采样模拟素描表现效果
1
2
3
4
5
6
7
8
9
10
11Properties
{
_Color("Color", Color) = (1, 1, 1, 1) // 整体颜色叠加
_TileFactor("TileFactor", Float) = 1 // 纹理平铺密度
_Sketch0("Sketch0", 2D) = {} // 第1张素描纹理
_Sketch1("Sketch1", 2D) = {} // 第2张素描纹理
_Sketch2("Sketch2", 2D) = {} // 第3张素描纹理
_Sketch3("Sketch3", 2D) = {} // 第4张素描纹理
_Sketch4("Sketch4", 2D) = {} // 第5张素描纹理
_Sketch5("Sketch5", 2D) = {} // 第6张素描纹理
} - 颜色属性
-
进行属性映射
权重计算(顶点着色器函数逻辑)
-
v2f
结构体声明顶点位置、UV、用两个
fixed3
记录素描纹理权重1
2
3
4
5
6
7struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
fixed3 sketchWeights0 : TEXCOORD1; // x,y,z 分别代表1,2,3张素描的权重
fixed3 sketchWeights1 : TEXCOORD2; // x,y,z 分别代表4,5,6张素描的权重
}; -
顶点着色器函数实现
- 顶点坐标转换
- UV 坐标平铺缩放 让纹理*平铺系数
- 世界空间光照方向、世界空间法线转换
- 兰伯特漫反射光照系数计算
- 将光照系数 从 0~1 扩充到 0~7
- 根据系数决定素描纹理权重
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord.xy * _TileFactor;
// 计算漫反射光照强度
fixed3 worldLightDir = normalize(WorldSpaceLightDir(v.vertex)); // 世界空间下的光照方向
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); // 世界空间法线方向转换
fixed diffuse = max(0, dot(worldLightDir, worldNormal)); // 漫反射光照强度计算
diffuse = diffuse * 7.0; // 将光照强度从0~1变化到0~7范围
// 用于记录6张纹理采样的权重,默认都是0,0意味着之后不会使用对应纹理颜色中采样的颜色
o.sketchWeights0 = fixed3(0, 0, 0);
o.sketchWeights1 = fixed3(0, 0, 0);
// 根据光照强度,决定各个图片的采样权重
if (diffuse > 6.0)
{
// 认为是最亮的部分,不修改任何权重值,意味着不会使用任何素描纹理中采样的颜色
}
else if (diffuse > 5.0)
{
o.sketchWeights0.x = diffuse - 5.0; // 修改第1张素描纹理权重
}
else if (diffuse > 4.0)
{
o.sketchWeights0.x = diffuse - 4.0; // 修改第1张素描纹理权重
o.sketchWeights0.y = 1.0 - o.sketchWeights0.x; // 修改第2张素描纹理权重
}
else if (diffuse > 3.0)
{
o.sketchWeights0.y = diffuse - 3.0; // 修改第2张素描纹理权重
o.sketchWeights0.z = 1.0 - o.sketchWeights0.y; // 修改第3张素描纹理权重
}
else if (diffuse > 2.0)
{
o.sketchWeights0.z = diffuse - 2.0; // 修改第3张素描纹理权重
o.sketchWeights1.x = 1.0 - o.sketchWeights0.z; // 修改第4张素描纹理权重
}
else if (diffuse > 1.0)
{
o.sketchWeights1.x = diffuse - 1.0; // 修改第4张素描纹理权重
o.sketchWeights1.y = 1.0 - o.sketchWeights1.x; // 修改第5张素描纹理权重
}
else
{
o.sketchWeights1.y = diffuse; // 修改第5张素描纹理权重
o.sketchWeights1.z = 1.0 - o.sketchWeights1.y; // 修改第6张素描纹理权重
}
return o;
}
颜色采样(片元着色器函数逻辑)
在片元着色器中实现:
- 对 16 张纹理进行采样 并乘以权重 得到各纹理采样颜色
- 根据 16 张纹理权重计算出白色高光部分颜色
- 将 1~6 张纹理采样颜色 和 白色部分 相加得到最终叠加颜色
1 | fixed4 frag (v2f i) : SV_Target |
外轮廓、阴影相关添加
-
外轮廓
直接复用卡通风格渲染 Shader 的外轮廓
Pass
代码,详见:US4L6——卡通风格渲染1
2
3
4
5
6
7
8
9
10SubShader
{
Tags { "RenderType"="Opaque" }
// OutLine Pass 描边渲染通道
UsePass "TeachShader/ToonShader/OutLinePass"
// Bass Pass 基础渲染通道
Pass {/*...*/}
} -
阴影相关添加
- 加入
SHADOW_COORDS
、TRANSFER_SHADOW
、UNITY_LIGHT_ATTENUATION
- 加入
FallBack "Diffuse"
- 加入
完整 Shader 代码如下:
1 | Shader "TeachShader/Sketch" |
显示效果
设置纹理平铺密度为8,边缘线宽度为0.005
可见,物体的光照效果由素描贴图来呈现,越暗的地方笔触密度越高
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 文KRIFE齐的博客!