按照惯例,先上图:
效果分析
- 整体上效果在模型外包围,且永远在模型后面
- 边缘噪波
- 两层描边
思路分析
- 效果上描边与模型渲染相对分离,且要控制附魔效果的出现与消失,初步思路使用双Pass对卡通模型和附魔效果分别渲染。之前的卡通渲染shader可以直接使用UsePass进行复用
由于之前使用的ToonLit shader是表面着色器,如下图,我保留了前向渲染(forward)与延迟渲染(deferred)路径,摒弃了遗留的延迟渲染(prepass)路径,所以在复用时需要指明是哪个pass,此处是前向渲染,所以使用forward,需要注意的是,在复用pass时需要大写
- 针对这里的描边效果,需要对附魔范围进行精确的调整,且已经分为单独的pass控制,很容易想到使用模型顶点延法线外扩的方案。但要注意的是,我们这次不能在模型空间来做这项操作(可能我经常这样做)。分析一下这个效果,就会发现它要求我们始终看到附魔效果在模型之后,且不随摄像机位置发生变化(这一点很重要),简单地说这是一个3D物体的2D附魔效果,也可以理解为屏幕空间特效(虽然不是在屏幕空间操作)。由于是模型顶点偏移,我们在齐次裁剪空间执行此次外扩操作(不在屏幕空间对片元进行操作也是为了优化,以及不影响其他后处理效果的使用)
此处有一点要注意的是,模型法线的空间转换不同于顶点,法线是向量,如果使用与顶点变换相同的变换矩阵,将有可能出现法线不再垂直于表面的现象(相关详细推理请见《Unity Shader入门精要》86页,乐乐永远的神~),我们使用其逆转置矩阵做为法线的变换矩阵,可以解决这个问题(使用切线与法线始终垂直进行推导)
- 附魔部分的代码如下,
首先,对于屏幕空间的UV滚动,我们可以使用屏幕空间的位置来做基础量
然后采样一张Noise图,制作边缘噪波备用
使用rim来制作一种边缘淡化的效果,_RimPower用来控制淡化边缘的宽度(一种透明度中心到边缘为1,边缘到边界为1到0的渐变模型)
rim减去noise用来实现不规则边界
edgeRim用来实现这个不规则边界的缩放,这样的话基础层就已经有了(如图深蓝和橘红部分)
extraRim的代码可能比较难以理解,这里可以一步一步来看,可以看到,相比edgeRim,extraRim先是在Rim.a的基础上加了个Edge,因为有saturate函数控制边界,其实就是实现了一个边界扩充的效果(原来为0的地方变成了Edge,边界被扩充了一小部分,Edge的范围也应当较小),减去edgeRim就实现了外边界,使用Brightness控制亮度
代码展示
Shader "MayoHa/VFX/AuraOutline"{ Properties { _Color("Color",Color) = (1,1,1,1) _MainTex ("Texture", 2D) = "white" {} _RampTex("Ramp Texture",2D) = "gray" {} [Header(Outline)] _NoiseTex("Noise Texture",2D) = "white" {} _OutlineScale("Outline Scale",float) = 0.3 _OutlineZ("Outline Z Offset",Range(-0.06,0)) = -1 _XSpeed("X Speed",float) = 1 _YSpeed("Y Speed",float) = 1 _RimPower("Rim Power",Range(-4,10)) = 1 _OffSet("Noise Opacity",Range(0.01,10)) = 1 _Scale("Scale",Range(0,0.1)) = 0.01 _Edge("Edge",Range(0,1)) = 1 _Brightness("Brightness",float) = 1 _EdgeColor("Edge Color",Color) = (1,1,1,1) _RimColor("Rim Color",Color) = (1,1,1,1) } CGINCLUDE #include "UnityCG.cginc" struct appdata{ float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f{ float4 pos : SV_POSITION; UNITY_FOG_COORDS(0) float3 normalDir :TEXCOORD1; float3 viewDir : TEXCOORD2; }; float _OutlineScale; float _OutlineZ; v2f vert(appdata v){ v2f o; o.pos = UnityObjectToClipPos(v.vertex); float3 viewNormal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV,v.normal)); float2 offset = TransformViewToProjection(viewNormal.xy); o.pos.xy += offset * _OutlineScale*o.pos.z ; o.pos.z += _OutlineZ; o.viewDir = normalize(WorldSpaceViewDir(v.vertex)); o.normalDir = normalize(UnityObjectToWorldNormal(v.normal)); UNITY_TRANSFER_FOG(o,o.pos); return o; } ENDCG SubShader { Tags { "RenderType"="Opaque" } LOD 100 UsePass "MayoHa/Toon/ToonLit/FORWARD" pass{ Name "Outline" Tags{"LightMode" = "Always"} ZWrite Off ColorMask RGB Blend SrcAlpha OneMinusSrcAlpha Cull Back CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fog float _XSpeed,_YSpeed; sampler2D _NoiseTex; float _RimPower; float _OffSet; fixed4 _EdgeColor; fixed4 _RimColor; float _Edge,_Brightness; float _Scale; fixed4 frag(v2f i):SV_TARGET{ float2 uv =float2 (i.pos.x * _Scale - _Time.x * _XSpeed,i.pos.y * _Scale - _Time.x * _YSpeed); float4 noise = tex2D(_NoiseTex,uv); //使用Rim 可以实现边缘淡化效果 float4 rim = pow(saturate(dot(i.viewDir,i.normalDir)),_RimPower); rim -= noise; float4 edgeRim = saturate(rim.a * _OffSet); //挤出外描边 float4 extraRim = (saturate((_Edge + rim.a) * _OffSet) - edgeRim) * _Brightness ; float4 result = (edgeRim * _EdgeColor)+(extraRim * _RimColor); UNITY_APPLY_FOG(i.fogCoord,result); return result; } ENDCG } }}
在使用时需要根据情况来控制附魔效果的产生和消失,由于附魔效果是使用单独一个Pass实现的,所以我们需要在脚本中控制该Pass的开启与关闭,这可以使用Unity自带的 SetShaderPassEnabled方法实现,具体使用如下
using System.Collections;using System.Collections.Generic;using UnityEngine;public class BufferFXMgr : MonoBehaviour{ private Material material; private bool isBufferd; private void Awake() { material = gameObject.GetComponent().material; isBufferd = false; } // Start is called before the first frame update private void Start() { } // Update is called once per frame private void Update() { if (Input.GetKeyDown(KeyCode.E)) { isBufferd = !isBufferd; } material.SetShaderPassEnabled("Always", isBufferd); }}
Always为附魔效果Pass的LightMode标签,SetShaderPassEnabled方法可以通过该标签来获取对应的Pass(可以多个),第二个参数用来控制该Pass的关闭与开启,但貌似只能用LightMode标签。
举个例子,如果你的shader中有LightMode为Always的Pass(用来处理折射效果),但你只想让有折射纹理的材质渲染该Pass,其余的不渲染,那么你可以使用这个函数实现分类
总结
1、边缘淡出效果极为常用,此处使用Rim配合saturate来实现是一种不错的方法
2、描边效果可以在基础效果上对Alpha进行偏移,这样的描边仍然有淡出效果
3、在适合的空间进行顶点操作往往可以减轻很大一部分工作负担,优化代码性能
4、setShaderPassEnabled可以根据LightMode Tag来控制材质shader中对应Pass的开启与关闭
往期精选
Unity3D游戏开发中100+效果的实现和源码大全 - 收藏起来肯定用得着
Shader学习应该如何切入?
喵的Unity游戏开发之路 - 从入门到精通的学习线路和全教程
声明:发布此文是出于传递更多知识以供交流学习之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与我们联系,我们将及时更正、删除,谢谢。
作者:江陵野少