Unity Shader-描边效果

Unity 2D Spine 外发光实现思路 对于3D骨骼,要做外发光可以之间通过向法线方向延申来实现。但是对于2D骨骼,各顶点的法线没有向3D骨骼那样拥有垂直于面的特性,那我们如何做2D骨骼的外发光效果呢? 阅读详情
AI摘要

本文详细介绍了在Unity中创建描边效果的各种方法,包括简单描边的原理,通过开启深度写入和使用Offset指令剔除正面描边,以及如何实现基于后处理的描边效果。

简介


描边效果是游戏里面非常常用的一种效果,一般是为了凸显游戏中的某个对象,会给对象增加一个描边效果。最近又跑回去玩了玩《剑灵》,虽然出了三年了,感觉在现在的网游里面画面仍然算很好的了,剑灵里面走近或者选中NPC的一瞬间,NPC就会出现描边效果,不过这个描边效果是渐变的,会很快减弱最后消失(抓了好久才抓住一张图....)

还有就是最常见的LOL中的塔,我们把鼠标移动到塔上,就会有很明显的描边效果:


简单描边效果的原理


描边效果有几种实现方式。其实边缘光效果与描边效果有些类似,适当调整边缘光效果,其实也可以达到凸显要表达的对象的意思。边缘光的实现最为简单,只是在计算的时候增加了一次计算法线方向与视线方向的夹角计算,用1减去结果作为系数乘以一个边缘光颜色就达到了边缘光的效果,是性能最好的一种方法,关于边缘光效果,可以参考一下之前的一篇文章: 边缘光效果。边缘光的效果如下图所示:

原始模型渲染:

使用了边缘光的效果:


边缘光效果虽然简单,但是有很大的局限性,边缘光效果只是在当前模型本身的光照计算时调整了边缘位置的颜色值,并没有达到真正的“描边”(当然,有时候我们就是想要这种边缘光的效果),而我们希望的描边效果,一般都是在正常模型的渲染状态下,在模型外面扩展出一个描边的效果。既然要让模型的形状有所改变(向外拓一点),那么肯定就和vertex shader有关系了。而我们的描边效果,肯定就是要让模型更“胖”一点,能够把我们原来的大小包裹住;微观一点来看,一个面,如果我们让它向外拓展,而我们指的外,也就是这个面的法线所指向的方向,那么就让这个面朝着法线的方向平移一点;再微观一点来看,对于顶点来说,也就是我们的vertex shader真正要写的内容了,我们正常计算顶点的时候,传入的vertex会经过MVP变换,最终传递给fragment shader,那么我们就可以在这一步让顶点沿着法线的方向稍微平移一些。我们在描边后,描边这一次渲染的边缘其实是没有办法和我们正常的模型进行区分的,为了解决这个问题,就需要用两个Pass来渲染,第一个Pass渲染描边的效果,进行外拓,而第二个Pass进行原本效果的渲染,这样,后面显示的就是稍微“胖”一点的模型,然后正常的模型贴在上面,把中间的部分挡住,边缘挡不住就露出了描边的部分了。

开启深度写入,剔除正面的描边效果


知道了原理,我们来考虑一下外拓的实现,我们可以在vertex阶段获得顶点的坐标,并且有法线的坐标,最直接的方式就是直接用顶点坐标+法线方向*描边粗细参数,然后用这个偏移的坐标值再进行MVP变换;但是这样做有一个弊端,其实就是我们透视的近大远小的问题,模型上离相机近的地方描边效果较粗,而远的地方描边效果较细。一种解决的方案是先进行MPV变换,变换完之后再去按照法线方向调整外拓。代码如下:
//描边Shader
//by:puppet_master
//2017.1.5

Shader "ApcShader/Outline"
{
	//属性
	Properties{
		_Diffuse("Diffuse", Color) = (1,1,1,1)
		_OutlineCol("OutlineCol", Color) = (1,0,0,1)
		_OutlineFactor("OutlineFactor", Range(0,1)) = 0.1
		_MainTex("Base 2D", 2D) = "white"{}
	}

	//子着色器	
	SubShader
	{
		
		//描边使用两个Pass,第一个pass沿法线挤出一点,只输出描边的颜色
		Pass
		{
			//剔除正面,只渲染背面,对于大多数模型适用,不过如果需要背面的,就有问题了
			Cull Front
			
			CGPROGRAM
			#include "UnityCG.cginc"
			fixed4 _OutlineCol;
			float _OutlineFactor;
			
			struct v2f
			{
				float4 pos : SV_POSITION;
			};
			
			v2f vert(appdata_full v)
			{
				v2f o;
				//在vertex阶段,每个顶点按照法线的方向偏移一部分,不过这种会造成近大远小的透视问题
				//v.vertex.xyz += v.normal * _OutlineFactor;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				//将法线方向转换到视空间
				float3 vnormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
				//将视空间法线xy坐标转化到投影空间,只有xy需要,z深度不需要了
				float2 offset = TransformViewToProjection(vnormal.xy);
				//在最终投影阶段输出进行偏移操作
				o.pos.xy += offset * _OutlineFactor;
				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target
			{
				//这个Pass直接输出描边颜色
				return _OutlineCol;
			}
			
			//使用vert函数和frag函数
			#pragma vertex vert
			#pragma fragment frag
			ENDCG
		}
		
		//正常着色的Pass
		Pass
		{
			CGPROGRAM	
		
			//引入头文件
			#include "Lighting.cginc"
			//定义Properties中的变量
			fixed4 _Diffuse;
			sampler2D _MainTex;
			//使用了TRANSFROM_TEX宏就需要定义XXX_ST
			float4 _MainTex_ST;

			//定义结构体:vertex shader阶段输出的内容
			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float2 uv : TEXCOORD1;
			};

			//定义顶点shader,参数直接使用appdata_base(包含position, noramal, texcoord)
			v2f vert(appdata_base v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				//通过TRANSFORM_TEX宏转化纹理坐标,主要处理了Offset和Tiling的改变,默认时等同于o.uv = v.texcoord.xy;
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.worldNormal = mul(v.normal, (float3x3)_World2Object);
				return o;
			}

			//定义片元shader
			fixed4 frag(v2f i) : SV_Target
			{
				//unity自身的diffuse也是带了环境光,这里我们也增加一下环境光
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;
				//归一化法线,即使在vert归一化也不行,从vert到frag阶段有差值处理,传入的法线方向并不是vertex shader直接传出的
				fixed3 worldNormal = normalize(i.worldNormal);
				//把光照方向归一化
				fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
				//根据半兰伯特模型计算像素的光照信息
				fixed3 lambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
				//最终输出颜色为lambert光强*材质diffuse颜色*光颜色
				fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;
				//进行纹理采样
				fixed4 color = tex2D(_MainTex, i.uv);
				color.rgb = color.rgb* diffuse;
				return fixed4(color);
			}

			//使用vert函数和frag函数
			#pragma vertex vert
			#pragma fragment frag	

			ENDCG
		}
	}
	//前面的Shader失效的话,使用默认的Diffuse
	FallBack "Diffuse"
}
开启了描边效果:

原始模型渲染采用了 半兰伯特Diffuse进行渲染,主要是前面多了一个描边的Pass。这个Pass里,我们没有关闭深度写入,主要是开启了模型的正面剔除,这样,在这个Pass渲染的时候,就只会渲染模型的背面,让背面向外拓展一下,既不会影响什么,并且背面一般都在正面的后面,一般情况下不会遮挡住正面,正好符合我们后面的部分外拓的需求。这个的主要优点是没有关闭深度写入,因为关闭深度写入,引入的其他问题实在是太多了。
附上一张进行了Cull Front操作的效果,只渲染了我们正常看不到的面,效果比较惊悚:

然后再来看看转换的部分,我们通过UNITY_MATRIX_IT_MV矩阵将法线转换到视空间,这里可能会比较好奇,为什么不用正常的顶点转化矩阵来转化法线,其实主要原因是如果按照顶点的转换方式,对于非均匀缩放(scalex, scaley,scalez不一致)时,会导致变换的法线归一化后与面不垂直。如下图所示,左边是变化前的,而中间是沿x轴缩放了0.5倍的情况,显然变化后就不满足法线的性质了,而最右边的才是我们希望的结果。造成这一现象的主要原因是法线只能保证方向的一致性,而不能保证位置的一致性;顶点可以经过坐标变换变换到正确的位置,但是法线是一个向量,我们不能直接使用顶点的变换矩阵进行变换。
我们可以推导一个法线的变换矩阵,就能够保证转化后的法线与面垂直,法线的变换矩阵为模型变换矩阵的逆转置矩阵。具体推导过程可以参考 这篇文章。
在把法线变换到了视空间后,就可以取出其中只与xy面有关的部分,视空间的z轴近似于深度,我们只需要法线在x,y轴的方向,再通过TransformViewToProjection方法,将这个方向转化到投影空间,最后用这个方向加上经过MVP变换的坐标,实现轻微外拓的效果。(从网上和书上看到了不少在这一步计算的时候,又乘上了pos.z的操作,个人感觉没有太大的用处,而且会导致描边效果越远,线条越粗的情况,离远了就会出现一团黑的问题,所以把这个去掉了)

上面说过,一般情况下背面是在我们看到的后面的部分,但是理想很美好,现实很残酷,具体情况千差万别,比如我之前常用的一个模型,模型的袖子里面,其实用的就是背面,如果想要渲染,就需要关闭背面剔除(Cull Off),这种情况下,使用Cull Front只渲染背面,就有可能和第二次正常渲染的时候的背面穿插,造成效果不对的情况,比如:

不过,解决问题的方法肯定要比问题多,我们可以用深度操作神器Offset指令,控制深度测试,比如我们可以让渲染描边的Pass深度远离相机一点,这样就不会与正常的Pass穿插了,修改一下描边的Pass,其实只多了一句话Offset 1,1:
//描边使用两个Pass,第一个pass沿法线挤出一点,只输出描边的颜色
		Pass
		{
			//剔除正面,只渲染背面,对于大多数模型适用,不过如果需要背面的,就有问题了
			Cull Front
			//控制深度偏移,描边pass远离相机一些,防止与正常pass穿插
			Offset 1,1
			CGPROGRAM
			#include "UnityCG.cginc"
			fixed4 _OutlineCol;
			float _OutlineFactor;
			
			struct v2f
			{
				float4 pos : SV_POSITION;
			};
			
			v2f vert(appdata_full v)
			{
				v2f o;
				//在vertex阶段,每个顶点按照法线的方向偏移一部分,不过这种会造成近大远小的透视问题
				//v.vertex.xyz += v.normal * _OutlineFactor;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				//将法线方向转换到视空间
				float3 vnormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
				//将视空间法线xy坐标转化到投影空间
				float2 offset = TransformViewToProjection(vnormal.xy);
				//在最终投影阶段输出进行偏移操作
				o.pos.xy += offset * _OutlineFactor;
				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target
			{
				//这个Pass直接输出描边颜色
				return _OutlineCol;
			}
			
			//使用vert函数和frag函数
			#pragma vertex vert
			#pragma fragment frag
			ENDCG
		}
这样,我们的描边效果也可以支持不能背面剔除的模型了:
UnityShader描边实现整理 描边 1、基于观察角度和表面法线的轮廓线渲染 基本思路:以归一化的顶点到摄像机的向量和归一化的顶点法线向量的点乘为参考值,该值越小(越接近于0),该像素就越解决于边缘。 优点:简单快速,只需要一个Pass。 缺点:局限性大,很多模型渲染出的效果不尽人意。 Shader "Unlit/RimLight" { Properties { _RimColor("RimColor",Color) = (1,1,1,1) _RimPower("Power",Rang 阅读详情

相关推荐

Unity Shader Graph 2D - 角色的外描边效果

在2D游戏中,角色的外描边是比较常用效果,能够达到强调角色轮廓,增加立体感和可视度,并增强角色的视觉效果,让玩家更好的认识并于角色互动。从本质上来讲,角色的外描边是在基于原来图像资源的基础上向外延伸一定宽度的带颜色的距离。

Giver的博客 1872

[Unity]Shader利用Geometry处理实现描边效果

1.原理 利用几何着色器,生成新的顶点,用新生成的顶点,构建描边,在用模板测试剔除原模型,如下图 灰色三角形是模型原来的一个三角面,用几何着色器,延顶点法线向外,创建出新的顶点A,B,C。已知一个面的构成需要三个顶点,依次连接顶点,总共创建了6个新的面,这六个面就构成灰色三角形的描边。再用模板测试的原理对模型进行Mask,得到描边。 2.代码 Properties { [...

Hu_GoldDragon的博客 1277

【unity shader】unity游戏特效-遮挡显示效果 (含边缘光、描边效果版)

不知道你们有没有在玩Black Squad这个游戏啊 在被对手干掉时会有敌人高亮显示效果 (未被做掉时) (被做掉后高亮显示敌人位置) 明明敌人被不透明物体挡住却仍然可以被渲染出来 这效果要是能扔进自己的期末作品设计逼格会高不少的好吧?! 那么这么棒的效果该怎么在unity里做出来呢? 模板测试(Stencil Test)就是本文的主角 先上官方链接:Unity Manual 模板测试位于透明度测试之后,在深度测试之前。 GPU会比较模板缓存中的值来决定哪些像素通过了模板测试,通过的像素可以到下一步深度

weixin_43740653的博客 3132

Unity Shader Graph 2D - 角色外描边流动能量效果

之前我们完成了角色外描边,现在我们再来做更进一步的效果美化,给他加上一个流动的动画效果,让他看起来更加酷炫。外描边的效果实现可以参考这一篇文章,这里我将接着之前外描边的着色器进行功能添加。那么其原理是什么?就是在外描边的Alpha的数据中通过添加遮罩的移动来实现,遮罩的形状需要是不规则的,这样在移动的时候才会产生出流动的效果。

Giver的博客 1474

Unity Shader 实现描边OutLine效果

Shader实现描边流程大致为:对模型进行2遍(2个pass)绘制,第一遍(描边pass)在vertex shader中对模型沿顶点法线方向放大,fragment shader设置输出颜色为描边颜色;第二遍正常绘制模型,除被放大的部分外,其余被覆盖,这样就有了描边的效果。 实现代码如下: Shader "Custom/OutlineShader" { Properties { _Mai...

elegentbeauty的博客 1万+

Unity Shader - 2D描边效果

2D描边效果: 上面是实现2D描边效果的3种不同效果,左边为原图,右边为效果图 方式一:平均透明度法 原理:取每个像素及其周围的Alpha值进行平分,边界处因为对边界外也进行了采样,所以叠加后透明度小于原有透明度,将小于一定阈值的alpha设置为描边色;(适用于周围为透明效果的2D贴图) 代码实现如下: Shader "Custom/...

起个名字好难啊 2774

【Unity Shader 描边效果_案例分享】

描边效果Shader有多种实现方式,可以通过后处理和MatCap实现。 这次主要想展示的是通过两个Pass实现。 当Shader中有多个Pass时,渲染流程会安装顺序依次执行,于是后面的Pass效果会覆盖前面Pass所绘制的图像。 实现的逻辑是:第一层,将模型的顶点沿着法线方向膨胀一段距离,膨胀的距离就是描边的宽度,膨胀的距离可以作为属性开放到材质面板上,方便调用。然后再为膨胀之后的模型指定一个纯色进行着色,着色不需要参与任何光照交互,膨胀之后的纯色就是描边的颜色。将颜色也作为属性开放到材质面板,方便后期更

暴走约伯的博客 2635

Unity Shader 之 外轮廓描边的效果实现

Unity Shader 之 外轮廓描边的效果实现 目录 Unity Shader 之 外轮廓描边的效果实现 一、简单介绍 二、实现原理 三、注意事项 四、效果预览 五、实现步骤 一、简单介绍 Shader Language的发展方向是设计出在便携性方面可以和C++、Java等相比的高级语言,“赋予程序员灵活而方便的编程方式”,并“尽可能的控制渲染过程”同时“利用图形...

仙魁XAN 6769

Unity Shader实现高级描边效果课程

Unity提供了多种Shader类型,以适应不同场景的需求:顶点/片元Shader(Vertex/Frag Shader):是最基础的Shader类型,允许开发者对每个顶点和片元进行计算。表面Shader(Surface Shader):基于物理的渲染模型,适合处理复杂的光照和材质效果。:适用于在GPU上执行并行计算任务,如粒子系统和全局光照。在Unity3D中,表面着色器(Surface Shader)是一种特别为编写复杂材质而设计的高阶抽象。

weixin_42560991的博客 850

Unity Shader实现描边OutLine效果

本文实例为大家分享了Unity Shader实现描边OutLine效果的具体代码,供大家参考,具体内容如下 Shader实现描边流程大致为:对模型进行2遍(2个pass)绘制,第一遍(描边pass)在vertex shader中对模型沿顶点法线方向放大,fragment shader设置输出颜色为描边颜色;第二遍正常绘制模型,除被放大的部分外,其余被覆盖,这样就有了描边的效果。 实现代码如下: Shader "Custom/OutlineShader" { Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} _OutLineW

概述DeepLab系列(deeplab v1, deeplab v2, deeplab v3, deeplab v3+)

前言:图像分割是指像素级别的图像识别,即标注出图像中每个像素所属的对象类别。 语义分割更注重类别之间的区分,而实例分割更注重个体之间的区别。 DeepLab是由Google团队提出的一系列图像分割算法。 DeepLab v1 (2014年) Paper (ICLR 2015) Code 创新点: 空洞卷积(Atrous Conv); <解决编码过程中信号不断被下采样,细节丢失的问题> 全连接条件随机场(Fully-connected Conditional Random Field)。

ZSH12345678900的博客 5528

FreeSurfer安装避坑指南:从邮箱选择到环境变量设置的全流程解析

本文详细解析了在Windows系统上安装和配置FreeSurfer神经影像分析工具的全流程避坑指南。重点涵盖WSL2深度配置、软件版本选择、许可邮箱获取策略、环境变量模块化设置以及图形界面调试等关键步骤,帮助用户高效构建稳定的分析环境,避免常见安装陷阱。

weixin_29243063的博客 229

SSTI模板注入及绕过姿势(基于Python-Jinja2)

http://xmctf.top:8962/?name={{%22%22[%22\x5f\x5fclass\x5f\x5f%22]["\x5F\x5Fbase\x5F\x5F"]["\x5F\x5Fsubclasses\x5F\x5F"]()[233]["\x5F\x5Finit\x5F\x5F"]["\x5F\x5Fglobals\x5F\x5F"]["\x5F\x5Fbuiltins\x5F\x5F"]['eval']("\x5F\x5Fimport\x5F\x5F('os'))")}}

Y4tacker的博客 1万+

SQLite → PostgreSQL 迁移脚本与踩坑清单

本文提供了SQLite迁移到PostgreSQL的两种可靠路径:首选使用pgloader工具自动转换schema和数据,支持类型映射和并行复制;备选方案通过sqlite3导出CSV再用PostgreSQL的COPY命令导入,适合需要自定义清洗的场景。重点介绍了数据类型映射规则、迁移步骤、校验方法和常见问题,包括自增主键、布尔值、时间类型等关键差异处理。迁移后需进行行数核对、哈希抽样等校验,并给出了性能优化建议。最后强调迁移不仅是数据转移,更需要同步升级数据模型和索引策略,根据业务场景选择合适的数据库方案。

aifs2025的博客 1885

第十二节:C#工业控制编程基础--输出正弦波实验

第十二节:基于c#使用开发板输出正弦波实验 文章目录第十二节:基于c#使用开发板输出正弦波实验一、实验目的:了解采集卡DA相关函数的使用。二、实验内容:利用C#生成正弦波,然后用采集卡DA输出。三、实验步骤:1.打开Visual Studio 2013,新建窗体应用程序项目,配置好板卡dll文件。2.在窗体中放入控件,对控件进行布局排版,并修改他们的Name属性和Text属性。3.在Chart控件内选择Spline型图表,如图2。4.进入编程区域,在public partial class Form1 :

weixin_49237052的博客 2295

基于STM32设计的心率脉搏测量仪(项目资料ID:9)

本设计基于STM32F103单片机开发了一款便携式心率脉搏测量仪,通过光电传感器采集脉搏信号,经放大(约1200倍)、滤波(低通及四阶低通滤波)、比较及偏置电压调理后,由STM32进行ADC采样与数据处理。硬件采用Altium Designer设计,包含信号调理电路(限定心率范围43.4-800次/分钟)及电源管理模块;软件基于STM32CubeMX开发,利用队列存储和时间间隔平均算法优化精度,实现多终端显示(OLED实时心率/动画、MATLAB波形分析、蓝牙传输至手机)。系统支持自动启停(检测脉搏启

以培养嵌入式工程师为己任 1万+

基于深度学习的灰度图像气泡群识别与优化

灰度图像气泡群深度学习识别优化研究 本研究针对MATLAB实现的深度学习气泡群识别系统在从1445个点到4992个点扩展时出现的精度不稳定问题,提出了一套完整的解决方案。文章首先分析了问题根源,包括数据复杂度增加、类别不平衡、网络容量不足等因素,然后系统性地提出了数据预处理增强、网络架构优化、训练策略调整和后处理算法改进等优化措施。通过改进的U-Net架构、自适应直方图均衡化等技术创新,有效提升了模型在大规模气泡群图像上的识别精度和稳定性,为化工、生物医学等领域的灰度图像分析提供了可靠的技术支持。

max500600的博客 411

从零构建爬虫抓取故宫数字文物库:Python最新实战技术详解

熟悉了动态内容加载的网页结构解析技巧。掌握了 Playwright + BeautifulSoup 的组合用法。实现了从网页→JSON→详情→图像的完整数据闭环。掌握了结构化保存与可视化分析能力。

2201_76125261的博客 1305

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

上一篇: CG标准函数库
下一篇: 安卓相关内容整理
puppet_master
puppet_master 领域专家: C/C++技术领域 领域专家: C/C++技术领域
博客等级 码龄12年 2695粉丝 161原创
评论 28
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值