unity实现描边的问题汇总

本文深入探讨游戏开发中描边效果的实现,特别是在Unity引擎下使用Shader编程技巧。文章详细解释了模型边缘描边的原理,包括如何在顶点着色器中沿法线方向偏移顶点,以及如何解决描边过程中出现的各种问题,如断层、渲染顺序冲突和UI遮挡描边效果等。此外,还提供了实用的解决方案和注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在游戏中,我们都喜欢加一些描边效果,来凸显人物的边缘,提高识别度。美术一般都喜欢加。描边方式一般有两种,一种的模型边缘描边,一种的人物的转折点描边(这种需要用到卷轴)

在游戏中比较常用的就是模型边缘描边了,shader一般是这样

Shader "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
		{
			...
		}
	}
	//前面的Shader失效的话,使用默认的Diffuse
	FallBack "Diffuse"
}

第一个问题:但是,如果这样放到游戏中回有一定的问题

可以看到,一些边缘的地方其实没显示边缘色,导致看起来是断层的。

原因是我们剔除了前面,朝向前方的定点被剔除,所以显示不完整。然后我设置为后,

就比较完美了。然后我一位这样就解决了。

第二个问题:但是后面在主场景中发现一个问题可以看到角色整个黑掉了,开始找原因,首先找到的是,人物身体的mesh多了一份,然后把他移除掉一份,其实还是不行。然后因为我们用了两个pass,两个pass都是transparent的,但在frame debug中看渲染顺序也是正常的,说明和渲染顺序无关。

再后来,发现出现这个问题的情景时两个人物叠加在一起。

由于渲染了front,两个英雄的向前的点会重叠(因为渲染完一个人物的两个pass,再到另一个人物的)

然后我想到的解决办法时,加一个模板,但大于等于0的点就设置为Zero。最终解决了这个问题。

可以看到没有显示黑的一团。

第三个问题:在渲染3d和2d结合的界面,如果我们直接渲染3d(而不是用rt来间接实现)的话会出现看不到描边的问题。这个的原因就是因为ui是会在3d模型之后渲染的,然后我们描边是不回写深度的所以会看不到描边(因为由于没写深度,描边在ui后面)。

因为我们游戏用的是ngui,用uitexture来显示背景,所以会按照ui的方式显示在前面。所以我用了一个quad去实现背景,并且把渲染顺序改为geometry,这样就会显示在后面。

至此就解决了描边的遇到的问题了。

当然很多人可能会说这个挡板也会使描边显示不出来。

对,是会显示不出来,因为他也是ui。但是要根据实际情况并且消耗情况,制作复杂情况来决定。这个挡板没描边是可以接受的,所以我们用一个比较简单消耗低的方式实现。

当然如果你要完全显示描边,那就可以把rt放到camera的target上,然后用uitexture或quad去装载这个rt,就能完全实现,但这样会对一个相机,并且每帧都要去做这样的操作。具体需要自己权衡。

 

 

注意:

1。描边的对象菱角最好不要多,因为描边是通过法线拓开面的方式做的,所以面会往法线上移,所以菱角多可能会出现有些地方空了

2.计算过程如果一方归一化后另一方也要归一化,不然计算会涉及一些不相关的部位。

data.NdotV.x = saturate(dot(normalize(v.normal), normalize(V)));

3.其实这里光照计算不需要用到真实光照,只需要知道一个光照方向就好了,模拟出真实光照感觉。因为如果用真实光,如果你每个场景人物的旋转角度不一致,那么看上去是不一致的,那么这时要调整光照。那么调整光照方向就是最直接的。

float3 lightDirNorm = normalize(_LightDir.xyz);
data.lightDir = lightDirNorm;

之后只需要用这个光照方向去计算边缘光,暗部渲染,高光等就好了,注意如果用到matcap,matcao的高亮部位要跟这个方向一致。

4.做matcap的时候我们会发现摄像机越远他会出现越多的不相关暗部。

这里是因为摄像机角度渲染时mask图涉及的区域可能会超过范围,这里渲染的就会超过一点影响到贴图的其他部位。那么要减少这个影响,就应该根据摄像机的远近来处理uv。减少影响。

(step(uvx1, s.uv_main.x) * step(s.uv_main.x, uvx2)) > 0 ? (_UseMatCap > 0 ? ((mask.a != 0) ? float3(c.rgb * matMaskACapColor * 1.5) : float3(c.rgb * matCapColor * _MatCapColor * 1.5)) : c.rgb) : c.rgb;

(step(uvx1, s.uv_main.x) * step(s.uv_main.x, uvx2))是说uv的x限制在uvx1和uvx2之间。

当超过了这个像素 就用回c.rgb。其他部分都是matcap部分,就不细说了。

<think>嗯,用户现在在Unity中需要实现描边效果,并提供了站内引用的资料。之前我已经详细解释了两种方法,但可能需要更细致的步骤指导。首先,用户可能是个Unity开发者,对Shader有一定了解但需要具体实现步骤。用户提供的引用[1]和[2]分别提到了使用Shader和ShaderGraph的方法,以及后处理轮廓检测。考虑到用户可能希望有更多的实现选项,我应该涵盖不同复杂度的方案。回顾之前的回答,已经介绍了扩展网格法、PostProcessing和后处理轮廓检测。现在需要检查是否有遗漏的细节或优化点。比如扩展网格法中的顶点膨胀计算,在Shader中的具体代码可能需要更清晰的说明。另外,引用[1]中提到使用Tags中的RenderType和Queue设置,这一点在扩展网格方法中很重要,确保染顺序正确避免遮挡问题。用户深层需求可能是希望选择最合适自己项目的方法,可能对性能有要求。例如,如果项目是移动端,后处理方案虽然效果统一但可能有性能开销,而扩展网格法消耗较低但多物体时可能出错。需要强调不同方法的适用场景。引用[2]提到的ShaderGraph方法,用户可能对可视化编辑感兴趣,尤其是对不熟悉代码的美术人员更友好。这里应该补充ShaderGraph的具体步骤,比如如何用Position节点进行偏移,使用Backface节点剔除正面,再混合颜色。另外,用户提到“方法教程”,需要结构清晰的分步指导。例如扩展网格法的第一步创建材质,第步编写Shader,第步调整参数。对于后处理方案,需要解释如何设置RenderTexture和深度法线纹理,可能用户对此不太熟悉,需要更详细的步骤,比如在摄像机添加组件,调整参数等。还要注意引用中提到的属性分组(Header指令),这在材质面板中提升易用性,应该在示例代码中保留此细节,帮助用户管理参数。同时,性能对比部分很重要,帮助用户权衡选择方案,特别是针对不同平台或场景。最后,确保相关问题引导用户深入,比如轮廓线变色或剔除问题,覆盖用户后续可能遇到的问题。需要验证每个方法的示例代码正确性,比如法线扩展中的normal和viewDir计算是否正确,避免错误。此外,多光源处理需要注意事项,可能需要提到额外的Pass或光照模型调整。</think> 好的,在 Unity实现物体描边效果主要有几种常用且高效的方法,下面我将详细介绍并提供关键代码示例: **常用描边方法及实现教程:** 1. **基于法线/顶点膨胀扩展网格法 (Silhouette Expansion):** * **原理:** 复制原模型网格,将其沿顶点法线 *向外* 膨胀一定距离(描边宽度),形成第个稍大的“轮廓网格”。然后关闭该轮廓网格的正面染(只染背面),并赋予特定的描边颜色。 * **优点:** 实现相对直接,效果清晰,对封闭物体效果好,性能通常较好。 * **缺点:** 在物体边界处(尤其是凹面)可能出现断线或自交叉;复杂模型需要更精细处理。 * **实现步骤:** 1. **SubShader & Pass:** 创建一个包含两个 `Pass` 的 `SubShader`。第一个 `Pass` 染轮廓(描边),第个 `Pass` 染原始模型。 2. **轮廓 Pass:** * **关键标签 (Tags):** 设置染队列 (`"Queue"="Transparent"`) 确保轮廓在原物体前染,并设置正确的 `RenderType` (通常是 `"Opaque"` 或 `"Transparent"`)。使用 `Cull Front` 剔除正面(只染背面)。 * **顶点膨胀:** 在顶点着色器中,将顶点沿着其法线方向 (`v.normal`) 向外膨胀一定量。膨胀量由 `_OutlineWidth` 控制。 ```glsl // 轮廓 Pass Vertex Shader 关键部分 v2f vert (appdata v) { v2f o; // 膨胀顶点:顶点位置 + 法线方向 * 宽度 * (可选缩放因子,如投影坐标w避免透视失真) float4 pos = UnityObjectToClipPos(v.vertex); float4 norm = normalize(mul(UNITY_MATRIX_IT_MV, float4(v.normal, 0))); pos.xyz += norm.xyz * _OutlineWidth * (1 / pos.w); // 透视补偿常用处理 o.position = pos; o.color = _OutlineColor; return o; } ``` * **片段着色器:** 直接输出 `_OutlineColor`。 3. **原始模型 Pass:** * 使用正常的光照模型(如 `Surface Shader` 或标准 `frag` 函数)染物体原本的外观。 4. **材质属性:** 在 `Properties` 块中暴露 `_OutlineColor` 和 `_OutlineWidth`,使用 `[Header]` 分组: ```glsl Properties { // ... 原有材质属性 (如 _MainTex, _Color) ... [Header(Outline Properties)] // 在 Inspector 面板分组显示 _OutlineColor ("Outline Color", Color) = (0,0,0,1) _OutlineWidth ("Outline Width", Range(0.0, 0.1)) = 0.01 } ``` * **参考:**[^1] 2. **基于后处理全屏轮廓检测法 (Post-Processing Edge Detection):** * **原理:** 利用屏幕空间深度 (Depth) 或法线 (Normal) 纹理信息。比较相邻像素的深度差或法线方向差异。差异超过某个阈值的区域判定为轮廓边缘,然后对该区域进行描边绘制。 * **优点:** 对场景中所有符合规则的物体统一进行描边,效果一致。不受模型复杂度的显著影响。易于调节效果。 * **缺点:** 占用额外的染纹理资源,性能开销相对较大(尤其在低端设备)。可能会对非物体边缘(如深度跳跃处)产生误描边。需要了解深度纹理设置。 * **实现步骤 (需 Unity Post Processing Stack):** 1. **启用深度/法线纹理:** 确保主摄像机 (`Camera`) 的 `Depth Texture Mode` 设置为 `Depth` 或 `DepthNormals`。 2. **创建后处理材质:** 编写一个着色器,包含: ```glsl sampler2D _CameraDepthNormalsTexture; // 深度法线纹理 float _EdgeThreshold; // 检测阈值 float4 _EdgeColor; // 描边颜色 float _EdgeOnly; // 是否只显示边缘 ``` 3. **边缘检测:** 在片段着色器中,采样当前像素及其周围像素 (如上下左右四个邻域) 的深度法线值 (`SAMPLE_DEPTH_NORMALS_TEXTURE`)。计算当前像素与邻居在深度或法线方向上的差异(如 Sobel, Roberts Cross 算子)。如果差异总和超过 `_EdgeThreshold`,则该像素在轮廓上。 ```glsl // 示例核心检测逻辑 (Sobel简化版) half GetEdgeValue(float2 uv, float stepSize) { half top = SampleDepthNormalDifference(uv + float2(0, stepSize)); // 伪代码,需实现采样比较 half bottom = SampleDepthNormalDifference(uv + float2(0, -stepSize)); half left = SampleDepthNormalDifference(uv + float2(-stepSize, 0)); half right = SampleDepthNormalDifference(uv + float2(stepSize, 0)); return (abs(top - bottom) + abs(left - right)) > _EdgeThreshold; } ``` 4. **输出颜色:** 如果检测到边缘 (`edge > 0`),则输出 `_EdgeColor`。可选 `_EdgeOnly` 模式控制是全屏混合描边还是仅在边缘显示颜色。 5. **应用脚本:** 创建一个 MonoBehaviour 脚本附加到摄像机上,使用 `OnRenderImage` 方法将后处理材质应用到染图像上。 3. **基于 Shader Graph (图形化着色器编辑):** * **原理 (参考方法1):** Shader Graph 可视化了方法1的核心思路。 * **实现步骤:** 1. 创建一个 `Lit Shader Graph` 或 `Unlit Shader Graph`。 2. 添加 `Position` 和 `Normal` 节点获取顶点原始位置和法线。 3. 添加一个 `Vector1` 节点控制描边宽度 (`_OutlineWidth`)。 4. 创建一个描边位置:使用 `Position` 节点选择 `Object` 空间,连接到 `Add` 节点的一个输入;将 `Normal` 节点(选择 `Object` 空间)连接到 `Multiply` 节点,再乘以 `_OutlineWidth`,然后连接到 `Add` 节点的另一个输入 → `OutlinePosition = ObjectPosition + (ObjectNormal * OutlineWidth)`。 5. 将计算出的 `OutlinePosition` 连接到 `Master Stack` 的 `Vertex Position`。 6. 添加一个 `Color` 节点 (`_OutlineColor`) 连接到 `Master Stack` 的 `Base Color`。 7. 在 `Master Stack` 设置中: * `Surface Type` 根据需要选择(常为 `Opaque`)。 * `Double Sided` 开启。 * `Blend` 可选择需要的混合模式(如 `Alpha` 混合)。 * **最关键:** 在 `Fragment` 阶段,添加一个 `Back Face` 节点,只让其输出有效 (`Is Back Face`),将此布尔值连接到 `Base Color` 的 `Alpha`(或使用 `Branch` 节点控制输出 `_OutlineColor`)。另一种更精确的方法是使用 `Pass` → 添加一个 `Pass` 作为轮廓Pass(如方法1),在其中进行膨胀并设置 `Cull Front`。主流方案是使用 `Back Face` 配合膨胀。 8. **参考:**[^2] 4. **其他方法:** * **几何着色器:** 在几何着色器中直接沿角形边生成新的四边形几何体作为描边。非常灵活但相对复杂,需要处理连接缝,平台支持性(如WebGL受限)和性能需注意。 * **预计算轮廓线:** 在模型导入/编辑阶段生成轮廓线数据(如烘焙到贴图UV或生成曲线网格),运行时仅显示这部分数据。离线方法,运行时效率高但不灵活。 **选择建议:** * **针对单个物体/少量物体 + 性能要求:** **扩展网格法 (方法1) 通常是首选。** 实现直接,性能较好。 * **针对场景中大量物体统一描边或复杂物体:** **后处理法 (方法2) 更合适。** 效果一致,不受单个模型限制。 * **美术友好 / 快速原型 / 无需手写代码:** **Shader Graph (方法3)。** 可视化操作,非常直观。 * **特定复杂需求(如特殊轮廓线类型/风格化):** **几何着色器或离线方法。** 需要更高的技术储备。 **性能注意事项:** * **扩展网格法:** 复制了一个稍大的网格,增加了顶点数,Draw Calls+1。适合少量物体或性能要求不苛刻的场景。 * **后处理法:** 消耗在额外的纹理采样和全屏计算。低分辨率+优化的采样方式有助于降低开销。 * **Shader Graph:** 性能取决于生成的具体HLSL代码复杂度。 **关键优化方向:** * **精确控制描边宽度:** 在扩展网格法和Shader Graph中,引入 `_OutlineWidth * (1 / vertex.w)` 或类似逻辑做透视补偿,避免近大远小问题。 * **剔除与遮挡:** 确保轮廓染队列 (`Queue`) 正确设置在原始物体之前 (`Geometry+1` 或 `Transparent`),保证轮廓不会被自身遮挡。 **总结:** 实现 Unity 描边效果的核心在于选择 **扩展网格法**、**后处理全屏轮廓检测法** 或 **利用 Shader Graph 实现原理**。根据你的具体需求(是单个物体还是全局效果、美术/程序偏好、性能要求)选择最适合的方案。`Tags { "RenderType"="Opaque" "Queue"="Transparent" }` 和 `Cull Front` 是扩展网格法实现的关键指令之一 [^1][^2]。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值