梦开始的地方
相信大家看番的时候,都注意到了,很多时候,在角色周围有一圈光晕
旧版《魔术快斗》剧照
《新蔷薇少女》剧照
我们将这种光晕,称之为边缘光
边缘光是描边的一种,动画师之所以加入边缘光,是为了凸现角色轮廓,使得角色区别于背景
不少游戏也有着这种边缘光
游戏《鸣潮》
游戏《原神》
经过观察,我们不难发现,边缘光与暗部的交界轮廓,与角色外轮廓基本一致
画师画边缘光时,有时会在绘图软件中,直接复制角色的图层,运用正片叠底发光等效果,线性减淡,以达到边缘光的效果(将图层涂黑便是描边)
画师的工程文件截图
但是在unity里面,我们要检测获取角色的轮廓呢?
或许我们可以通过片原的法向量与视线向量的夹角判断?
当该片原法向量与视线向量的夹角,大于某个角度(如80)度,则视为该片原处于边缘
我们不妨看看效果
效果图1
可以看到,在脸颊,出现了十分诡异的边缘光,显然这不是我们想要的效果
正片开始
我们可以通过unity中的深度图,来获取角色轮廓
参考文章:屏幕空间等距边缘光 - 哔哩哔哩
在unity shader中,其实存储了物体在屏幕空间中的深度信息,我们将存储了这个信息的图,称之为深度图(其中的数学原理,上述文章讲的已经非常非常好了,想知道数学原理的可以直接看参考文献)
我们先来看看深度图长什么样
unity中的深度图
深度图是一张灰度图,深度越小,越黑,反之越白,而角色与其背景的深度在大部分时候是不一样的,这意味着我们可以通过屏幕深度来获取角色轮廓。
判断轮廓的思路
我们将深度图的片元的uv坐标向左(或者向右)进行偏移,将偏移前后的深度值(在深度图中体现为RGB值)进行比较,如果差值大于某个值,则视为该片元处于边缘
采样过程(片原向左偏移边缘光出现在角色左边,向右偏移则出现在角色右边)
代码实现(部分)
首先我们需要一个脚本开启摄像机的深度模式,将该脚本挂于摄像机下
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GetDepth : MonoBehaviour
{
private Camera currentCamera = null;
void Awake()
{
currentCamera = GetComponent<Camera>();
}
void Start()
{
currentCamera.depthTextureMode = DepthTextureMode.Depth;
}
void Update()
{
//currentCamera.depthTextureMode = DepthTextureMode.Depth;
}
}
此外,有了采样的思路代码就很好实现了(CG语言)
float2 offectSamplePos = screenParams01-float2(_RimOffect/input.clipW,0);//向右采样则是+
float offcetDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, offectSamplePos);
float trueDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, screenParams01);//等价于tex2D(_CameraDepthTexture, screenParams01);
float linear01EyeOffectDepth = Linear01Depth(offcetDepth);
float linear01EyeTrueDepth = Linear01Depth(trueDepth);
float depthDiffer = linear01EyeOffectDepth-linear01EyeTrueDepth;
float rimIntensity = step(_Threshold,depthDiffer);
效果
基于屏幕深度的边缘光效果图1
基于屏幕深度的边缘光效果图2
可是此时,角色只有一边拥有边缘光。
我想让角色两边都有边缘光,该如何实现呢?
其实原理大同小异,只是这次,我们需要在角色两边都进行采样(uv坐标同时向两侧偏移)
采样过程
代码实现(部分)
float2 offectSamplePosLeft = screenParams01-float2(_RimOffect/input.clipW,0);
float2 offectSamplePosRight = screenParams01+float2(_RimOffect/input.clipW,0);
float offcetDepthLeft = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, offectSamplePosLeft);
float offcetDepthRight = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, offectSamplePosRight);
float trueDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, screenParams01);
float linear01EyeOffectDepthLeft = Linear01Depth(offcetDepthLeft);
float linear01EyeOffectDepthRight = Linear01Depth(offcetDepthRight);
float linear01EyeTrueDepth = Linear01Depth(trueDepth);
float depthDifferLeft = linear01EyeOffectDepthLeft-linear01EyeTrueDepth;
float depthDifferRight = linear01EyeOffectDepthRight-linear01EyeTrueDepth;
float rimIntensityLeft = step(_Threshold,depthDifferLeft);
float rimIntensityRight = step(_Threshold,depthDifferRight);
float rimIntensity = max(rimIntensityLeft,rimIntensityRight);
效果
基于屏幕深度的边缘光效果图3
整合Shader
废话不多说了直接上代码(CG语言实现)
//This Shader just for study,not for project
//LZX-VS2022-2024-10-22-002
Shader "Toon/Rim"
{
Properties
{
[Header(Main)]
_MainTex("Main Tex",2D) = "white"{}
//Light Pass的参数还有其他pass的参数,因为我这个Shader只演示边缘光,所以只写了Rim Pass的参数
[Header(Rim)]
[Toggle] _SAMPING_BOTHSIDE("- SAMPING_BOTHSIDE ", float) = 0
_RimOffect("RimOffect",range(0,1)) = 0.5
_Threshold("RimThreshold",range(-1,1)) = 0.5
_RimLightStrength("Rim Light Strength",Range(1,10)) = 2
}
SubShader
{
Pass//Lighting Pass
{
//正常情况下Light Pass不可能这么简单,这个Light Pass只是为了让角色衣服正常显示
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MainTex;
struct c2v
{
float4 vertex:POSITION;
float3 normal:NORMAL;
float2 uv:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
float3 worldNormal:NORMAL;
float2 uv:TEXCOORD;
float3 objViewDir:COLOR1;
float3 normal:NORMAL2;
float clipW:TEXCOORD1;
};
v2f vert(c2v input)
{
v2f output;
output.pos = UnityObjectToClipPos(input.vertex);
output.worldNormal = normalize( mul((float3x3)unity_ObjectToWorld,input.normal) );
output.uv = input.uv;
float3 ObjViewDir = normalize(ObjSpaceViewDir(input.vertex));
output.objViewDir = ObjViewDir;
output.normal = normalize(input.normal);
output.clipW = output.pos.w;
return output;
}
fixed4 frag(in v2f input) : SV_Target
{
return tex2D(_MainTex, input.uv);
}
ENDCG
}
Pass//Rim Light Pass
{
Tags { "RenderType" = "Transparent" "Queue" = "Transparent" "IgnoreProjector" = "True"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma shader_feature _SAMPING_BOTHSIDE_ON
#pragma multi_compile_fog
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MainTex;
float _RimOffect;
float _Threshold;
sampler2D _CameraDepthTexture;
float _RimLightStrength;
struct c2v
{
float4 vertex:POSITION;
float3 normal:NORMAL;
float2 uv:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
float3 worldNormal:NORMAL;
float2 uv:TEXCOORD;
float3 objViewDir:COLOR1;
float3 normal:NORMAL2;
float clipW:TEXCOORD1;
};
v2f vert(c2v input)
{
v2f output;
output.pos = UnityObjectToClipPos(input.vertex);
output.worldNormal = normalize( mul((float3x3)unity_ObjectToWorld,input.normal) );
output.uv = input.uv;
float3 ObjViewDir = normalize(ObjSpaceViewDir(input.vertex));
output.objViewDir = ObjViewDir;
output.normal = normalize(input.normal);
output.clipW = output.pos.w;
return output;
}
fixed4 frag(in v2f input) : SV_Target
{
fixed3 originCol = tex2D(_MainTex, input.uv).rgb;
float2 screenParams01 = float2(input.pos.x/_ScreenParams.x,input.pos.y/_ScreenParams.y);
#ifdef _SAMPING_BOTHSIDE_ON
//两边都有边缘光
float2 offectSamplePosLeft = screenParams01-float2(_RimOffect/input.clipW,0);
float2 offectSamplePosRight = screenParams01+float2(_RimOffect/input.clipW,0);
float offcetDepthLeft = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, offectSamplePosLeft);
float offcetDepthRight = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, offectSamplePosRight);
float trueDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, screenParams01);
float linear01EyeOffectDepthLeft = Linear01Depth(offcetDepthLeft);
float linear01EyeOffectDepthRight = Linear01Depth(offcetDepthRight);
float linear01EyeTrueDepth = Linear01Depth(trueDepth);
float depthDifferLeft = linear01EyeOffectDepthLeft-linear01EyeTrueDepth;
float depthDifferRight = linear01EyeOffectDepthRight-linear01EyeTrueDepth;
float rimIntensityLeft = step(_Threshold,depthDifferLeft);
float rimIntensityRight = step(_Threshold,depthDifferRight);
float rimIntensity = max(rimIntensityLeft,rimIntensityRight);
#else
//只有一边有边缘光
float2 offectSamplePos = screenParams01-float2(_RimOffect/input.clipW,0);
float offcetDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, offectSamplePos);
float trueDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, screenParams01);
float linear01EyeOffectDepth = Linear01Depth(offcetDepth);
float linear01EyeTrueDepth = Linear01Depth(trueDepth);
float depthDiffer = linear01EyeOffectDepth-linear01EyeTrueDepth;
float rimIntensity = step(_Threshold,depthDiffer);
#endif
return fixed4(originCol * _RimLightStrength,rimIntensity);
}
ENDCG
}
}
FallBack "Diffuse"
}
补充
实际上,这种基于屏幕深度的边缘光/描边普遍存在一个bug,那就是当角色处于屏幕边缘时,描边可能断开
Bug
游戏《绝区零》中出现的Bug
这是由于在屏幕空间中,角色模型的三角面被切割导致的
不过整体来说,无伤大雅
标签:深度图,图片说明,float,Shader,边缘,CameraDepthTexture,output,input,float2 From: https://blog.csdn.net/m0_49904624/article/details/143157194