最终实现效果如下图所示,星空颜色随时间逐渐变换,星星从远处渐入,在近处渐出。

如何做出图中的星空效果?

我们需要在屏幕上生成若干个随机的点,并将这些点之间相连。但是又不能太过随机,因为如果有一条辉光从左下角的星与右上角的星相连,那么其观感会大打折扣;其次,每个点又不能与过多的其他点连接;然后,每颗星周围是高亮的,每条线是半透明的,多条线交叠的地方会更亮;最后,在远处的星是渐入进来,到了近处又渐出出去,如此循环。

为了能够与鼠标位置产生交互,需要另外写一个C#脚本用来为着色器文件提供鼠标位置。

着色器功能解析:

顶点着色器:从输入数据中获取顶点的物体空间坐标,并将其转换成裁剪空间坐标。然后将这个坐标位置再转到屏幕空间,用于后续计算。注意,这里的 fragCoord 是 float4 类型,其中fragCoord.xy是屏幕坐标(范围在 [0, w] 之间),fragCoord.z是该点原始裁剪空间的深度值,fragCoord.w是原始裁剪空间的 w 分量(透视除法所需)。

片元着色器:完成星图的绘制。

首先获取屏幕的分辨率并将其存储在 iResolution 变量中,其中_ScreenParams 是shaderlab中的内置变量,用于获取窗口的分辨率。

然后计算当前像素在窗口的位置,并将这个位置坐标归一化,使其y值在[-0.5, 0.5]。screenUV 是当前像素的窗口坐标范围在[0, 1],其中左下角为原点。假设分辨率为1920*1080,那么最终uv.x∈[-0.89, 0.89], uv.y∈[-0.5, 0.5]。

获取鼠标位置是通过C#脚本实时地将鼠标坐标传递给_MousePos,其范围在屏幕分辨率内。然后将其转换到以屏幕中间为坐标原点,将其横纵坐标等比例缩小使其mousePos.y∈[-0.5, 0.5],并存储值到 mousePos 中。

从内置变量中获取程序运行的时间,然后放缩到合适的大小以控制着色器动画的节奏。

然后使用时间变量来计算旋转矩阵,并将其应用在当前像素坐标和当前鼠标坐标,得到旋转后的像素和鼠标位置坐标。

接下来计算当前像素的颜色,其中颜色强度在 0 - 1 之间。颜色强度初始为0,然后进行循环计算。一共有 _Layers 层,第0层离摄像机最远,深度在[0, 1],每一层的深度随时间不断变化。近大远小,远处物体数量是近处的15倍。然后是淡入淡出,近处和远处的物体要淡入淡出,所以颜色强度会低,即深度值靠近0和1的颜色强度最低。netlayer用于计算所有发光的线段、点对当前像素颜色强度的影响值,即一条发光的线段或点越靠近当前像素坐标,那么发光强度越高,但是最高不超过1。将计算的最终发光强度赋值给 m 即可。

然后计算基础颜色,使基础颜色可以随时间不断变化,同时通过加权平均使颜色亮度又不至于太低。最终颜色 = 基础颜色 * 颜色强度。

随后是全局像素颜色的配置:
color *= 1.0 - dot(uv, uv); // 径向模糊效果,屏幕中间的像素亮、四周的像素暗
time = fmod(_Time.y, 230.0); // 对时间取模实现颜色亮度呼吸的循环效果
color *= smoothstep(0.0, 20.0, time) * smoothstep(224.0, 200.0, time); // 颜色呼吸效果,前20秒和后30秒是一个整体颜色渐变,实现每230s一次亮度的呼吸

函数功能解析:

n21:噪声函数,输入一个二维坐标,输出一个基于该坐标的随机数,范围在0到1之间

getPos:生成动态粒子/物体位置

标准利萨如曲线公式:x = sin(ω₁ * t + φ₁) ,y = cos(ω₂ * t + φ₂)
函数中:pos = offset + float2(sin(n1*t + n*n1), cos(n2*t + n*n2))*0.4; // 0.4用于控制运动幅度

df_line:线段外一点距离线段的最短距离,若在延长线上则返回p与端点之间的距离

line_uv:线段光绘函数,用于绘制带发光效果的线条

netLayer:生成动态网格/网络层,用于创建粒子连接效果

片段着色器调用语句:m += fade * netLayer(st*size - mousePos*z, layer, _Time.y);

其中参数列表中:
float2 st:st*size - mousePos*z,st是当前像素旋转之后的坐标经过放缩后的位置,size = lerp(15.0, 1.0, z),mousePos是经过旋转之后的鼠标坐标,z是当前层与摄像机之间的深度。简述就是,当前像素位置在放缩后,经过鼠标位置影响偏移后的位置。
float n:layer,层数。
float t:_Time.y,程序运行开始之后的时间。

float2 id = floor(st) + n; 计算当前层的 id,每层一个。
st = frac(st) - 0.5; 取放缩后坐标位置的小数部分范围[0, 1),减0.5后范围在[-0.5, 0.5)。
这两步的目的是将当前像素的位置映射到一个新的空间,并将当前像素位置放到空间的中心。

随后生成该空间中的3x3的邻域点阵,并根据id和时间计算出该层该格子邻域中9个点在当前时间的位置坐标。每个格子中包含一个点,但是该点不严格在该格子中,也就是说有可能会随时间运动出去。
1、每个格子的id不一致;
2、每个格子中包含若干像素;
3、每层的格子数不同;
4、不同层中格子包含的像素不同,离得越近(z越大)包含像素越多。
因为在越靠近摄像机的层,z值越接近1,所以size值越小,坐标的放缩越小,然后由于netLayer中映射id,就有更多的像素被映射到相同的id,每个id又代表一个格子,所以离得越近包含像素越多。

m 表示当前像素的颜色强度,sparkle

 m += line_uv(pos[4], pos[i], st); // 从中心向四周连起来的线段对当前像素的颜色影响值累加

然后循环后面的部分是用于计算9个粒子闪烁光芒对当前像素的影响:
float d = length(st - pos[i]); // 当前像素与粒子之间的距离
float s = (0.005/(d*d)); // 光芒对当前像素颜色的影响与距离的平方成反比
s *= smoothstep(1.0, 0.7, d); // 如果与粒子之间的距离超过1,则不影响,反之若距离小于0.7,则完全影响
float pulse = sin((frac(pos[i].x) + frac(pos[i].y) + t)*5.0)*0.4 + 0.6; // 为粒子光芒添加脉冲效果,脉冲随时间呼吸
pulse = pow(pulse, 20.0); // 使闪烁更集中在亮点
s *= pulse; // 将粒子光芒乘脉冲系数,实现脉冲呼吸效果
sparkle += s; // 将粒子光芒对当前像素的影响单独记录

然后计算另外四根线的影响并加到最终影响上。

完整代码:

通过网盘分享的文件:星空宇宙 链接: https://pan.baidu.com/s/1smoEd0xmRn9G7JOBAjByDw?pwd=9527 提取码: 9527

源代码地址:https://www.shadertoy.com/view/lscczl

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐