图形渲染风格主要分为真实感渲染(Photorealistic rendering)和非真实感渲染(Non-photorealistic rendering,NPR)。 写实渲染的目的是渲染出逼真的画面,而非写实渲染的目的则更为多样,主要是模拟艺术绘画风格,呈现手绘的效果。 常见的非写实渲染技术有卡通渲染、油画渲染、像素渲染、铅笔画、素描、蜡笔画和水墨画等类型。 其中,使用最广泛、研究最多的绘图类型是卡通渲染(Cel Shading或Toon Shading)。 接下来我们介绍几种常见的、实用的、好玩的非真实感渲染技术和技巧。
卡通渲染
卡通渲染是非真实感渲染领域中使用最广泛的渲染技术,在游戏和影视领域非常普遍。 影视领域卡通渲染的主要代表作是《你的名字》; 而游戏领域的大作很多,最著名的有《大神》、《崩坏3》、《塞尔达传说》和《军团要塞2》。下图是《大神》和《塞尔达传说》游戏的实时卡通渲染图.
卡通渲染风格
游戏领域的卡通渲染主要分为美式卡通风格和日式卡通风格两种。 其中美式卡通的色彩比较连续,不会有明显的色块分割线。 主要代表作是军团要塞2; 而日系漫画在着色上色块明显,色块分割线清晰。 主要代表作是《崩溃》。 坏3。 从这些图片中我们可以发现,卡通渲染和写实渲染有很多区别,最大的区别在于笔触处理和艺术着色。 下面就从这两个方面来介绍卡通渲染。
军团要塞2和崩坏3招
轮廓渲染是几乎所有非写实渲染都需要达到的效果,但是不同的渲染风格有不同的笔触细节。 在Real-Time Rendering一书中,描边技术分为以下五类:基于法线和透视的描边(Shading Normal Contour Edges)、程序几何描边(Procedural Geometry Silhouetting)、基于图像处理的描边Edge Detection by Image处理、几何轮廓边缘检测和混合剪影。 其中,基于法线和透视的描边、程序几何描边和基于图像处理的描边这三种方法使用最多。
这种描边算法主要是利用视角方向和法线方向的点积结果 float vdotn = dot(viewDir, normal 来获取轮廓线信息。通过观察,我们很容易发现边缘上的像素点与法线方向的夹角视角接近90°,像素点距离边缘越远,与视线的夹角越小。这种描边方式可以通过一个阈值参数_EdgeThred来控制轮廓线的宽度,也可以通过一个维纹理贴图。这种方法其实相当的用环境贴图(Environment Map)来处理物体的表面。基于法线和透视的描边最大的优点就是简单易实现,并且只需要一个Pass就可以得到描边效果;缺点也很明显,从下图中我们可以清楚的看到描边线条的粗细变化很大,不容易控制,h 作为很大的局限性。
法线与透视的关系
基于法线和透视描边得到的效果
描边线条粗细不一,图片来自RTR,4th
程序几何笔划需要两次笔划处理。 第一遍只渲染背面,达到描边效果; 然后在第二遍中正常渲染模型。 在第一遍进行笔划处理时,我们使用顶点着色器将物体本身沿法线方向延伸,得到比原模型稍大的模型,以达到物体轮廓可见的效果。 这种方法一般称为Shell方法或Halo方法; 除了这种方法,还有Z-bias stroke的方法,就是将顶点的Z值沿法线方向移动固定的距离来实现描边。 这种方法是比较不可能的。 control,达到的效果比Shell方法差很多。 如果我们想要实现不同轮廓宽度的效果,我们可以使用顶点颜色来控制笔触的细节,比如使用A通道来控制线条的粗细。
程序几何笔画的过程
程序化几何笔画原理演示
下图比较了Shell方法和Z-bias两种方法实现后的效果。 我们可以很明显的看到Z-bias在很多细节上都很一般,描边效果很差。
Shell method:
float4 viewPos = mul(UNITY_MATRIX_MV, i.vertex);
float3 normal = mul( (float3x3)UNITY_MATRIX_IT_MV, i.normal);
normal.z = -4.0;
viewPos = viewPos + float4(normalize(normal),0) * _Outline;
o.pos = mul(UNITY_MATRIX_P, viewPos);
Z-bias:
float4 viewPos = mul(UNITY_MATRIX_MV, i.vertex);
viewPos.z += _Outline;
o.pos = mul(UNITY_MATRIX_P, viewPos);
Shell法(左)与Z-bias(右)效果对比
这里专门说说使用Shell方法沿着vertex shader的法线方向展开模型时可能遇到的问题。 如果我们简单地扩展,可能会导致后面的patch沿法线扩展的Z值小于原来需要渲染的patch的Z值,覆盖掉了后面需要渲染的patch前面,导致模型渲染出错,这种情况在凹模型中很容易出现,比如下图模型的嘴巴。 我们需要对变换后的法线normal.z = -0.4;进行一次展平处理,使得第一个Pass得到的模型轮廓变成一个平面。 有关详细信息,请参阅 UnityGems 上的这篇文章:Shader Part 6。
Shell法可能遇到的问题在凹模型中容易出现
Procedural geometric stroking的优点是易于实现,可以获得均匀的描边效果,对大多数模型都有效。 该方法在游戏Cel Damage中用于实现模型的行程; 同样的方法也有很多缺点:不能用来描边角模型,比如立方体; 一般只能用来勾勒出物体的外轮廓(Silhouette),不能画出物体内部的轮廓(Contour); 需要处理两倍数量的Mesh,性能不友好。
程序几何笔划不适用于立方体
这里我们对Silhouette和Contour这两个表达方式做一个简单的区分。 虽然中文翻译叫contour,但在Real-Time Rendering一书中认为Silhouette表达的是物体的外轮廓; 而Contour不仅是外面的轮廓,另外,它还包含了模型内部的各种线条细节,也就是说Silhouette其实是Contour的一个子集,下图给出了很好的区分。
RTR的Silhouette和Contour的区别
基于图像的算法通过图像处理方法进行边缘检测,一般用于屏幕空间检测。 我们通常使用深度、法线、亮度和颜色等属性作为边缘判断的依据。 因此,在处理之前,我们需要获取场景对应的深度图和法线图,然后使用边缘检测算子来判断边缘。 常见的边缘检测算子有:Sobel算子、Canny算子、Laplace算子、Robert算子和Prewitt算子。 在延迟渲染非常流行的今天,我们一般可以直接在G-Buffer中获取需要的深度信息和法线信息。 基于图像处理的方法最大的优点是适应性广,大多数边缘检测都可以采用这种方法; 缺点是如果没有G-Buffer,需要分别获取深度图和法线图,深度和法线变化非常困难。 小地方可能检测不到,比如桌上的纸。
使用深度、法线、亮度和颜色进行边缘检测
下图简单的展示了使用深度图和法线图得到的边缘效果,然后将两个得到的边缘图叠加得到我们最终的效果。
使用深度和法线信息获得描边效果
透视投影经过透视分割后,得到的NDC坐标的深度值是非线性的,我们通常需要使用线性深度值,即我们需要将投影的深度值变换到线性空间中,通常转化为观测值空间。 具体的数学推导可以参考这篇文章:OpenGL投影矩阵。 Unity中提供了两个函数来完成变换过程:LinearEyeDepth和Linear01Depth。 LinearEyeDepth 将投影深度值转换到观察空间,范围为[Near, Far]; 而 Linear01Depth 获取范围 [0,1] 中的深度值。 我们可以使用 DecodeDepthNormal 函数从 _CameraDepthNormalsTexture 中采样深度和法线贴图。 得到的正常范围是[-1,1],需要转化为[0,1]; 也可以直接从_CameraDepthTexture采样深度图,然后使用Linear01Depth转换为[0, 1]。
DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.scrPos.xy), depthValue, normalValues);
fixed4 normalColor = fixed4(normalValues*0.5+0.5, 1);
depth = fixed4(depthValue, depthValue, depthValue, 1.0);
depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
float linearDepth = Linear01Depth(depth);
return fixed4(linearDepth, linearDepth, linearDepth, 1.0);
inline void DecodeDepthNormal(float4 enc, out float depth, out float3 normal)
{
depth=DecodeFloatRG(enc.zw);
normal=DecodeViewNormalStereo(enc);
}
DecodeDepthNormal 利用 DecodeFloatRG 和 DecodeViewNormalStereo 分别解码深度和法线纹理信息。 下图是我们对原图进行处理后得到的法线和深度信息。
原图、法线贴图和深度贴图
几种常见的查边算子
接下来,我们使用 Sobel 算子来检测图像的边缘。 下面三张图分别代表使用颜色信息、深度&法线信息、颜色&深度&法线信息得到的轮廓图。
基于颜色信息的边缘效果
基于法线和深度信息的边缘效果
基于颜色、法线和深度信息的边缘效果
我们在原图的基础上,叠加上面通过边缘检测算法得到的轮廓图,得到如下效果。 这里先添加颜色,再添加法线和深度得到的笔划贴图。
将上面得到的边缘图像叠加在原图上的效果
下图是原图和轮廓图切换的效果。
原图和边缘轮廓图切换效果
游戏无主之地使用了基于图像的边缘检测算法进行边缘处理,渲染结果如下图所示。 我们主要介绍了基于法线和透视的三种笔画方法,程序几何笔画和图像处理。 事实上,除了这些算法之外,还有许多其他变体。 每种算法都有自己的优点和缺点,没有一种算法适用于任何情况。 因此,需要根据不同的应用场景选择不同的算法进行边缘处理。
游戏无主之地使用基于图像处理的方法获得等高线图的艺术着色
卡通渲染的着色方式主要有Cel Shading和Tone Based Shading。 卡通渲染的一个很重要的特点就是色阶少,着色呈现块状感。 这些效果都可以在着色方法中实现。 这两种上色方式的主要区别在于色块的风格和色块的边界处理,得到的效果会有很大的不同。
卡通着色
卡通着色示例
卡通着色一般根据NdotL = dot(normal, lightDir)的结果分配不同的颜色值来模拟卡通色块的着色效果; 除了这种方法,更常见的处理方法是使用NdotL的结果从一维渐变纹理中采样到RampTexture中,部分渐变纹理如下图所示。
3个渐变纹理
基于NdotL采样梯度纹理后得到的卡通效果
游戏《罪恶装备》中使用了卡通着色的想法。
游戏《罪恶装备》使用卡通着色器
如果我们要模拟与视角相关的效果,比如菲涅尔光照,那么我们需要从二维纹理中采样。 除了前面的NdotL,我们还需要一个组件NdotV = dot(normal, viewDir。在军团要塞2中就是通过使用菲涅尔因子来调整高光来保证非常好的画面效果。卡通着色最大的特点就是明暗对比强烈,边界明显,广泛应用于日本卡通渲染,如崩坏3,卡通着色由于块间颜色突然变化,容易出现块间走样,抗锯齿处理是必需的,我们可以使用smoothsetp(-w, w, spec - threshold)来处理边缘突变,其中w表示像素之间的导数值unity外轮廓描边,可以通过CG函数fwidth得到。
抗锯齿高亮边缘
与传统的高光效果不同,卡通渲染会追求风格化的高光效果,比如棱角分明、形状各异的高光样式。 Stylized Highlights for Cartoon Rendering and Animation这篇论文给出了几个有趣的卡通高光效果图,包括Translation, Rotation, Split, Directional Scaling and Squaring )几种。 这些光照实现的思路是对Blinn-Phong模型中的Half向量做一些修改,然后根据法线和Half向量的点积结果改变高光的形状。
几种不同类型的卡通亮点
论文展示了几个程式化的卡通亮点
基于色调的着色在论文 A Non-Photorealistic Lighting Model for Automatic Technical Illustration 中提出。 它与卡通涂色最大的区别是色阶是连续的,色阶是通过在暖色调和冷色调之间插值来渐进的。 种类。 根据NdotL在冷暖色调之间进行插值的结果材质材料硬件设备,作者在论文中使用蓝色模拟冷色调,黄色模拟暖色调,如下公式所示。
基于色调的插值着色
论文中给出的冷暖色定义
军团要塞2是卡通渲染领域的经典之作。 他们在著名论文Illustrative Rendering in Team Fortress 2中介绍了他们使用的渲染技术,主要是着色分为两个部分,View-Independent Lighting Terms和View Dependent Lighting Terms。 所有这些光都是按像素计算的,大部分材质信息包括法线、漫反射反照率、镜面反射分量和各种蒙版都是从纹理中采样的。
view-independent item View-Independent Lighting Terms主要包括空间变化的平行光和修正的Lambert光照。 公式第一部分用于计算环境光部分,通过像素的法线从环境立方体采样得到; 第二部分是朗伯光照的变形,使用半朗伯光照模型,即 alpha = beta = 0.5 ;w()函数是一个Diffuse Warping函数,用来控制颜色之间的渐变效果色块。 军团要塞 2 中的实现是从下面的纹理中采样的。
独立于视图的项目
渐变纹理
使用Amient cube和Diffuse Warp得到的效果如下图(d)所示。 我们可以清楚的看到人物明暗交界处的偏红效果,这是Warp函数的作用。 同时模拟了全局光照的效果,起到了很大的作用。 公式的其他参数请参考论文中的描述。
军团要塞 2 的效果图
与视角相关的View Dependent Lighting Terms主要有传统的Phong高光模型和自定义边缘光照,如下图。 公式左边计算Phong高光和边缘光效果,菲涅尔因子用于调整高光。 论文叫做Multiple Phong Term,可以得到很好的高亮效果; 公式右边用来模拟环境光照效果。 从下图可以看出菲涅尔系数调整后的高光非常柔和,没有过曝,也不会显得油腻。
透视相关项目
叠加菲涅尔因子后的高光效果
float3 halfVector = normalize(lightDir + viewDir);
float3 specBase = pow(saturate(dot(halfVector, s.Normal)), _SpecularPower);
float fresnel = 1.0 - dot(viewDir, halfVector);
fresnel = pow(fresnel, 5.0);
fresnel += _SpecularFresnel * (1.0 - fresnel);
我们通过叠加漫反射分量、镜面高光、环境光和边缘光来获得最终的角色效果。
漫反射分量、镜面高光、环境光和边缘光叠加的过程
下面是使用 Toon Shading、Stroke 和 Rim Light 获得的卡通外观。 更多卡通渲染相关技术可参考崩坏3的技术分享:米哈游技术总监第一篇分享:移动端高质量卡通渲染的实现与优化。
卡通效果展示油画效果图
油画(Oil Painting)是西方绘画的一种。 其主要特点是色彩丰富,笔触细腻,但追求更多层次的细节,需要有明显的边缘效果。 在图像处理方面,一般通过像素周围的权重滤波来模拟油画的特征。 过滤算法的两个重要参数是:过滤半径Radius和灰度区间Bucket。 其中,滤波半径用于确定参与计算的像素个数,一般采用(半径*2+1)*(半径*2+1)的大小面积; 而灰度区间是用来对每个灰度值的像素点进行hash,找到所有Bucket中像素点最多的那个,用Bucket中像素点的平均颜色作为该像素点的颜色。 灰度区间一般也称为哈希桶。
在该算法中,过滤半径Radius用于决定过滤的整体质量,也就是说Radius越大,得到的图片的块状性越强,耗时也越长; 而灰度区间Bucket是用来判断图片的平滑度的。 即块之间的平滑度。 我们一般将像素点(0~255)的灰度值散列到桶区间,找到像素点最多的桶。 总的来说,该算法主要包括以下三个步骤: 1.图像灰度化; 2. 像素散列; 3、统计结算。 下图显示了此过程的各个阶段。
油画图像处理算法
实现中,NumberVector和ColorVector用于记录落入某个区间的像素个数和对应的颜色值,用于后续计算统计,_Bucket用于表示使用的区间数,_Radius用于表示过滤半径.
for(int i = 0; i <= _Radius; i++) {
for(int j = 0; j <= _Radius; j++) {
int BucketIndex = (int)((Luminance(col) * Bucket));
NumberVector[BucketIndex] += 1;
ColorVector[idx] += col;
}
}
对对应的像素点进行哈希处理后,我们只需要统计落入每个桶中的像素点个数,求出桶中所有像素点的平均颜色值,将数量最大的像素点作为该像素点的颜色。
int maxCount = -1;
int maxIndex = 0;
for(int k = 0; k < _Bucket; ++k){
if(NumberVector[k] > maxCount){
maxCount = NumberVector[k];
maxIndex = k;
}
}
fixed4 finalcolor;
finalcolor.rgb = ColorVector[maxIndex] / maxCount;
finalcolor.a = 1.0;
return finalcolor;
我们看到算法在处理每个像素的时候其实需要过滤掉周围的(Radius * 2 + 1) * (Radius * 2 + 1)个像素,所以我们应该尽可能的减小Radius的大小; 当Radius = 4时,我们需要遍历每个像素点81次,也就是说要求处理速度是原来的十分之几,可见该算法是比较耗时的。
算法实现的效果如下图。 我们这里使用分屏显示。 左边是原图,右边是处理后的油画效果。 这里,半径 = 10。
油画分屏显示对比
除了这种方法,还有一种典型的实现方法,就是使用桑原过滤。 桑原滤波算法是数字图像处理领域中非常常用的一种滤波方法。 它是一种非线性平滑滤波器。 我们通常会使用低通滤波器来降低图片的噪点,但一个问题是它会使图片的边缘变得模糊,如下图所示。 虽然使用Box Blur过滤掉了白噪声,但是图像变得模糊,图像的边缘失去了原有的清晰度。 我们接下来要介绍的桑原滤波算法可以在对图像进行平滑处理的同时,保留图像边缘的原始属性。
方形滤波使图像边缘模糊
Kuwahara滤波使用四个卷积核对像素的左上、左下、右上、右下方向的像素进行计数。 下图展示了桑原卷积核的运算过程,其中红点表示当前处理的像素点。 统计半径也就是Radius,每次统计的像素个数为(Radius + 1) * (Radius + 1),统计四次,对应上面说的四个方向。 在每个卷积核的统计过程中,我们需要记录每个块对应的均值(mean)和方差(Variance),并用这两个参数来衡量卷积核中颜色值的整体波动情况。 对于颜色相近的卷积核,对应的方差波动较小,对于颜色差异较大的卷积核,方差差距较大。 由于图片的边缘颜色值相差较大,方差波动较大。 因此,我们在处理边缘像素时,往往会选择图片内部或外部的卷积色值来保留边缘效果。 这就是桑原过滤的原理。
桑原过滤器演示
桑原过滤器演示
在数据统计步骤中,我们会选择方差波动最小的卷积核作为当前像素颜色,这样就可以得到平滑过渡的图像,同时保留图像原有的边缘效果。 下面两张图分别展示了Kuwahara算法在处理图片边缘像素时的运行过程。 在第一个像素点的计算过程中,桑原滤波器最终会选择绿色方块的卷积核作为该像素点的颜色值,因为这个块的颜色值全是黑色,方差波动最小; 而第二个像素最后会使用黄色方块的卷积核作为颜色值。
演示 Kuwahara 过滤如何保留边缘效应
演示 Kuwahara 过滤如何保留边缘效应
我们从右下角开始计数,然后使用偏移数组在其他三个方向上偏移计算像素颜色值 。 我们这里其实默认使用四个桶进行统计,记录每个桶的颜色值的均值和方差来统计卷积核的波动。 我们用mean数组记录均值,用Variance数组记录方差。
float2 offset[4] = {{-_Radius, -_Radius}, {-_Radius, 0}, {0, -_Radius}, {0, 0}};
for (int k = 0; k < 4; k++) {
for(int i = 0; i <= _Radius; i++) {
for(int j = 0; j <= _Radius; j++) {
pos = float2(i, j) + offset[k];
col = tex2Dlod(_MainTex, float4(uv + float2(pos.x * _MainTex_TexelSize.x, pos.y *_MainTex_TexelSize.y),0., 0.)).rgb;
mean[k] += col;
Variance[k] += col * col;
}
}
}
经过以上统计,我们用均值数组和方差数组的数据统计四个卷积核的波动情况,记录整体波动最小的颜色值作为该像素的输出。
方差与均值的关系
for (int k = 0; k < 4; k++) {
mean[k] /= n;
Variance[k] = abs(Variance[k] / n - mean[k] * mean[k]);
res = Variance[k].r + Variance[k].g + Variance[k].b;
if (res< min) {
min = res;
color.rgb = mean[k].rgb;
}
}
其实这两种算法最终的效果是差不多的。 Shadertoy中的油画效果是使用Kuwahara算法完成的,但是我们发现该算法需要的遍历次数为4 * (Radius + 1) * (Radius + 1 ),比我们的算法计算量更大上面介绍过。
下面是算法得到的一些效果:
Radius从0到10的变化过程
屏幕空间扫描效果
Kuwahara滤波算法的卷积核方向与像素的局部方向相同,即所谓的轴对齐卷积核(Axis-aligned Kernels)。 这种过滤方式可能会导致生成的图像有方块感。 解决方案是使用Directional Kuwahara Filter和Anisotropic Kuwahara Filter。
定向过滤过滤过程示意图
影线是一种非常流行的非写实渲染风格。 当前的大多数草图算法都是基于 Praun 等人发表的一篇非常著名的论文 Real-Time Hatching。 2001年。这篇论文描述了如何通过预先生成的素描纹理来实现实时素描风格的渲染。 在论文中,这组纹理被称为Tonal Art Map (TAM),如下图所示。 色调艺术图的笔触从左到右逐渐增加,以模拟物体的漫反射效果。 同一列中不同分辨率的纹理代表多级渐进纹理,用于更好地模拟远离相机的物体。 TAM 图像之间的笔画是特殊的。 我们发现右边的纹理图像的笔画一定包含左边的笔画。 如果不遵循这个规则,产生的素描效果会很奇怪。 论文给出了一种自动生成TAM的方法。 除了使用六张纹理贴图外unity外轮廓描边,我们还可以将纹理贴图进行打包,得到如下两张RGB纹理贴图。 RGB的三个通道分别对应前三个和后三个纹理贴图。 纹理信息。
论文中显示的 TAM 贴图包含渐进纹理
以上六张贴图可以打包成对应的两张RGB贴图
不同的TAM笔画可以得到不同风格的素描效果,文中给出了六种不同的效果。
论文给出了不同TAM图模拟出来的不同sketch效果
这种效果最简单直接的实现就是根据漫反射在不同的TAM图像中采样,然后混合得到最终的效果。 这种实现方式最大的问题是过多的动态分支操作会大大降低GPU的性能,所以一般采用采样权重法来计算不同TAM图的比例。
half3 hatching = half3(0.0, 0.0, 0.0);
hatching += hatch0.r * weightsA.x;
hatching += hatch0.g * weightsA.y;
hatching += hatch0.b * weightsA.z;
hatching += hatch1.r * weightsB.x;
hatching += hatch1.g * weightsB.y;
hatching += hatch1.b * weightsB.z;
We generally need to do some processing on the weights so that the obtained weights have at most two non-zero values, and we will interpolate to get other values in the middle, so we need to eliminate the weight components with lower values and keep the highest weight values to As texture sampling; in addition, we also need to ensure that the sum of the obtained weight values weightsA and weightsB is 1. Generally, the obtained weights can be processed by the following method.
Interpolate between different texture maps
weights0.xy -= weights0.yz;
weights0.z -= weights1.x;
weights1.xy -= weights1.zy;
The following is the effect after sketch processing.
Sketch effect 01
Sketch Effect 02
Sketch Effect 03
Sketch Effect 04
The effect of changing the direction of light.
Sketch effects that change lighting
We can get the ballpoint pen effect in a similar way.
The effect of ballpoint pen drawing shows other non-photorealistic styles
A very important stage of non-photorealistic rendering is stroke, which can simulate some hand-painted effects. Different stroke methods can present different hand-painted styles, so stroke plays a very important role in non-photorealistic rendering. effect. We can superimpose multiple strokes through multiple passes, and each time the vertices are randomly offset to simulate the feeling of hand-painting. Here we use the procedural geometric stroke method introduced earlier to perform stroke processing. Here we simply simulate the effect of two strokes of ink painting and chalk painting.
The following compares the difference between a regular stroke and 4 irregular strokes.
One stroke versus 4 strokes
Ink painting display
In addition to the irregular processing of strokes, we also smoothed the transition of pixel colors, quantized output, and improved contrast. However, the obtained ink painting effect is actually relatively general. It is actually quite difficult to do a good job of ink painting in non-photorealistic rendering. It is very difficult to simulate the strokes of ink painting, and there are not many related research papers. Post-processing to achieve ink style rendering "Low Poly" changed to "ink painting", ink style rendering: how to draw a monkey elegantly, and an attempt to perform ink style 3D rendering in Unity. Although these articles simulated ink painting, they were obtained In fact, the effects are relatively general, and these methods are not general, and the effects obtained under different models or textures are very different.
Below is the effect of the chalk stroke.
The following is the link address of the HTML and PDF of the article! 参考