粒子系统+着色器开发一款无人机概念效果图

1,612 阅读6分钟

主要技术采用着色器的切割渲染,和之前写的风车可视化的文章不同,这次的切割效果是在着色器的基础上实现的,并新增了很多可调节的变量,兄弟们,走曲儿~

线上演示地址,点击体验


1 完整效果.gif

正文

从图中大概可以看出以下信息,一个由线组成的无人机模型,一个由粒子组成的无人机模型,那么这些粒子、线都是从一个无人机的gltf模型中提取出来的,无人机模型由很多个小的Mesh组成的一个大的group,那么为了着色器的效果好处理,我们将所有的顶点信息收集起来,文中不讲加载gltf模型的内容哦~不了解的童鞋可以参考博主之前的文章。

收集点位信息

收集点位信息主要应用到threejs提供的实用工具BufferGeometryUtils中的mergeGeometriesAPI,将所有的geometry合并为同一个BufferGeometry。

...
let geometries: BufferGeometry[] = [];
gltf.scene.traverse((child: any) => {
if (child.isMesh) {
  geometries.push(child.geometry);
}
});

let combinedGeometry = BufferGeometryUtils.mergeGeometries(geometries);
...

这样我们就得到一个新的geometry: BufferGeometry,但是里面包含了所有无人机的零部件。这样就可以统一处理这些信息了

创建线条模型和点阵模型

代码中封装了一个创建线条的方法(也包含了创建点阵的方法,这里不细介绍),代码主要逻辑:拿到顶点信息,通过边缘几何体(EdgesGeometry)计算相邻面和法线角度判断当前顶点信息是否需要渲染,也是过滤顶点信息,减缓浏览器渲染压力,得到一个过滤后的geometry,再将顶点信息渲染为 线段(LineSegments),这里有同学要问哦,同样都是接受一个geometry为啥需要用边缘几何体处理呢?那我们在这里做一个对比,左图是未经过处理的,右图是经过处理的,从左面的图可以看出来,原模型组成面的三角线也都被渲染出来了,所以导致线条特别凌乱。而且渲染的内容越多,对浏览器的压力也越大。

4 处理边缘几何体.jpg

/**
 * 获取线条和点
 * @param geometry 几何体模型
 * @param thresholdAngle 相邻面的法线之间的角度
 * @returns 线条和点
 */
 
getLine:(geometry: BufferGeometry, thresholdAngle?:number)=>{ line: LineSegments, points: Points }

接受的第一个参数就是BufferGeometry类型,可以将刚才合并的geometry传进来,第二个参数用于判断计算相邻面和法线的角度,如果这个角度大于传入的值才会将geometry渲染为线段。

// 创建线条,参数为 几何体模型,相邻面的法线之间的角度,
    // 创建线条材质,使用初始线条颜色
    const lineMaterial = material(new Color(guiParams.initLineColor));
    // 创建几何体的边缘,用于过滤多余的顶点信息
    var edges = new EdgesGeometry(geometry, thresholdAngle);
    // 创建线条段,使用过滤后的顶点信息和线条材质
    var line = new LineSegments(edges, lineMaterial);
    // 设置用户数据,标记为光线
    line.userData.isLight = true;

添加线条后的效果图

5 添加线条.jpg

着色器材质

材质采用着色器材质,通过修改顶点信息做出相应的效果,所以主要修改的内容都在vertexShader顶点着色器中,可配参数为time 运行时间angle 切割角度clipDistanceScale切割距离reversal 反向,通过时间计算当前切割位置float clipDistance = z - sin( time ) * clipDistanceScale;,将模型裁切,通过修改gl_ClipDistance切割距离,判断切割位置。将计算出来的clipDistance结合传入的reversal判断不同的切割方向,区分出粒子模型和线段模型。uniforms中储存的是外部的变量,可以通过material.uniforms进行修改,可以在顶点着色器中获取并使用。接下来便是通过裁切位置判断颜色变化了。如果裁切位置小于传入的着色器变量gradientDistance将顶点颜色修改为变量gradientColor1gradientColor2之间的渐变色

...
 uniforms: {
        time: { value: 1.0 },
        angle: { value: -0.1 * Math.PI },  // 添加角度uniform
        originalColor: { value: color }, // 本色
        gradientColor1: { value: new Color(0xc9e6ff) }, // 渐变色1
        gradientColor2: { value: new Color(0xffc7d8) }, // 渐变色2
        gradientDistance: { value: 2.0 }, // 渐变判断距离
        clipDistanceScale: { value: 6 }, // 提取裁切距离缩放因子为变量
        reversal: { value: false }, // 反向裁切
    },
 vertexShader: `	
    uniform float time;
    uniform float angle;  // 添加角度uniform
    uniform vec3 originalColor; // 本色
    uniform vec3 gradientColor1; // 渐变色1
    uniform vec3 gradientColor2; // 渐变色2
    uniform float gradientDistance; // 渐变判断距离
    uniform float clipDistanceScale; // 提取裁切距离缩放因子为变量
    uniform bool reversal; // 反向裁切

    varying vec4 vColor;

    void main() {

        vColor = vec4(originalColor, 1.0); 

        #ifdef USE_CLIP_DISTANCE
            vec4 worldPosition = modelMatrix * vec4( position, 1.0 );
            // 使用角度计算裁切平面,并根据时间增加裁切距离
            float z = cos(angle) * worldPosition.z - sin(angle) * worldPosition.y;
            float clipDistance = z - sin( time ) * clipDistanceScale;
            gl_ClipDistance[ 0 ] = reversal ? clipDistance : -clipDistance; // 根据reversal变量决定是否反向裁切
            // 如果距离切割位置小于gradientDistance,则将颜色渐变为红色
            if (abs(gl_ClipDistance[ 0 ]) < gradientDistance) {
                float ratio = 1.0 - (abs(gl_ClipDistance[ 0 ]) / gradientDistance);
                // 使用两个渐变色进行渐变
                vec3 gradientColor = mix(gradientColor1, gradientColor2, ratio);
                vColor = vec4(gradientColor, 0.8); // 渐变色
            }
        #endif
        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    }`,
...

切割部分关键代码

在上述代码中有一个判断语句#ifdef USE_CLIP_DISTANCE,如果是裁切模式,则进行下面的交互,那么这个参数又是从哪里来的呢?可以查看官网着色器材质中的属性

extensions : Object

一个有如下属性的对象:

this.extensions = { 
   clipCullDistance: false, // 设置为使用顶点着色器剪裁
   multiDraw: false
};

lineMaterial.extensions.clipCullDistance = true;,通过修改着色器参数判断当前是否裁切

render的修改

那么除了在着色器上修改裁切相关参数,还需要修改renderer中的裁切方式来配合着色器进行裁切作业。

首先通过renderer获取 WebGL 扩展:

const ext = renderer
    .getContext()
    .getExtension('WEBGL_clip_cull_distance');

这个扩展用于控制几何体的裁剪和剔除。

然后获取webGL的上下文

const gl = renderer.getContext();

这是WebGLRenderer提供的方法用于获取webgl上下文

获取到上下文以后就可以启用裁切平面了,这里我们启用第0个裁切平面

gl.enable(ext.CLIP_DISTANCE0_WEBGL);

这个地方需要注意一下,如果这里启用的是第0个裁切平面,在着色器中的判断裁切距离变量gl_ClipDistance也要取第0个,这是一个数组,两个变量需要配套

在GUI控制器中,设定一下裁切方式,用于切换当前场景的渲染方式

export const clipCull = gui.add(guiParams, 'clip_cull', ['CLIP_DISTANCE0_WEBGL', 'SAMPLE_ALPHA_TO_COVERAGE'])

CLIP_DISTANCE0_WEBGL 是我们刚才用到的裁切平面

SAMPLE_ALPHA_TO_COVERAGE 为抗锯齿渲染方式,用于处理透明化的模型,这个处理方式不进行裁切

通过改变不同的场景处理方式,切换渲染模式

6 渲染模式.gif

着色器材质的修改

前面的修改都是修改外部数据影响模型的渲染,接下来是通过修改着色器内部变量实现着色器的效果变化

我们拿time举例,通过修改time的值,让裁切位置进行改变,uniforms是着色器内置的可以修改参数的方法。

  • 内置attributes和uniforms与代码一起传递到shaders。 如果您不希望WebGLProgram向shader代码添加任何内容,则可以使用RawShaderMaterial而不是此类。
if (line) {
    (line.material as any).uniforms.time.value = clock.getElapsedTime() * speed;
    (points.material as any).uniforms.time.value = clock.getElapsedTime() * speed;
}

speed为速度变量,在gui控制器中可进行修改,在GUI中修改着色器变量也是相同的操作。

如有不理解或者作者说错了地方,欢迎私信。兄弟们,撤~

视频案例