赞
踩
效果一览
外描边是许多游戏的画面需求,通常大体分为法线外扩和后处理边缘检测两种,法线外扩通常用于特殊需求,如外描边高亮关键物体,选中外描边高亮等,后处理边缘检测画面表现力更强一点,通常用于全屏的风格化描边,如卡通渲染,素描风格画面等(其实我也不清楚,凭感觉应该有这样的使用趋向区别),本篇文章主要讲解法线外扩外描边效果。
基本原理
一个shader两个pass,第一个pass绘制法线外扩后的纯色,第二个pass正常绘制
具体细节
UnityShader代码如下
- Shader "Custom/Outline"
- {
- Properties{
- _MainTex("Texture", 2D) = "white"{}
- _Diffuse("DiffuseColor", Color) = (1,1,1,1)
- _Specular("SpecularColor",Color)=(1,1,1,1)
- _Gloss("Gloss",Range(8,256))=32
- _OutlineColor("OutlineColor", Color) = (1,0,0,1)
- _OutlineLength("OutlineLength", Range(0,1)) = 0.1
- }
-
- SubShader
- {
-
- //第一个pass,各顶点沿法线向外位移指定距离,只输出描边的纯颜色
- Pass
- {
- //剔除正面,只渲染背面,防止描边pass与正常渲染pass的模型交叉
- Cull Front
- //深度偏移操作,两个参数的数值越大,深度测试时该pass渲染的片元将获得比原先更大的深度值
- //即距离相机更远,更容易被正常渲染的pass覆盖,防止描边pass与正常渲染pass的模型交叉
- Offset 20,20
- //Zwrite Off
-
- CGPROGRAM
- #include "UnityCG.cginc"
- fixed4 _OutlineColor;
- float _OutlineLength;
-
- struct v2f
- {
- float4 pos : SV_POSITION;
- };
-
- v2f vert(appdata_full v)
- {
- v2f o;
- //在物体空间下,每个顶点沿法线位移,这种描边会造成近大远小的透视问题
- //v.vertex.xyz += v.normal * _OutlineLength;
- o.pos = UnityObjectToClipPos(v.vertex);
- //将法线方向转换到视空间,为接下来转换到投影空间做准备
- float3 normalView = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
- //将视空间法线xy坐标转换到投影空间,z深度不转换的原因是尽量避免垂直于视平面的顶点位移
- //防止描边pass与正常渲染pass的模型交叉
- float2 offset = TransformViewToProjection(normalView.xy);
- //最终在投影空间进行顶点沿法线位移操作
- o.pos.xy += offset * _OutlineLength;
- return o;
- }
-
- fixed4 frag(v2f i) : SV_Target
- {
- //这个Pass直接输出描边颜色
- return _OutlineColor;
- }
-
- #pragma vertex vert
- #pragma fragment frag
- ENDCG
- }
-
- //第二个pass利用Blinn-Phong着色模型正常渲染
- Pass
- {
- CGPROGRAM
-
- #include "Lighting.cginc"
-
- fixed4 _Diffuse;
- sampler2D _MainTex;
- //使用了TRANSFROM_TEX宏就需要定义XXX_ST
- float4 _MainTex_ST;
- fixed4 _Specular;
- float _Gloss;
-
- struct v2f
- {
- float4 pos : SV_POSITION;
- float3 worldNormal : TEXCOORD0;
- float2 uv : TEXCOORD1;
- float3 worldPos : TEXCOORD2;
- };
-
- v2f vert(appdata_base v)
- {
- v2f o;
- o.pos = UnityObjectToClipPos(v.vertex);
- o.worldPos = mul((float3x3)unity_ObjectToWorld, v.vertex);
- //通过TRANSFORM_TEX转化纹理坐标,主要处理了Offset和Tiling的改变,默认时等同于o.uv = v.texcoord.xy;
- o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
- o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
- return o;
- }
-
- fixed4 frag(v2f i) : SV_Target
- {
-
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
- fixed3 worldNormal = normalize(i.worldNormal);
- fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
- fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
- fixed3 halfDir = normalize(viewDir + worldLightDir);
- fixed3 specular = _Specular * pow(saturate(dot(halfDir, worldNormal)), _Gloss);
- fixed3 diffuse =_LightColor0.xyz * _Diffuse * saturate(dot(worldNormal, worldLightDir));
- fixed4 color = tex2D(_MainTex, i.uv);
- color.rgb = color.rgb * diffuse + ambient;
- return color;
- }
- #pragma vertex vert
- #pragma fragment frag
-
- ENDCG
- }
- }
- FallBack "Diffuse"
- }
-
其中关于第一个pass中的Offset深度便宜操作有以下解释:
格式为Offset factor , units,默认不设置时是Offset 0 , 0
每一个Fragment的深度值都会增加如下所示的偏移量:
offset = (m * factor) + (r * units)
m是多边形的深度的斜率(在光栅化阶段计算得出)中的最大值。这句话难以理解,你只需知道,一个多边形越是与近裁剪面(near clipping plan)平行,m就越接近0。
r是能产生在窗口坐标系的深度值中可分辨的差异的最小值,r是由具体实现OpenGL的平台指定的一个常量。
一个大于0的offset 会把模型推到离你(摄像机)更远一点的位置,相应地,一个小于0的offset 会把模型拉近。
Offset在这里的作用为防止描边pass与正常渲染pass的模型交叉,例子如下:
若描边pass开启Zwrite Off,则会解决pass模型交叉的问题
- Pass
- {
- //剔除正面,只渲染背面,防止描边pass与正常渲染pass的模型交叉
- Cull Front
- //深度偏移操作,两个参数的数值越大,深度测试时该pass渲染的片元将获得比原先更大的深度值
- //即距离相机更远,更容易被正常渲染的pass覆盖,防止描边pass与正常渲染pass的模型交叉
- Offset 0,0
- Zwrite Off
- ......
可以看到Zwrite Off解决了内部模型交叉问题,同时也带来了一个新特性,多个物体重叠时只会描外边,内部重叠的地方没有描边,适用于多选重叠的物体时高亮提醒的外部描边。
Zwrite Off同时也会带来诸多其他不稳定因数,如下例
通过分析Frame Debug,发现天空盒的绘制顺序在描线物体之后,因为描边没有写入深度而被天空盒覆盖
Unity的geometry类型的渲染顺序是从前往后的,而Transparent类型是从后往前的。天空盒的渲染顺序位于geometry之后,Transparent之前。因此我们把描边pass放在Transparent的渲染顺序就不会被geometry类型遮挡了。(此处解释摘自https://zhuanlan.zhihu.com/p/50450545,但我有疑问,天空盒不是有默认的渲染顺序background=1000吗?怎么这里会变成geometry之后,Transparent之前?是否是unity为了early-Z而做的内部优化?)
解决方案
在描边shader中更改绘制顺序为Queue=Transparent
- SubShader
- {
- Tags{"Queue"="Transparent"}
- //第一个pass,各顶点沿法线向外位移指定距离,只输出描边的纯颜色
- Pass
- {......
虽然解决了当前的问题,但关闭深度写入,调整渲染顺序并不是很稳妥的做法,随着工程继续不知道还会出现什么坑,在此不鼓励这么做,对于教程,it just work就OK啦
缺点
法线外面边对于边缘平滑,法线变化不大的模型效果比较好,对于形如立方体这类多边形较明显,法线突变状况比较多的模型下效果较差,会出现描边断裂的情况,如:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。