0x00需求
特效同学需要一个能随着距离摄像机距离变化,而颜色逐渐变淡的需求。
0x01分析需求
把美术同学的需求转化成程序需求便是:透明度随着距离越来越小。那么问题的关键就变成了如何计算距离,而计算到摄像机的距离,首先想到的是通过计算两个点之间的距离来计算。
// 方法1.摄像机的世界坐标 - 转换到世界空间的顶点坐标
o.distance = length(_WorldSpaceCameraPos - mul(unity_ObjectToWorld, v.vertex).xyz);
我在本地的测试,使用一个Capsule,改变它与Camera的距离来测试,一切正常。
可以看到随着Capsule到摄像机的距离越来越近,透明度逐渐变小,越来越透明。
然而交付给美术同学后,美术同学反馈,这个材质应用在粒子特效上,有的粒子看起来正常,有的看起来透明度没有明显变化。
0x02分析原因
这个需求看似很简单,问题的关键就在于计算顶点的距离,我第一反应是计算距离的方法有问题,于是使用矩阵计算,替换了上面与Camera坐标求解的方法:
// 方法2.从模型空间到观察空间
// Camera的参数(Position, Target, Up)决定了view matrix。模型在World Space里的位置,经过view matrix的变换,就变成了在View Space里的位置。
o.distance = length(mul(UNITY_MATRIX_MV, v.vertex).xyz);
经过测试,表现依旧。在我的测试环境下一切正常,而在美术同学使用Particle System时就会出现,部分粒子正常,部分粒子不正常的现象。
事出反常必有妖!
那么问题来了,
妖在哪里???
看代码实在看不出问题,我就打开了一个粒子发射器,使之对准摄像机,我从摄像机的位置出发,盯着满屏幕的粒子发呆。
A few minutes later!
看出来了吗?
没有?
没有继续看。
A few days later!
看出来了吗?
没有?
没有继续看。
A thousand years later!
一千多年以后,我忽然觉得屏幕边缘的粒子确实有渐隐的效果,而屏幕中间的粒子却没有。
这是为什么?难道中间的粒子有什么奇怪的地方?
确实很奇怪!
如果把摄像机的位置当成一个点C,那么中间的粒子P1到点C的距离,与边缘的粒子P2到点C的距离是不同的!此时若用两点间的距离来映射到透明度,那么就会出现中间的粒子透明了,而边缘的粒子却没有变透明。
0x03解决方案
那么如何让不同点到摄像机的距离不一样,却能通过某种函数映射到同一个值呢?
那只能凭借直觉了 = =
没错,是直觉。因为问题不是出现在数学计算上,而是出现在需求的分析上。
其实本文一开始的需求分析就有问题,只是直接将美术同学表述的需求转化成了程序需求,而希望粒子在某一位置透明度变淡,这一位置,其实是一个平面。有感觉的同学肯定已经想到了,我们需要计算的距离是点到面的距离,而不是点到点的距离,那么是哪个面呢?从摄像机看出去,第一面必然是近裁剪面了。问题得证!
0x04效果图&源代码
最后是代码:
Shader "game/particle/Alpha_FadeByDistance"
{
Properties
{
_MainTex ("主帖图", 2D) = "white" {}
[Gamma][HDR]_MainColor ("主颜色", Color) = (1,1,1,1)
_Threshold ("渐隐阈值", Float) = 5
}
CGINCLUDE
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 vertexColor : COLOR;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float distance : TEXCOORD1;
float4 vertexColor : COLOR;
};
uniform sampler2D _MainTex;
uniform float4 _MainTex_ST;
uniform float4 _MainColor;
uniform float _Threshold;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.vertexColor = v.vertexColor;
// 方法1.摄像机的世界坐标 - 转换到世界空间的顶点坐标
// o.distance = length(_WorldSpaceCameraPos - mul(unity_ObjectToWorld, v.vertex).xyz);
// 方法2.从模型空间到观察空间
// Camera的参数(Position, Target, Up)决定了view matrix。模型在World Space里的位置,经过view matrix的变换,就变成了在View Space里的位置。
// o.distance = length(mul(UNITY_MATRIX_MV, v.vertex).xyz);
// 方法3.将模型空间下的顶点坐标转换到观察空间,然后计算顶点z值与近裁剪面的距离
o.distance = -UnityObjectToViewPos(v.vertex).z - _ProjectionParams.y;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
// 将距离映射为alpha[0, 1],为了在粒子系统中控制颜色的变化,需要*vertexColor
col.a *= clamp(i.distance / _Threshold, 0.0, 1.0) * _MainColor.a * i.vertexColor.a;
col.rgb *= _MainColor.rgb * i.vertexColor.rgb;
return col;
}
ENDCG
SubShader
{
Tags { "RenderType"="Transparent" "Queue" = "Transparent+200" }
Blend SrcAlpha OneMinusSrcAlpha
Cull Off
ZWrite Off // 需要关闭深度写入,否则会有纹理的半透明切边
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
}