这些屏幕特效是咋实现的
随着图形卡以及图像 API 的升级换代,越来越多的特效使用可编程渲染实现,也是就是所谓的着色器。不同实时渲染接口的着色器语法不尽相同,在英伟达的统一下,CG 语言成为行业标准(下列片元着色器代码片段中均使用 CG 语法),着色器代码在实时渲染的地位越来越高,许多经典的屏幕特效也源于着色器
那些常用特效实现方法
1. 数字图像处理来实现简单特效
-
负片效果
这个效果是我接触数字图像处理的第一个例子,那时候很流行冈萨雷斯的那本书,基本方法就是把图像的 rgb 通道反色,在口碑不好的拳皇 2001 中几乎所有 MAX 超必杀都有背景负片效果
拳皇 2001 拉尔夫超级机炮拳color.rgb = 1 - col.rgb;
-
模糊与锐化效果
模糊效果是美术最常用的的效果,在游戏开发中,根据像素深度来重建场景深度,远景使用模糊效果,近景清晰来模拟人眼专注效果;也可以模拟失去或恢复意识的相机效果。在使命召唤 8 现代战争 3 游戏中,有类似的模糊效果
使命召唤 8 现代战争 3 单人剧情开始// 高斯模糊 /* 1 2 1 2 4 2 1 2 1 */ fixed3 col = tex2D(_MainTex,i.uv + _MainTex_TexelSize.xy*r*fixed2(-1,-1)).rgb * 1; col += tex2D(_MainTex,i.uv + _MainTex_TexelSize.xy*r*fixed2(0,-1)).rgb * 2; col += tex2D(_MainTex,i.uv + _MainTex_TexelSize.xy*r*fixed2(1,-1)).rgb * 1; col += tex2D(_MainTex,i.uv + _MainTex_TexelSize.xy*r*fixed2(-1,0)).rgb * 2; col += tex2D(_MainTex,i.uv + _MainTex_TexelSize.xy*r*fixed2(0,0)).rgb * 4; col += tex2D(_MainTex,i.uv + _MainTex_TexelSize.xy*r*fixed2(1,0)).rgb * 2; col += tex2D(_MainTex,i.uv + _MainTex_TexelSize.xy*r*fixed2(-1,1)).rgb * 1; col += tex2D(_MainTex,i.uv + _MainTex_TexelSize.xy*r*fixed2(0,1)).rgb * 2; col += tex2D(_MainTex,i.uv + _MainTex_TexelSize.xy*r*fixed2(1,1)).rgb * 1; col = col / 16;
-
Bloom 效果
杀手 6 迈阿密赛车关卡
Bloom 效果是一种高亮度侵入,来实现昏暗光照切换到明亮光照的人眼不适应的效果。具体实现是先判断亮度较高的区域生产像素贴图,然后对该贴图经行高斯模糊,将亮度扩散到四周,最后和原图混合。下图为《杀手6》的光线效果,非常震撼 -
其他形态学检测
边缘检测在图像处理中比较重要,但是在游戏开发中,边缘检测一般不在屏幕特效中完成,往往在单个模型的着色器中完成描边效果,所以这里不介绍
2. 噪声算法辅助完成复杂特效
柏林噪声的发明者 Ken Perlin 因此算法获得奥斯卡科技成果奖,该噪声符合自然规律,可以模拟很多自然现象,比如云雾、消融、火焰等效果。基本思想是在两两随机数之间进行平滑差值,让随机数生成具有过渡性,而不是断落变化
平滑插值
插值是图形学的基础运算 lerp(a,b,c) = a * c + (1 - c)*b
说到平滑插值,最有必要聊聊贝塞尔曲线,做过动画的小伙伴就知道,动画进度如果与时间成正比,动画会很单调,我们常常看到的 UI 弹窗动画都会“俏皮”回弹一下,这样的动画玩家更会买账,根据贝塞尔曲线模拟出响应的动画速度,比如先慢后快,比如回弹,比如越来越快等等
先慢后快的贝塞尔曲线 | 回弹的贝塞尔曲线 |
float rand(float2 p)
{
return frac(sin(dot(p ,float2(12.9898,78.23673))) * 43758.5453);
}
// 二维柏林噪声
float noise(float2 x)
{
float2 i = floor(x);
float2 f = frac(x);
float a = rand(i);
float b = rand(i + float2(1.0, 0.0));
float c = rand(i + float2(0.0, 1.0));
float d = rand(i + float2(1.0, 1.0));
float2 u = f * f * f * (f * (f * 6 - 15) + 10);
float x1 = lerp(a,b,u.x);
float x2 = lerp(c,d,u.x);
return lerp(x1,x2,u.y);
}
// 多噪声叠加,代码可以优化成不用循环的
float fbm(float2 x)
{
float scale = 0.2;
float res = 0;
float w = 5;
for(int i=0;i<4;++i)
{
res += noise(x * w);
w *= 1.5;
}
return res*scale;
}
3. 修改 uv 的小技巧
顶点着色器传递给片元着色器的纹理坐标 uv, 是片元着色器的最基础参数,我们适当加以扰动可以做出很多绚丽的效果。比如水波纹效果,用的是正/余函数的扰动来模拟水波效果
sampler2D _MainTex;
float _Amount;
float _W;
float _Speed;
fixed4 frag (v2f i) : SV_Target
{
float2 center_uv = {0.8,0.08};
float2 uv = i.uv;
float2 dt = center_uv - uv;
float len = sqrt(dot(dt, dt));
float amount = min(_Amount,_Amount / (0.0001 + len*len*_Speed));
if(amount < 0.005) // 使用 step 优化
{
amount = 0;
}
uv.y += amount * cos(len * _W *UNITY_PI + UNITY_PI/2);
fixed4 col = tex2D(_MainTex, uv);
return col;
}