在一些数字孪生场景特效中,常常有光环的设计。本文在 shadertoy 找了以下三个光环的实现,并解释其实现原理

imgimgimg

2D 的光很简单,一般都有以下类似的函数,当2D 平面的点离发光位置越远,光就衰减了。

img

Halo1

https://www.shadertoy.com/view/MlyGzW

图片

作者写的代码是经过计算优化的, 以下给出我转化的代码

st = st * 2.0 - 1.0;
vec2 center = vec2(0.0);
vec2 offset = vec2(cos(iTime / 2.0) * 0.5, sin(iTime/ 2.0)* 0.5);
vec3 lightColor = vec3(sin(iTime), 0.84, cos(iTime));

float radius = 0.5;
float dist1 = distance(st, center)-radius;
float dist2 = distance(st, offset)-radius;
float intensity =  0.03 / abs(dist1);
if (dist1 < 0.) {
    intensity = 0.03/  abs(dist1)/  abs(dist2);
}
retCol = lightColor * intensity;

最关键的就是 intensity的计算。同时作者在外圆内部叠加了一个带偏移的圆,同时通过偏移圆的运动,达到类似于 smooth min的效果,我猜想如果试用 smooth min 函数最好 的简化运算 应该也会变成 dist 相乘

Halo2

https://www.shadertoy.com/view/3tBGRm

图片

这个效果就更加丰富一些,首先圆开始做了一些轻微的变形,另外能看到有一个强光在边缘不断流动。代码中用到了上面作者一样的计算优化技巧,例如喜欢用 mix到 1.0 来让计算在[0, 1.0]这个值空间

77:  radius = mix(mix(innerRadius, 1.0, 0.4), mix(innerRadius, 1.0, 0.6), noise);

另外也喜欢用 normalized这种形式,计算 dist

78:  d0 = distance(uv, r0 / len * uv);

不过没搞明白为什么这么写,是不是 GPU友好,或者有其他原因。我将用比较好理解的方式去重新写这个代码。第一个效果是圆环的扭动。

float n0 = snoise3( vec3(st * noiseScale, iTime * 0.5) ) * 0.5 + 0.5;
float r = mix(innerRadius-0.05, innerRadius+0.05, n0);

我这里使用最简单的方式,通过 radom函数让 radius在一个[-0.05, 0.05]之间变化。另外这个 shader中使用两个光函数, 简单来说就是衰减的快慢程度,第二个函数适合做一个高光。因为衰减的更快

float light1(float intensity, float attenuation, float dist)
{
    return intensity / (1.0 + dist * attenuation);
}
float light2(float intensity, float attenuation, float dist)
{
    return intensity / (1.0 + dist * dist * attenuation);
}

于是的到一个基础光

float dist = abs(len - r);
float baseLight = light1(1.0, 10.0, dist);

图片而后为了屏幕不会光雾蒙蒙的, 原作者用了更加复杂的方法也是为了能够计算适配更强。我这里简单对外圈和内圈做一个 smoothstep.

// outer
float decay = smoothstep(r * 1.05, r, len);
// inner
float hole = smoothstep(innerRadius*0.8, innerRadius, len);

图片

这里颜色太简单,作者混合了两种颜色,并让两种颜色的混合点做旋转。另外一个颜色更加亮一些接近白色,这样再做高光的时候,可以看上去像是高光影响到了圆的表面

float cl = cos(angle + iTime * 2.0) * 0.5 + 0.5;
vec3 col = mix(color1, color2, cl);

图片

最后就是高光了,高光的逻辑很简单,获取圆表面的点,通过两种 light做一下混合调出比较舒服的高光效果。

float a = iTime * -1.0;
vec2 pos = vec2(cos(a), sin(a)) * r; // 边缘点
dist = distance(st, pos);
float highLight = light2(1.5, 5.0, dist);
highLight *= light1(1.0, 10.0, dist);

最后完整的渲染颜色为,混合色*(基本强度+高光强度)

retCol  = col * (baseLight + highLight)  * hole * decay;

当然如果可以按照这个方法尝试新的配色,例如这个作者就做了另外一个颜色的光环,也挺漂亮的图片

Halo3

图片

这个光环比较有特点的地方是,他的脉冲波从中心到边界对于颜色都有一种增强另外 我比较喜欢波以脉冲的形式到了边界之后有一种被吸收了的感觉。作者在写这个画面的时候使用了两种不同的计算方式,相较于直接写 smoothstep ,习惯他绘图的思维方式之后,实现效果更加轻松,有点类似于在 Figma不断做 bool运算的感觉。脉冲被吸收是因为作者取得外圈的时间函数和脉冲的时间函数设置的比较巧妙。

float circle2(float d, float radius, float vignette) {
    return smoothstep(radius * vignette, radius, d);
}

float circle3(float d, float radius) {
    return .1 - smoothstep(radius, radius + 0.01, d);
}

circle2函数用来画圆的外部空间,circle3将圆内部空间降到 0.1的值之外,还会将外部空间的值变成 -0.9. 这样很巧妙的做了一个 limit 的动作。其绘画逻辑如下

// 基础色
v += circle2(len, radius + 0.02, 0.8);
// 边框轻微收缩/扩张
v += circle2(len, radius + 0.02, 0.8);
// 做一次减法,并把内核画出来
v += circle3(len, .13) ;

通过三个函数,获取一个会呼吸的外框图片

这个时候只要再加几次limiter, 因为就可以把外边框去除

v += circle3(len, radius)*3.;

图片

接下去就是做脉冲波,脉冲波是一个非常简单的往外扩散的半径变化

float radius3 = fract(iTime / 2.4);

而画脉冲也非常简单, 搞一个外圆,再做一次 limiter。搞定

v += circle2(len, radius3, 0.5);
v += circle3(len, radius3);

转自公众号:艺术的技术