unity 3d物体描边效果_在URP实现局部后处理描边

欢迎参与讨论,转载请注明出处。

前言

最近在Demo开发的过程中,遇到了一个细节问题,场景模型之间的边界感很弱:

3170e42a77b1cf091bcaa274fb46bb2b.png


  这样就会导致玩家难以分辨接下来面对的究竟是可以跳下去的台阶,亦或是要跳过去的台阶了。我们想到的解决方法便是给场景模型加个外描边,以此区分:

8ca6de9ec3d8359e35aa4c3aef2be164.png


  整挺好,于是本文就来介绍一下实现思路。首先按照惯性我们直接采用了与人物相同的法线外扩描边,但是效果却不尽人意:

cb4bae7eb140f7171abfd4f93e6c90f3.png


  这完全就牛头不对马嘴,既然老办法不好使那就看看后处理描边吧。不过由于Demo使用的渲染管线是URP,在后处理这块与原生完全不同。于是乎再一次踏上了踩坑之旅…… 另附源码地址:https://github.com/MusouCrow/TypeOutline

RenderFeature

  经过调查发现,URP除了Post-processing之外,并没有直接提供屏幕后处理的方案。而URP的Post-processing尚不稳定(与原生产生了版本分裂),所以还是去寻找更稳妥的方式。根据官方例程找到了实现屏幕后处理描边的方式,当然它们的描边实现方式很搓,并不适合我们项目。于是取其精华去其糟粕,发现了其实现后处理的关键:RenderFeature。

a48437c392283609400ff115a1ec3d3b.png


  RenderFeature系属于URP的配置三件套之一的Forward Renderer,你可以在该配置文件里添加想要的RenderFeature,可以将它看做是一种自定义的渲染行为,通过CommandBuffer提交自己的渲染命令到任一渲染时点(如渲染不透明物体后、进行后处理之前)。URP默认只提供了RenderObjects这一RenderFeature,作用是使用特定的材质,在某个渲染时机,对某些Layer的对象进行一遍渲染。这显然不是我们所需要的,所幸官方例程里提供了我们想要的RenderFeature——Blit,它提供了根据材质、且材质可获取屏幕贴图,并渲染到屏幕上的功能:

Shader "Custom/Test"
{
Properties
{ [HideInInspector]_MainTex("Base (RGB)", 2D) = "white" {}
}
SubShader
{
Pass
{
HLSLPROGRAM

#pragma vertex vert #pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);

struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
};

struct Varyings
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
};

Varyings vert(Attributes input)
{
Varyings output;

VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
output.vertex = vertexInput.positionCS;
output.uv = input.uv;

return output;
}

float4 frag(Varyings input) : SV_Target
{
float4 color = 1 - SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);

return color;
}

ENDHLSL
}
}
FallBack "Diffuse"
}
b3688827718aee804b3118271b7dba1c.png


  如此这般便实现了经典的反色效果,只要引入Blit的相关代码,然后在Forward Renderer文件进行RenderFeature的相关配置,并实现Shader与材质,即可生效。较之原生在MonoBehaviour做这种事,URP的设计明显更为合理。

Outline

  后处理部署完毕,接下来便是描边的实现了。按照正统的屏幕后处理做法,应该是基于一些屏幕贴图(深度、法线、颜色等),使用Sobel算子之类做边缘检测。然而也有一些杂技做法,如官方例程以及此篇。当然相同的是,它们都需要使用屏幕贴图作为依据来进行处理,不同的屏幕贴图会导致不一样的效果,如上文那篇就使用深度与法线结合的贴图,产生了内描边的效果。然而我们只需要外描边而已,所以使用深度贴图即可。
  深度贴图在URP的获取相当简单,只需要在RenderPipelineAsset文件将Depth Texture勾选,然后便可在后处理Shader通过_CameraDepthTexture变量获取:

012cdb0a7b540782e853ca9e410aca5f.png


  有了深度贴图,那么接下来逮着别人的Shader抄就完事了——然而那些杂技做法的效果通通不行:官方的更适合美式风格,上文那篇的做法在某些场合会产生奇怪的斑点。于是只好按照《UnityShader入门精要》的写法来了:

Shader "Custom/Outline"
{
Properties
{ [HideInInspector]_MainTex("Base (RGB)", 2D) = "white" {}
_Rate("Rate", Float) = 0.5
_Strength("Strength", Float) = 0.7
}
SubShader
{
Pass
{
HLSLPROGRAM

#pragma vertex vert #pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);

TEXTURE2D(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);
float4 _CameraDepthTexture_TexelSize;

float _Rate;
float _Strength;

struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
};

struct Varyings
{
float4 vertex : SV_POSITION;
float2 uv[9] : TEXCOORD0;
};

Varyings vert(Attributes input)
{
Varyings output;

VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
output.vertex = vertexInput.positionCS;

output.uv[0] = input.uv + _CameraDepthTexture_TexelSize.xy * half2(-1, -1) * _Rate;
output.uv[1] = input.uv + _CameraDepthTexture_TexelSize.xy * half2(0, -1) * _Rate;
output.uv[2] = input.uv + _CameraDepthTexture_TexelSize.xy * half2(1, -1) * _Rate;
output.uv[3] = input.uv + _CameraDepthTexture_TexelSize.xy * half2(-1, 0) * _Rate;
output.uv[4] = input.uv + _CameraDepthTexture_TexelSize.xy * half2(0, 0) * _Rate;
output.uv[5] = input.uv + _CameraDepthTexture_TexelSize.xy * half2(1, 0) * _Rate;
output.uv[6] = input.uv + _CameraDepthTexture_TexelSize.xy * half2(-1, 1) * _Rate;
output.uv[7] = input.uv + _CameraDepthTexture_TexelSize.xy * half2(0, 1) * _Rate;
output.uv[8] = input.uv + _CameraDepthTexture_TexelSize.xy * half2(1, 1) * _Rate;

return output;
}

float4 frag(Varyings input) : SV_Target
{
const half Gx[9] = {
-1, 0, 1,
-2, 0, 2,
-1, 0, 1
};

const half Gy[9] = {
-1, -2, -1,
0, 0, 0,
1, 2, 1
};

float edgeY = 0;
float edgeX = 0;
float luminance = 0;

float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv[4]);

for (int i = 0; i < 9; i++) {
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, input.uv[i]);
luminance = LinearEyeDepth(depth, _ZBufferParams) * 0.1;
edgeX += luminance * Gx[i];
edgeY += luminance * Gy[i];
}

float edge = (1 - abs(edgeX) - abs(edgeY));
edge = saturate(edge);

return lerp(color * _Strength, color, edge);
}

ENDHLSL
}
}
FallBack "Diffuse"
}
3b9eac5218fc6476133e2037eb873bc8.png


  很棒,但是可以看到,身为一般物件的方砖也被描边了,可我们想要的只是场景描边而已——于是进入了最后的难题:对特定对象的后处理。

Mask

  首先我们参考原生下的做法,利用模板测试的特性,对特定对象的Shader写入模板值,然后在后处理时根据模板值做判断是否处理,确实是个绝妙的做法——很可惜,在URP下我找不到能够生效的做法。根据上文那篇需要渲染出深度法线结合的屏幕贴图的需要,作者实现了一个新的RenderFeature:根据渲染对象们的某个Pass,渲染成一张新的屏幕贴图(可选择使用特定的材质,若不使用则是Pass的结果)。并可作为全局变量供后续的后处理Shader使用。我将之命名为RenderToTexture,这也是后处理常用的一种技术。
  有了这个便有了新的想法:为所有渲染对象的Shader添加新的Pass(名为Mask),该Pass根据参数配置决定渲染成怎样的颜色(需要描边为白色,不需要为黑色)。如此渲染成屏幕贴图后便可作为描边Shader的参考(下称Mask贴图),决定是否需要描边:

aa136e947f4311a7adf423a34a957422.png


  注意要为Mask贴图的底色设置为非黑色,否则与底色接壤的物件会描边失败。那么见证成果吧:

Shader "Custom/Outline"
{
Properties
{ [HideInInspector]_MainTex("Base (RGB)", 2D) = "white" {}
_Rate("Rate", Float) = 0.5
_Strength("Strength", Float) = 0.7
}
SubShader
{
Pass
{
HLSLPROGRAM

#pragma vertex vert #pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);

TEXTURE2D(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);
float4 _CameraDepthTexture_TexelSize;

TEXTURE2D(_MaskTexture);
SAMPLER(sampler_MaskTexture);

float _Rate;
float _Strength;

struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
};

struct Varyings
{
float4 vertex : SV_POSITION;
float2 uv[9] : TEXCOORD0;
};

Varyings vert(Attributes input)
{
Varyings output;

VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
output.vertex = vertexInput.positionCS;

output.uv[0] = input.uv + _CameraDepthTexture_TexelSize.xy * half2(-1, -1) * _Rate;
output.uv[1] = input.uv + _CameraDepthTexture_TexelSize.xy * half2(0, -1) * _Rate;
output.uv[2] = input.uv + _CameraDepthTexture_TexelSize.xy * half2(1, -1) * _Rate;
output.uv[3] = input.uv + _CameraDepthTexture_TexelSize.xy * half2(-1, 0) * _Rate;
output.uv[4] = input.uv + _CameraDepthTexture_TexelSize.xy * half2(0, 0) * _Rate;
output.uv[5] = input.uv + _CameraDepthTexture_TexelSize.xy * half2(1, 0) * _Rate;
output.uv[6] = input.uv + _CameraDepthTexture_TexelSize.xy * half2(-1, 1) * _Rate;
output.uv[7] = input.uv + _CameraDepthTexture_TexelSize.xy * half2(0, 1) * _Rate;
output.uv[8] = input.uv + _CameraDepthTexture_TexelSize.xy * half2(1, 1) * _Rate;

return output;
}

float4 frag(Varyings input) : SV_Target
{
const half Gx[9] = {
-1, 0, 1,
-2, 0, 2,
-1, 0, 1
};

const half Gy[9] = {
-1, -2, -1,
0, 0, 0,
1, 2, 1
};

float edgeY = 0;
float edgeX = 0;
float luminance = 0;

float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv[4]);
float mask = 1;

for (int i = 0; i < 9; i++) {
mask *= SAMPLE_DEPTH_TEXTURE(_MaskTexture, sampler_MaskTexture, input.uv[i]);
}

if (mask == 0) {
return color;
}

for (int i = 0; i < 9; i++) {
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, input.uv[i]);
luminance = LinearEyeDepth(depth, _ZBufferParams) * 0.1;
edgeX += luminance * Gx[i];
edgeY += luminance * Gy[i];
}

float edge = (1 - abs(edgeX) - abs(edgeY));
edge = saturate(edge);

return lerp(color * _Strength, color, edge);
}

ENDHLSL
}
}
FallBack "Diffuse"
}
c116c9fd672feb78aa90a61e30c5e414.png


  很棒,这下一般物件不会被描边了,局部后处理描边完成!当然随后遇到一个新的问题:

39072b69d8037cddea74b58186c681ee.png


  这是因为透明(Transparent)模式下的对象按照通用做法是不会写入深度信息的(为了透明时能看到模型内部),然而我们描边需要的正是深度信息,由于树叶没有写入深度信息,所以在描边时当它不存在了,于是产生了这样的结果。解决方法也好办,在透明模式也写入深度信息(ZWrite)即可,毕竟我们的透明模型不需要看到内部,一举两得。

后记

  其实期间还产生了投机心理,想着把角色自带的描边给废了,统一后处理,岂不美哉?很可惜搞出来的效果始终是不满意,法线外扩 is Good,没办法喽——
  顺带一提,对于后处理的贴图创建记得将msaaSamples属性设为1,否则就会进行抗锯齿处理,那可真的炸裂……

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Unity3D是一款非常流行的游戏开发引擎,它提供了丰富的功能和工具,使开发人员能够轻松创建各种类型的游戏。其中一个强大的功能就是模型描边shader,它可以增强游戏中的视觉效果模型描边shader是一种特殊的着色器程序,用于给游戏中的3D模型添加轮廓线。通过描边shader,我们可以让模型的边缘线条更加清晰、醒目,让物体在场景中更加凸显出来。 使用Unity3D模型描边shader非常方便。首先,我们需要将描边shader应用于相应的材质上。然后,通过调整一些参数,如描边宽度、颜色等,可以实现不同的效果。在游戏运行时,模型的边缘线条就会被自动描绘出来,使得物体在场景中更加鲜明。 这个功能对于游戏开发人员来说非常实用。通过使用模型描边shader,我们可以增强游戏中物体的可视性,使它们在复杂的环境中更容易被玩家注意到。而且,模型描边shader还可以用于创建一些特殊效果,如描边发光等,让游戏更加生动有趣。 总的来说,Unity3D模型描边shader是一个非常方便实用的功能。它提供了丰富的定制选项,使开发人员能够轻松地改善游戏的视觉效果。无论是用于增强物体的可视性,还是为游戏添加特殊效果模型描边shader都是一个非常有用的工具,可以使游戏更加出色。 ### 回答2: Unity3D模型描边Shader是一种非常方便实用且强大的技术。该Shader能够为模型的边缘添加一个特殊的描边效果,从而使模型在视觉上更加突出和立体。 使用Unity3D模型描边Shader非常方便。开发者只需将该Shader应用于目标模型的材质中,然后调整描边效果的参数即可。这些参数包括描边的颜色、宽度、锐度等。通过简单的参数调整,开发者可以获得不同效果描边效果。 此外,Unity3D模型描边Shader还具有强大的功能。开发者可以根据需要自定义描边的样式,如添加渐变效果、使用图片纹理等。这样,描边效果不再局限于简单的线条,而是可以根据需求进行更加丰富多样的设计。 使用Unity3D模型描边Shader可以带来许多好处。首先,它能够增强模型的可视性,使其在游戏或应用中更加突出。其次,描边效果能够帮助玩家或用户更好地理解模型的形状和结构。再次,通过调整描边的参数,开发者可以在远近不同的场景中实现不同的效果,提升用户体验。 总之,Unity3D模型描边Shader是一种非常方便实用且强大的技术。它不仅能够为模型增加视觉效果,还能够提升用户体验,使游戏或应用的视觉呈现更加出色和吸引人。 ### 回答3: Unity3D模型描边Shader是一种非常方便实用且强大的功能。这个Shader可以在模型的边缘添加一条描边效果,使得模型在场景中更加突出和鲜明。 使用Unity3D模型描边Shader非常简单。首先,我们需要在Unity中创建一个材质,并将该材质的Shader类型设置为描边Shader。接下来,我们可以调整描边的颜色、宽度和透明度等属性,以满足我们的需求。 当我们将这个材质应用到模型上时,模型的边缘将自动显示出描边效果。不仅如此,Unity还提供了一些额外的功能,如控制描边的深度和三维感以及添加高光效果等。 使用Unity3D模型描边Shader的好处不仅仅是增加模型的视觉效果,它还可以提高游戏的性能。相较于其他方法,使用Shader进行描边可以在不增加更多几何体或复制模型的情况下实现,从而减少显卡的负载。 此外,Unity3D模型描边Shader还可以很容易地与其他特效和Shader进行结合。我们可以通过修改Shader代码来实现更多的效果,如闪烁、动态描边和不同颜色的描边等。这使得我们可以根据具体的游戏需求来定制和优化描边效果。 综上所述,Unity3D模型描边Shader是一项非常方便实用且强大的功能。它可以轻松地为模型添加描边效果,提高游戏的视觉效果和性能。无论是开发者还是玩家,都可以从中受益并创造出更加出色的游戏作品。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值