Unity3d游戏角色描边
本文发布于游戏程序员刘宇的个人博客,欢迎转载,请注明来源https://www.cnblogs.com/xiaohutu/p/10834491.html
游戏里经常需要在角色上做描边,这里总结一下平时几种常见的描边做法。
一,两批次法:
优点是简单,效果直接,性价比高。
1. 定点对着法线方向外移,缺点是可以看出顶点之间有断裂

Shader "ly/Outline_2Pass_1"
{
Properties
{
_MainTex("Texture", 2D) = "white"{}
_Outline(, )) = 0.02
_OutlineColor(,,,)
}
SubShader
{
//第一个批次,画描边
Pass
{
//Cull掉前面的一半,只让描边显示在后面
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
fixed _Outline;
fixed4 _OutlineColor;
struct v2f
{
float4 pos : SV_POSITION;
float4 color : COLOR;
};
v2f vert (appdata_full v)
{
v2f o;
//源顶点位置添加法线方向乘以参数的偏移量
v.vertex.xyz += v.normal * _Outline;
//位置从自身坐标系转换到投影空间
//旧版本o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
o.pos = UnityObjectToClipPos(v.vertex);
//描边颜色
o.color = _OutlineColor;
return o;
}
float4 frag (v2f i) : COLOR
{
return i.color; //描边
}
ENDCG
}
//第二个批次
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_ST;
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
fixed4 color : COLOR;
};
v2f vert(appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
o.color = fixed4(, , , );
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
2. 得到法线在投影空间上的xy轴,作为偏移方向将顶点外移,得到的结果类似1,也有断裂

3. 顶点的位置作为方向矢量,则不会因为方向差距较大而断裂

Shader "ly/Outline_2Pass_2"
{
Properties
{
_MainTex("Texture", 2D) = "white"{}
_Outline(, )) = 0.02
_OutlineColor(,,,)
}
SubShader
{
//第一个批次,画描边
Pass
{
//Cull掉前面的一半,只让描边显示在后面
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
fixed _Outline;
fixed4 _OutlineColor;
struct v2f
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
};
v2f vert (appdata_full v)
{
v2f o;
//位置从自身坐标系转换到投影空间
//旧版本o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
o.pos = UnityObjectToClipPos(v.vertex);
//方式二,扩张顶点位置
//法线变换到投影空间
//float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
//得到投影空间的偏移
//float2 offset = TransformViewToProjection(normal.xy);
////方式三,把顶点当做方向矢量,在方向矢量的方向偏移
float3 dir = normalize(v.vertex.xyz);
dir = mul((float3x3)UNITY_MATRIX_IT_MV, dir);
float2 offset = TransformViewToProjection(dir.xy);
//有一些情况下,侧边看不到,所以把方式一和二的算法相结合
//float3 dir = normalize(v.vertex.xyz);
//float3 dir2 = v.normal;
//float D = dot(dir, dir2);
//D = (1 + D / _Outline) / (1 + 1 / _Outline);
//dir = lerp(dir2, dir, D);
//dir = mul((float3x3)UNITY_MATRIX_IT_MV, dir);
//float2 offset = TransformViewToProjection(dir.xy);
//offset = normalize(offset);
//在xy两个方向上偏移顶点的位置
o.pos.xy += offset * o.pos.z * _Outline;
return o;
}
float4 frag (v2f i) : COLOR
{
return _OutlineColor; //描边
}
ENDCG
}
//第二个批次,略
}
二,边缘光
顶点的视角dir和法线dir点乘,得出偏离度,越靠近边缘,颜色的强度越高。
优点是节约批次。
v2f vert (appdata_full v)
{
v2f o;
//略 //_RimColor边缘光颜色 //_RimPower边缘光强度
float3 viewDir = normalize(ObjSpaceViewDir(v.vertex));
- dot(normalize(v.normal), viewDir);
fixed3 rimCol = smoothstep( - _RimPower, 1.0, dotProduct) * _RimColor;
o.color = rimCol;
//略
return o;
}
三,后处理方式来画描边
优点是效果完美,缺点是消耗性能。
摄像机上挂一个脚本,处理后处理的步骤,outlineCamera 为临时摄像机,参数与主摄像机相同,看着同样的Unit层。
临时摄像机渲染到RT上,先画剪影,然后用自定义的描边shader画上去。
using UnityEngine;
using UnitySampleAssets.ImageEffects;
[RequireComponent(typeof(Camera))]
[AddComponentMenu("Image Effects/Other/Post Effect Outline")]
class PostEffectOutline : PostEffectsBase
{
public enum OutLineMethod
{
eIteration,
eScale,
}
private Camera attachCamera;
private Camera outlineCamera;
private Shader simpleShader;
private Shader postOutlineShader;
private Material postOutlineMat;
private RenderTexture mTempRT;
, 1f, , 1f);// Color.green;
[Range(, )]
;
[Range(, )]
;
public OutLineMethod outlineMethod = OutLineMethod.eIteration;
void Awake()
{
FindShaders();
}
void FindShaders()
{
if (!simpleShader)
simpleShader = Shader.Find("ly/DrawSimple");
if (outlineMethod == OutLineMethod.eIteration)
{
if (!postOutlineShader)
postOutlineShader = Shader.Find("ly/PostOutlineIteration");
}
else
{
if (!postOutlineShader)
postOutlineShader = Shader.Find("ly/PostOutlineScale");
}
}
protected override void Start()
{
base.Start();
attachCamera = GetComponent<Camera>();
if (outlineCamera == null)
{
outlineCamera = new GameObject().AddComponent<Camera>();
outlineCamera.enabled = false;
outlineCamera.transform.parent = attachCamera.transform;
outlineCamera.name = "outlineCam";
}
postOutlineMat = new Material(postOutlineShader);
}
public override bool CheckResources()
{
CheckSupport(false);
if (!isSupported)
ReportAutoDisable();
return isSupported;
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (CheckResources() == false)
{
Graphics.Blit(source, destination);
return;
}
outlineCamera.CopyFrom(attachCamera);
outlineCamera.clearFlags = CameraClearFlags.Color;
outlineCamera.backgroundColor = Color.black;
outlineCamera.cullingMask = << LayerMask.NameToLayer("Unit");
if (mTempRT == null)
mTempRT = RenderTexture.GetTemporary(source.width, source.height, source.depth);
mTempRT.Create();
outlineCamera.targetTexture = mTempRT;
outlineCamera.RenderWithShader(simpleShader, "");
postOutlineMat.SetTexture("_SceneTex", source);
postOutlineMat.SetColor("_Color", outlineColor);
postOutlineMat.SetInt("_Width", outlineWidth);
postOutlineMat.SetInt("_Iterations", iterations);
//画描边混合材质
Graphics.Blit(mTempRT, destination, postOutlineMat);
mTempRT.Release();
}
}
先用简单的shader画出剪影
Shader "ly/DrawSimple"
{
FallBack OFF
}
然后就是这个自定义的描边shader画的过程。
第一种是类似高斯模糊的方式来迭代,迭代次数越多则越细腻。
// ly 类似高斯模糊方式迭代循环处理描边
Shader "ly/PostOutlineIteration"
{
Properties
{
_MainTex("Main Texture", 2D) = "black"{} //画完物体面积后的纹理
_SceneTex("Scene Texture", 2D) = "black"{} //原场景纹理
_Color(,,,0.8) //描边颜色
_Width( //描边宽度
_Iterations( //描边迭代次数(越多越平滑,消耗越高)
}
SubShader
{
Pass
{
CGPROGRAM
sampler2D _MainTex;
float2 _MainTex_TexelSize;
sampler2D _SceneTex;
fixed4 _Color;
float _Width;
int _Iterations;
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord.xy;
return o;
}
half4 frag(v2f i) : COLOR
{
//迭代次数为奇数,保证对称
+ ;
float ColorIntensityInRadius;
float Tx_x = _MainTex_TexelSize.x * _Width;
float Tx_y = _MainTex_TexelSize.y * _Width;
//计算是否大于0,则此像素属于外边的范围内
; k<iterations; k+=)
{
; j<iterations; j+=)
{
ColorIntensityInRadius += tex2D(_MainTex, i.uv.xy + float2((k - iterations / ) * Tx_x, (j - iterations / ) * Tx_y));
}
}
//如果有颜色,或者不在外边的范围内,则渲染原场景。否则,在外边内,渲染描边。
|| ColorIntensityInRadius == )
return tex2D(_SceneTex, i.uv);
else
- _Color.a)*tex2D(_SceneTex, i.uv);
}
ENDCG
}
}
}
第二种方法简单些,直接把剪影的部分uv扩大,再把原图叠上去。
// ly 扩张剪影uv来填充描边
Shader "ly/PostOutlineScale"
{
Properties
{
_MainTex("Main Texture", 2D) = "black"{} //画完物体面积后的纹理
_SceneTex("Scene Texture", 2D) = "black"{} //原场景纹理
_Color(,,,) //描边颜色
_Width( //描边宽度
}
SubShader
{
Pass
{
CGPROGRAM
sampler2D _MainTex;
sampler2D _SceneTex;
float2 _SceneTex_TexelSize;
fixed4 _Color;
float _Width;
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float4 pos : SV_POSITION;
half2 uv[] : TEXCOORD0;
half2 uv2[] : TEXCOORD2;
};
v2f vert(appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv[] = v.texcoord.xy;
o.uv[] = v.texcoord.xy;
half2 offs = _SceneTex_TexelSize.xy * _Width;
o.uv2[].x = v.texcoord.x - offs.x;
o.uv2[].y = v.texcoord.y - offs.y;
o.uv2[].x = v.texcoord.x + offs.x;
o.uv2[].y = v.texcoord.y - offs.y;
o.uv2[].x = v.texcoord.x + offs.x;
o.uv2[].y = v.texcoord.y + offs.y;
o.uv2[].x = v.texcoord.x - offs.x;
o.uv2[].y = v.texcoord.y + offs.y;
)
{
o.uv[].y = - o.uv[].y;
o.uv2[].y = - o.uv2[].y;
o.uv2[].y = - o.uv2[].y;
o.uv2[].y = - o.uv2[].y;
o.uv2[].y = - o.uv2[].y;
}
return o;
}
half4 frag(v2f i) : COLOR
{
fixed4 stencil = tex2D(_MainTex, i.uv[]);
// 有剪影的部分,显示原图
if (any(stencil.rgb))
{
fixed4 framebuffer = tex2D(_SceneTex, i.uv[]);
return framebuffer;
}
// 没有剪影的部分,先把剪影扩张,扩张出颜色的部分用剪影,没有颜色的用原图
else
{
fixed4 color1 = tex2D(_MainTex, i.uv2[]);
fixed4 color2 = tex2D(_MainTex, i.uv2[]);
fixed4 color3 = tex2D(_MainTex, i.uv2[]);
fixed4 color4 = tex2D(_MainTex, i.uv2[]);
fixed4 color;
color.rgb = max(color1.rgb, color2.rgb);
color.rgb = max(color.rgb, color3.rgb);
color.rgb = max(color.rgb, color4.rgb);
if (any(color.rgb))
{
return _Color;
//color.a = (color1.a + color2.a + color3.a + color4.a) * 0.25;
//return color;
}
else
{
fixed4 framebuffer = tex2D(_SceneTex, i.uv[]);
return framebuffer;
}
}
}
ENDCG
}
}
SubShader
{
Pass
{
SetTexture[_MainTex]{}
}
}
Fallback Off
}
Unity3d游戏角色描边的更多相关文章
- Unity3D 游戏开发构架篇 ——角色类的设计与持久化
在游戏开发中,游戏角色占了很大的篇幅,可以说游戏中所有的内容都是由主角所带动.这里就介绍一下角色类的设计和持久化. 一.角色类应用场景和设计思想 游戏中的角色类型不一而足,有不同的技能,有不同的属性等 ...
- Unity3D游戏开发从零单排(四) - 制作一个iOS游戏
提要 此篇是一个国外教程的翻译,尽管有点老,可是适合新手入门. 自己去写代码.debug,布置场景,能够收获到非常多.游戏邦上已经有前面两部分的译文,这里翻译的是游戏的最后一个部分. 欢迎回来 在第一 ...
- [Unity3D]Unity3D游戏开发3D选择场景中的对象,并显示轮廓效果强化版
大家好,我是秦培,欢迎关注我的博客,我的博客地址blog.csdn.net/qinyuanpei. 在上一篇文章中,我们通过自己定义着色器实现了一个简单的在3D游戏中选取.显示物体轮廓的实例. 在文章 ...
- [Unity3D]Unity3D游戏开发之跑酷游戏项目解说
大家好,我是秦元培.我參加了CSDN2014博客之星的评选,欢迎大家为我投票,同一时候希望在新的一年里大家能继续支持我的博客. 大家晚上好.我是秦元培,欢迎大家关注我的博客,我的博客地址是blog.c ...
- [Unity3D]Unity3D游戏开发之伤害数值显示
大家好,我是秦元培,欢迎大家关注我的博客,我的博客地址是blog.csdn.net/qinyuanpei.众所周知,在RPG游戏策划中最为重要的一个环节是数值策划.数值策划是一个关于游戏平衡方面的概念 ...
- [Unity3D]Unity3D游戏开发之怪物AI
大家好.欢迎大家关注由我为大家带来的Unity3D游戏开发系列文章,我的博客地址为:http://blog.csdn.net/qinyuanpei. 在上一篇文章中,我们基本上实现了一个 ...
- Unity3D游戏开发之C#编程中常见数据结构的比较
一.前言 Unity3D是如今最火爆的游戏开发引擎,它可以让我们能轻松创建诸如三维视频游戏.建筑可视化.实时三维动画等类型的互动内容.它支持2D/3D游戏开发,据不完全统计,目前国内80%的手机游戏都 ...
- Unity3D游戏开发初探—2.初步了解3D模型基础
一.什么是3D模型? 1.1 3D模型概述 简而言之,3D模型就是三维的.立体的模型,D是英文Dimensions的缩写. 3D模型也可以说是用3Ds MAX建造的立体模型,包括各种建筑.人物.植被. ...
- Unity3D游戏在iOS上因为trampolines闪退的原因与解决办法
http://7dot9.com/?p=444 http://whydoidoit.com/2012/08/20/unity-serializer-mono-and-trampolines/ 确定具体 ...
随机推荐
- H3C端口绑定技术简介
- 【t088】倒水
Time Limit: 1 second Memory Limit: 128 MB [问题描述] 一天辰辰买了N个容量可以认为是无限大的瓶子,开始时每个瓶子里有1升水.接着辰辰发现瓶子实在太多了,于是 ...
- 【62.89%】【BZOJ 1072】[SCOI2007]排列perm
Time Limit: 10 Sec Memory Limit: 128 MB Submit: 1862 Solved: 1171 [Submit][Status][Discuss] Descri ...
- Linux网络IO模型
同步和异步,阻塞和非阻塞 同步和异步 关注的是结果消息的通信机制 同步:同步的意思就是调用方需要主动等待结果的返回 异步:异步的意思就是不需要主动等待结果的返回,而是通过其他手段比如,状态通知,回调函 ...
- 新书《iOS编程(第6版)》抢鲜试读
我最近翻译了Big Nerd Ranch的<iOS编程(第6版)>.我用了大半年时间,尽可能做到通顺易懂.不足之处请大家多多指正.感谢辛苦审校的丁道骏同学. 这本书得过Jolt大奖,原书在 ...
- Python10_代码规范和可读性
养成好的编程习惯和方法对提升代码可读性至关重要. 1.类.模块.包:不要用下划线,命名要简短 2.类:命名最好以大写开头 3.模块.包:用小写单词 4.变量.函数.方法:可以用下划线提高可读性,尽量都 ...
- Python之end关键字使用
关键字end可以用于将结果输出到同一行,或者在输出的末尾添加不同的字符,实例如下: a, b = 0, 1 while b < 1000: print(b, end=',') a, b = b, ...
- 在Spring Boot中使用Docker在测试中进行高级功能测试
最近又学到了很多新知识,感谢优锐课老师细致地讲解,这篇博客记录下自己所学所想. 想更多地了解Spring Boot项目中的功能测试吗?这篇文章带你了解有关在测试中使用Docker容器的更多信息. 本文 ...
- linux-iostat、sar、top、htop
1.iostat 实时显示linux的任务 centos7中默认没有安装iostat,所以得提前安装sysstat yum install -y sysstat -c 统计cpu信息 -d 统计磁盘信 ...
- 洛谷$P$3327 约数个数和 $[SDOI2015]$ 莫比乌斯反演
正解:莫比乌斯反演 解题报告: 传送门! 先考虑证明一个结论,$d_{i\cdot j}=\sum_{p|i}\sum_{q|j}[gcd(p,q)==1]$ 看起来就很对的样子,但还是证下趴$QwQ ...