主要技术采用着色器的切割渲染,和之前写的风车可视化的文章不同,这次的切割效果是在着色器的基础上实现的,并新增了很多可调节的变量,兄弟们,走曲儿~
线上演示地址,点击体验
正文
从图中大概可以看出以下信息,一个由线组成的无人机模型,一个由粒子组成的无人机模型,那么这些粒子、线都是从一个无人机的gltf模型中提取出来的,无人机模型由很多个小的Mesh
组成的一个大的group,那么为了着色器的效果好处理,我们将所有的顶点信息收集起来,文中不讲加载gltf模型的内容哦~不了解的童鞋可以参考博主之前的文章。
收集点位信息
收集点位信息主要应用到threejs提供的实用工具BufferGeometryUtils中的mergeGeometries
API,将所有的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为啥需要用边缘几何体处理呢?那我们在这里做一个对比,左图是未经过处理的,右图是经过处理的,从左面的图可以看出来,原模型组成面的三角线也都被渲染出来了,所以导致线条特别凌乱。而且渲染的内容越多,对浏览器的压力也越大。
/**
* 获取线条和点
* @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;
添加线条后的效果图
着色器材质
材质采用着色器材质,通过修改顶点信息做出相应的效果,所以主要修改的内容都在vertexShader顶点着色器
中,可配参数为time 运行时间
,angle 切割角度
,clipDistanceScale切割距离
,reversal 反向
,通过时间计算当前切割位置float clipDistance = z - sin( time ) * clipDistanceScale;
,将模型裁切,通过修改gl_ClipDistance
切割距离,判断切割位置。将计算出来的clipDistance
结合传入的reversal
判断不同的切割方向,区分出粒子模型和线段模型。uniforms
中储存的是外部的变量,可以通过material.uniforms
进行修改,可以在顶点着色器中获取并使用。接下来便是通过裁切位置判断颜色变化了。如果裁切位置小于传入的着色器变量gradientDistance
将顶点颜色修改为变量gradientColor1
与gradientColor2
之间的渐变色
...
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 为抗锯齿渲染方式,用于处理透明化的模型,这个处理方式不进行裁切
通过改变不同的场景处理方式,切换渲染模式
着色器材质的修改
前面的修改都是修改外部数据影响模型的渲染,接下来是通过修改着色器内部变量实现着色器的效果变化
我们拿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中修改着色器变量也是相同的操作。
如有不理解或者作者说错了地方,欢迎私信。兄弟们,撤~