【Shader笔记】【NPR】卡通渲染-轮廓线的渲染

2023-11-26 21:50

本文主要是介绍【Shader笔记】【NPR】卡通渲染-轮廓线的渲染,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

  • 写在前面
  • 【NPR】轮廓线的渲染
    • Surface Angle Silhouette
      • 参数&纹理控制
    • Procedural Geometry Silhouette
      • z-bias和vertex normal的方法

写在前面

本文借鉴大佬的《【NPR】漫谈轮廓线的渲染》
地址:https://blog.csdn.net/candycat1992/article/details/45577749

【NPR】轮廓线的渲染

Surface Angle Silhouette

利用viewpoint和surface normal的点乘结果得到轮廓线信息,结果越接近0,说明离轮廓线越近。在实际应用中,我们通常使用一张一维纹理来模拟,即使用视角方向和顶点法向的点乘对该纹理进行采样。使用了两种方法实现这种技术:

  • 一种是使用一个参数_Outline来控制轮廓线的宽度

  • 另一种方式是使用了一张一维纹理来控制
    在这里插入图片描述

参数&纹理控制

使用纹理为:
在这里插入图片描述
使用纹理控制的轮廓效果很难控制,有的地方轮廓很宽,有些地方又捕捉不到。
这种方法的优点在于简单快速,可以在一个Pass里得到结果,而且还可以使用texture filtering对轮廓线进行抗锯齿。
不过也有很多的局限性,比如只适用于某些模型,对于像cube这样的模型就会有问题。虽然我们可以使用变量来控制轮廓线的宽度(如果使用纹理的话就是纹理中黑色的宽度),但实际的效果是依赖于表面的曲率(curvature)的。对于像cube这样表面非常平坦的物体,它的轮廓线回发生突变,要么没有,要么全黑。
在这里插入图片描述

Shader "NPR/Tex_Surface Angle Sihouetting"
{Properties{	_MainTex ("Base(RGB)", 2D) = "white" {}_Outline("Outline",Range(0,1))=0.4_SilhouetteTex("Silhouette Texture",2D)="white"{} //一维纹理}SubShader{Tags { "RenderType"="Opaque" }LOD 200Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"#include "Lighting.cginc"#include "AutoLight.cginc"sampler2D _MainTex;float _Outline;sampler2D _SilhouetteTex;struct v2f{float4 pos : SV_POSITION;float2 uv : TEXCOORD0;float3 worldLightDir:TEXCOORD1;float3 worldNormal:TEXCOORD2;float3 worldViewDir:TEXCOORD3;};v2f vert (appdata_full v) //appdata_full:包含位置、法线、切线、顶点色和两个纹理坐标{v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.uv=v.texcoord;o.worldLightDir = UnityWorldSpaceLightDir(v.vertex);o.worldNormal = UnityObjectToWorldNormal(v.normal);o.worldViewDir = UnityWorldSpaceViewDir(v.vertex);TRANSFER_VERTEX_TO_FRAGMENT(o);  //宏,在顶点着色器中计算上一步中声明的阴影纹理坐标//TRANSFER_SHADOW(o); 好像现在都是这样了return o;}//用view和normal点乘计算轮廓线的函数fixed3 GetSilhouetteUseConstant(fixed3 normal , fixed3 vierDir){//saturate将数控制在【0,1】内,大于1为1,小于0为0,0-1则为本身fixed edge = saturate ( dot (normal ,vierDir)); edge =edge < _Outline ? edge/4 :1;  return fixed3 (edge ,edge,edge);}//使用view和normal的点乘,对一维纹理进行采样的函数fixed3 GetSilhouetteUseTexture(fixed3 normal , fixed3 vierDir){fixed edge = dot(normal ,vierDir);edge = edge *0.5 +0.5;return tex2D(_SilhouetteTex , fixed2(edge,edge)).rgb;}fixed4 frag (v2f i) : SV_Target{fixed3 worldNormal = normalize(i.worldNormal);fixed3 worldLightDir = normalize(i.worldLightDir);fixed3 worldViewDir = normalize(i.worldViewDir);fixed3 col = tex2D(_MainTex , i.uv).rgb;//Use a constant to render Silhouette使用参数来控制轮廓线的宽度//fixed3 silhouetteColor = GetSilhouetteUseConstant (worldNormal ,worldViewDir);//Or use a one dime silhouette Texture使用了一维纹理来控制轮廓线fixed3 silhouetteColor = GetSilhouetteUseTexture( worldNormal ,worldViewDir);fixed4 fragColor;fragColor.rgb = col * silhouetteColor ;fragColor.a =1.0;  // 等于return fixed4(fragColor , 1.0)吧return fragColor;}ENDCG}}
}

Procedural Geometry Silhouette

这种方法的核心是用两个Pass渲染:

  • 第一个Pass中正常渲染frontfaces
  • 第二个Pass中再渲染backfaces,并使用某些技术来让它的轮廓可见。
    当然,渲染背面的方法有很多,比如shell or halo method,即沿着法线方向移动backfaces中的顶点
    下面列举一些其他渲染背面的方法:
  • 只渲染backfaces的edges(可以理解把渲染模式设置为DRAW_EDGE),然后使用一些biasing等技术来保证这些线会在frontfaces的前面渲染。
  • Z-bias方法。把backfaces渲染成黑色,然后在屏幕空间的z方向上向前移动它们,使其可见。移动的距离可以是一个固定值,或其他适应后的值。
    缺点:不能创建宽度相同的轮廓,因为frontface和backface的夹角不一样,可控性很弱。
  • Triangle Fatting。把每个backface triangle的edges都“变胖”一定程度,使其在视角空间中看起来宽度是一致的。
    缺点:对于一些瘦长的triangles(三角形)来说,它的corner(角)也会变得很细长,一种解决的方法就是把拓展后的edges链接在一起形成斜接在一起的corners。
    而且这种方法也无法应用在GPU生成的一些curved surfaces上(因为是弯曲的面,没有edges)
    在这里插入图片描述
  • Shell or halo method,把backface的顶点沿着顶点法向向外扩张。
    优点:很快速,可以在vertex shader中就完成,而且具有一定的健壮性。不需要任何关于相邻顶点/边等信息,所有的处理都是独立的,因此从速度上来说很快。
    缺点:像cube这样的模型,它的同一个顶点在不同面上具有不同的顶点法向,所以向外扩张后会形成一个gaps(间隙),一种解决方法是,强迫同一个位置的顶点具有相同的法向。另一个方法是在这些轮廓处创建额外的网格结构。

上面所列出的所有方法都有一个共同的缺点,那就是对轮廓线的外观可控性很少,而且如果没有进行一些反锯齿操作,轮廓线看起来锯齿比较严重。

z-bias和vertex normal的方法

有两个地方需要注意

  • 首先对顶点只会在xy方向上扩张,这种操作在模型全是外凸的情况下基本没有什么问题,但是一个模型有内凹的部分就有可能会出现轮廓线挡住frontfaces的情况;
  • 另一点是,在扩张顶点时考虑了顶点在投影矩阵中的深度值,这意味着模型轮廓线的宽度会随摄像机移动而改变
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'Shader "NPR/Procedural Geometry Silhouette"
{Properties{_MainTex ("Base(RGB)", 2D) = "white" {}_Outline("Outline",Range(0,1))=0.1}SubShader{Tags { "RenderType"="Opaque" }LOD 200Pass{Tags{"LightMode" = "ForwardBase"}Cull BackLighting OnCGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"#include "Lighting.cginc"#include "AutoLight.cginc"sampler2D _MainTex;struct v2f{float2 uv : TEXCOORD0;float4 pos : SV_POSITION;};v2f vert (appdata_full v) //appdata_full:包含位置、法线、切线、顶点色和两个纹理坐标{v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.uv = v.texcoord;TRANSFER_VERTEX_TO_FRAGMENT(o);return o;}fixed4 frag (v2f i) : COLOR{fixed3 col = tex2D(_MainTex ,i.uv).rgb;fixed4 fragColor;fragColor.rgb =col;fragColor.a = 1.0;return fragColor;}ENDCG}Pass{Tags{"LightMode" = "ForwardBase"}Cull FrontLighting OffCGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"#include "Lighting.cginc"#include "AutoLight.cginc"sampler2D _MainTex;float _Outline;struct v2f{float2 uv : TEXCOORD0;float4 pos : SV_POSITION;};void ZBiaMethod(appdata_full i , inout v2f o) //顶点沿着法线方向偏移方法一{//简单粗暴,直接与参数_Outline相加float4 viewPos = mul(UNITY_MATRIX_MV , i.vertex);viewPos.z +=_Outline;o.pos = mul(UNITY_MATRIX_P , viewPos);}void VertexNormalMethod0(appdata_full i , inout v2f o) //顶点沿着法线方向偏移方法二{	//将法线变换到视角空间后,再计算offset的程度,之后与顶点相加//缺点:如果直接使用顶点法线进行拓展,对于一些内凹的模型,就可能发生背面面片遮挡正面面片的情况o.pos =UnityObjectToClipPos( i.vertex);//法线转换到视角空间,为了让描边可以在观察空间达到最好的效果float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV , i.normal);//将视空间法线xy坐标转化到投影空间,只有xy需要,z深度不需要了 float2 offset = TransformViewToProjection(normal.xy); //在最终投影阶段输出进行偏移操作o.pos.xy +=offset * o.pos.z * _Outline; }void VertexNormalMethod1(appdata_full i , inout v2f o)  //顶点沿着法线方向偏移方法三{	//同理法线转换到视角空间,利用法线的方向与_Outline相乘,得出偏移程度//首先对顶点法线的z分量进行处理,使它们等于一个定值//然后把法线归一化后再对顶点进行扩张。这样的好处在于,拓展后的背面更加扁平化,从而降低了遮挡正面面片的可能性。float4 viewPos = mul(UNITY_MATRIX_MV , i.vertex);float3 normal = mul ( (float3x3) UNITY_MATRIX_IT_MV , i.normal);//设置法线的z分量为定值,对其归一化后再将顶点沿其方向扩张normal.z = -1.0;viewPos = viewPos + float4 (normalize ( normal) ,0) * _Outline;o.pos = mul(UNITY_MATRIX_P , viewPos);}v2f vert ( appdata_full i ){v2f o;//ZBiaMethod (i , o);//VertexNormalMethod0 (i , o);VertexNormalMethod1 (i ,o);o.uv = i.texcoord;TRANSFER_VERTEX_TO_FRAGMENT(o); return o;}fixed4 frag (v2f i) : COLOR{return fixed4 (0,0,0,1);}ENDCG}}FallBack "DIFFUSE"
}

这篇关于【Shader笔记】【NPR】卡通渲染-轮廓线的渲染的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/426356

相关文章

【学习笔记】 陈强-机器学习-Python-Ch15 人工神经网络(1)sklearn

系列文章目录 监督学习:参数方法 【学习笔记】 陈强-机器学习-Python-Ch4 线性回归 【学习笔记】 陈强-机器学习-Python-Ch5 逻辑回归 【课后题练习】 陈强-机器学习-Python-Ch5 逻辑回归(SAheart.csv) 【学习笔记】 陈强-机器学习-Python-Ch6 多项逻辑回归 【学习笔记 及 课后题练习】 陈强-机器学习-Python-Ch7 判别分析 【学

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

论文阅读笔记: Segment Anything

文章目录 Segment Anything摘要引言任务模型数据引擎数据集负责任的人工智能 Segment Anything Model图像编码器提示编码器mask解码器解决歧义损失和训练 Segment Anything 论文地址: https://arxiv.org/abs/2304.02643 代码地址:https://github.com/facebookresear

数学建模笔记—— 非线性规划

数学建模笔记—— 非线性规划 非线性规划1. 模型原理1.1 非线性规划的标准型1.2 非线性规划求解的Matlab函数 2. 典型例题3. matlab代码求解3.1 例1 一个简单示例3.2 例2 选址问题1. 第一问 线性规划2. 第二问 非线性规划 非线性规划 非线性规划是一种求解目标函数或约束条件中有一个或几个非线性函数的最优化问题的方法。运筹学的一个重要分支。2

【C++学习笔记 20】C++中的智能指针

智能指针的功能 在上一篇笔记提到了在栈和堆上创建变量的区别,使用new关键字创建变量时,需要搭配delete关键字销毁变量。而智能指针的作用就是调用new分配内存时,不必自己去调用delete,甚至不用调用new。 智能指针实际上就是对原始指针的包装。 unique_ptr 最简单的智能指针,是一种作用域指针,意思是当指针超出该作用域时,会自动调用delete。它名为unique的原因是这个

查看提交历史 —— Git 学习笔记 11

查看提交历史 查看提交历史 不带任何选项的git log-p选项--stat 选项--pretty=oneline选项--pretty=format选项git log常用选项列表参考资料 在提交了若干更新,又或者克隆了某个项目之后,你也许想回顾下提交历史。 完成这个任务最简单而又有效的 工具是 git log 命令。 接下来的例子会用一个用于演示的 simplegit

记录每次更新到仓库 —— Git 学习笔记 10

记录每次更新到仓库 文章目录 文件的状态三个区域检查当前文件状态跟踪新文件取消跟踪(un-tracking)文件重新跟踪(re-tracking)文件暂存已修改文件忽略某些文件查看已暂存和未暂存的修改提交更新跳过暂存区删除文件移动文件参考资料 咱们接着很多天以前的 取得Git仓库 这篇文章继续说。 文件的状态 不管是通过哪种方法,现在我们已经有了一个仓库,并从这个仓

忽略某些文件 —— Git 学习笔记 05

忽略某些文件 忽略某些文件 通过.gitignore文件其他规则源如何选择规则源参考资料 对于某些文件,我们不希望把它们纳入 Git 的管理,也不希望它们总出现在未跟踪文件列表。通常它们都是些自动生成的文件,比如日志文件、编译过程中创建的临时文件等。 通过.gitignore文件 假设我们要忽略 lib.a 文件,那我们可以在 lib.a 所在目录下创建一个名为 .gi

取得 Git 仓库 —— Git 学习笔记 04

取得 Git 仓库 —— Git 学习笔记 04 我认为, Git 的学习分为两大块:一是工作区、索引、本地版本库之间的交互;二是本地版本库和远程版本库之间的交互。第一块是基础,第二块是难点。 下面,我们就围绕着第一部分内容来学习,先不考虑远程仓库,只考虑本地仓库。 怎样取得项目的 Git 仓库? 有两种取得 Git 项目仓库的方法。第一种是在本地创建一个新的仓库,第二种是把其他地方的某个

Git 的特点—— Git 学习笔记 02

文章目录 Git 简史Git 的特点直接记录快照,而非差异比较近乎所有操作都是本地执行保证完整性一般只添加数据 参考资料 Git 简史 众所周知,Linux 内核开源项目有着为数众多的参与者。这么多人在世界各地为 Linux 编写代码,那Linux 的代码是如何管理的呢?事实是在 2002 年以前,世界各地的开发者把源代码通过 diff 的方式发给 Linus,然后由 Linus