最简光追粒子效果

2,009 阅读6分钟

本文会介绍一个简单的粒子效果,其原理类似RayMarch,属于广义上的光追path tracing

光追绘图的基本思路

我们来简单回顾一下。 人能看见物体,是因为有光线进入人眼了,我们看见屏幕上的图案,是屏幕上每个像素都有光线进入人眼了。

那么,根据光路的可逆性,从人眼发出光线,反向即可抵达对应的像素点。

假设虚拟世界是存在的,那么就是虚拟世界里的物体发出或者反射的光线,透过屏幕上的某个像素点,最后进入到人的眼睛。

把眼睛换成相机,虚拟世界是我们用代码创造出来的。 那么就是从相机发出的光线,透过一个像素点后,会打在某个物体的表面。 如果,要回溯光源的话,那就是再用法线和反射定律求出反射光线,所有的反射光线最终会回到光源。

image.png

这种光追就是天然的透视投影,所以透视投影矩阵就可以省了。

如果不做任何优化,理论上,有多少个像素点,就会从相机发出多少根射线, 每根射线又需要和全部的物体的三角形,判断是否相交。所以光追才那么费硬件。

本文用了非常简单的一个方法,不涉及反射,所以也无需法线,性能也比较差,因为没有用sdf。

下面是片元着色器代码的基本结构。先处理好坐标的转换,然后用shere函数求距离,如果距离小于某个值就给它着色 。


const float PI = 3.14159265359;
const  vec3 eye = vec3 ( 0,0,-2.);

float t  ;
float random (float x){ 
    return sin( fract(2342.78943 * x + 67894.2342))* .5 + .5 ;
}
void main(){ 
    vec2 st = v_Uv *2. -1. ;
    st.x*= u_CanvasSize.x/u_CanvasSize.y ;
     t = u_Time * .001 ;
    vec3 color = vec3(0 );
    vec3 raydir = normalize(vec3( st, -1.) - eye ); // 视线 ,从相机到
     float d = sphere(raydir, vec3(.3)) ; // shere 是求距离的 具体实现看下文
    oColor.a =1. ;
    if( d<= .1){ oColor.xyz  = vec3(.3,.4,.5);}
    else { oColor =vec4(color,1.);}
}

一个粒子

粒子的形状是球,因为这种画法,球形是最容易的。

现在在空间中某个点o(x,y,z)处,有个一个半径为 r的球体。

当前像素点st 对应的射线就是 vec3 raydir = normalize(vec3( st, -1.) - eye ); , 有人可能注意到这里莫名就多了一个z坐标 -1, 它的含义是,把栅格(屏幕)放在z=-1的位置, 而相机在eye(0,0,-2)这个位置,所以z值大于-1的物体全部不可见;

sphere函数求的是,球心到射线的距离。 为什么要求这个距离,因为这个线是视线,如果这个距离小于球的半径,那就说明当前视线ray会穿过这个球,也就是我们能看见这个球。 这里有一个问题,暂且按下不表,不影响后续实现。

sphere的实现如下。

 float sphere(vec3 ray , vec3 o  ){ 
        vec3 co  = o - eye ; // 相机到球心的射线
        vec3 cd = ray * dot(ray, co) ; // co 在视线方向上投影的向量 因为ray是单位向量
        vec3 f  = cd + eye ; //  等于是 过o 点作 视线的垂线 垂足为 f  
        float d = length (f -o); //所以这就是 o 到视线的距离
       return d; 
 }

image.png

一个球就出来了,但是因为没有法线和光照,这里看上去就是个圆,还不是很圆。不过,不要紧,我们要做的是粒子效果,这个球体粗糙一些也无妨。

image.png

多个粒子的球形分布

一个粒子的效果做出来了,接下来只要如法炮制,改变球心的坐标就可以做出多个粒子。 这里我想把粒子分布在一个球体表面, 那么就用球坐标来生成三维坐标。

遍历全部球心,找到距离当前视线最近的那个球心,最后判断这个最近的球心的距离是不是小于半径。 效果如下。

vec4 res = vec4(9.);
    float N = 20.;
    for(float i = 0.; i < N; i++) { 
        // 下面会用球坐标计算 出随机的点坐标 所以会随机极角和方位角 ,但是又会和i关联,我想试试不要随机呢
        float ratio = i / N;
        float phi = ratio * PI; // 这样的话极角的取值范围 [0, pi]
        float r2 = sin(phi);
        float M = floor((PI * .5 - abs(phi - PI * .5)) * 5.);
        for(float j = 0.; j < M; j++) {
            float theta = 2. * j / M * PI;

            vec3 p = vec3(r2 * sin(theta), cos(phi), r2 * cos(theta));

            float d = length(raydir * dot(raydir, p - eye) + eye - p); // 前面的sphere函数,就这一行。
        if(d < res.w) {
                res.w = d;
                res.xyz = p;

            }
        }
    }

image.png

让粒子动起来

先来个竖直方向的运动,只要让球坐标的极角动起来即可。

 float phi = fract(t / 16. + ratio) * PI;

粒子1-690x640.gif 看起来效果一般,可能是视角的问题,以及逻辑的问题,上面球坐标分布,我是按极角越接近90度,分布粒子越多来做的。

现在不做这个方位角的循环了, 直接随机,每个极角只有一个粒子了,所以极角那里的分段数可以适当加大。 经过实验, 169这个数字会比较好。

还有这里的效果上,粒子运动都是连续的,这是因为这里的随机函数是个伪随机,对于确定值其返回值也确定。

float N = 169.
  for(float i = 0.; i < N; i++) { 
        // 下面会用球坐标计算 出随机的点坐标 所以会随机极角和方位角 ,但是又会和i关联,我想试试不要随机呢
        float ratio = i / N;
        // float phi = ratio * PI; // 这样的话极角的取值范围 [0, pi]
        float phi = fract(t / 10. + ratio) * PI;
        float r2 = sin(phi);
         float theta = 2. * random(ratio) * PI; // 应该是random函数有点问题 2的话居然只要一半。 
        vec3 p = vec3(r2 * sin(theta), cos(phi), r2 * cos(theta));

![粒子2-611x544.gif](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c9329f715ceb4363bd92aac6e2837480~tplv-k3u1fbpfcp-watermark.image?)
            float d = length(raydir * dot(raydir, p - eye) + eye - p); // 这个距离是随机的点到视线的距离 ,显然这个距离越小,这个点就越靠近这条视线
            if(d < res.w) {

                res.w = d;
                res.xyz = p;
            }
      
    }

粒子2-611x544.gif

最后

来点色彩,最简单的方式就把坐标映射到颜色上。因为这个球形分布的球心是原点半径是1,所以全部坐标都是单位向量,正好方便。

 oColor.a = 1.;
    if(res.w <= .01) {
        oColor.xyz = res.xyz * .5 + .5;
    } else {
        oColor = vec4(color, 1.);
    }

这种画法,虽然也是光追,但是没有解决遮挡问题,这个效果里面是看可以忽略这一点的。就是,如果,当前射线穿过两个球体,到底显示哪个球体的颜色,这就得看 那个取最小值的逻辑和 遍历的顺序了。后面用sdf 改造一下,也许可以提升不少性能。

更新

sdf是优化不了了,因为sdf的开销更大,这种方式每根射线遍历一次全部的粒子, 而如果用sdf rayMarch ,那就是每根射线至少遍历一次全部的粒子 。

但是,求最小距离这里可以优化, 前面是直接求距离的, 实际上这里我们已经把向量算出来了,然后计算向量的距离,就会涉及到开平方, 我们完全可以,一开始用向量积,也就是距离的平方,得出最小值。 后面也是用这个值去比较,所以也不用开方了。

优化之后,粒子数目可提升至500以上。

本文正在参加「金石计划」