出场就要帅气!
1.画出模型的点
js
this.loadModel('apple.glb').then((model) => { let obj = model.children[0].children[0];let geometry = obj.geometry; //放大两倍 geometry.scale(2, 2, 2); //形状点整体居中 geometry.center(); //渲染方式为画点 let material = newTHREE.PointsMaterial({ size: 1,//点大小 color: newTHREE.Color(that.color), //颜色 transparent: true, //开启透明 }); this.mesh = newTHREE.Points(geometry, material); this.scene.add(this.mesh); //设置视角this.setView(that.cameraPos, that.controlsPos); })
- 为了操作点方便,所以这里使用的是
BufferGeometry
,以上是苹果模型的BufferGeometry,如果是一些像球体等封装好的图元,可以采用以下方式转换成对应的缓存图元
js
//转为缓存图元 geometry = newTHREE.BufferGeometry().fromGeometry(geometry);
2.画圆点
- 可以看到原始的点是正方形的,为了好看,我们需要将点变成圆形,那么我们需要在编译前修改片元着色器
js
material.onBeforeCompile = (shader) => { console.log(shader) })
- 打印
shader.fragmentShader
可以看到编译前的代码,有点多和复杂,不用读懂,我们只需要替换其中要用的一句代码,变成我们需要的结果。
js
material.onBeforeCompile = (shader) => { //修改片元着色器shader.fragmentShader = shader.fragmentShader.replace(`gl_FragColor = vec4( outgoingLight, diffuseColor.a );`, ` //需要替换的代码……` );
- 以下是画圆点的着色器,实现逻辑:计算每个片元离中心点的距离,远离中心点的片元没有过颜色
c++
//计算离中心点距离float d=distance(gl_PointCoord, vec2(0.5, 0.5)); //离中心点0.5以外没有颜色 if(d>0.5) discard; //复用替换前的代码 gl_FragColor = vec4(outgoingLight , diffuseColor.a );
3.圆点变光点
- 一个个圆点貌似有点丑,巴啦啦小魔仙仙,呜呼啦呼,变!
c++
float d=distance(gl_PointCoord, vec2(0.5, 0.5));if(d < 0.3){//保持原色 gl_FragColor = diffuseColor;}else{//透明渐变 gl_FragColor.rgb = diffuseColor.rgb;float cd =(1.0-d*2.0);gl_FragColor.a=diffuseColor.a*cd*0.5;}
- 离中心点0.3以内的保持原来颜色
- 其他部分根据离中心点距离透明渐变
- 但是你会发现当点出现叠加时,有个黑框正方形,那是因为深度冲突的问题,这时候就需要关闭材质的深度测试
js
let material = newTHREE.PointsMaterial({ size: 1, color: newTHREE.Color(that.color), transparent: true, depthTest: false//关闭深度测试 });
- 可以看到黑框不见了,发光点正常了!
4.出场方式1:让点从底部逐渐上升
获取顶点位置,然后复制存一份作为原始值,另一份设置成全部点落在底
geometry.boundingBox
:形状的包围框,可以获取底部的位置和顶部的位置
js
const positions = geometry.attributes.position; const pos = positions.clone(); //底部位置 const bottom = geometry.boundingBox.min.y; this.distance = bottom; //顶部位置 this.max = geometry.boundingBox.max.y; const count = pos.count; for (let i = 0; i < count; i++) { //将y轴坐标全部置底 pos.setXYZ(i, pos.getX(i), bottom, pos.getZ(i)); } pos.needsUpdate = true; geometry.setAttribute('position', pos); geometry.setAttribute('initialPosition', positions.clone()); geometry.attributes.position.setUsage(THREE.DynamicDrawUsage);
- 可以看到3D形状像是变成扁平二维了
动起来,让世界变得精彩
this.speed
:运动速度this.speed1
:运动加速度this.distance
:上升的距离
动画逻辑:
- 当初始点小于上升距离时,则点回到原来的位置
- 当点大于上升距离时,点维持上升距离的位置
- 上升距离随着时间增大,这样就可以呈现沿着一个平面,点逐步复原的出场效果!
- 当上升距离大于最高点则结束动画,出场完成!
js
animateAction() { if (this.mesh && this.distance <= this.max) { this.speed += this.speed1; this.distance += this.speed; let dist = this.distance; const positions = this.mesh.geometry.attributes.position; const initialPositions = this.mesh.geometry.attributes.initialPosition; const count = positions.count; let t = this.max - this.distance; for (let i = 0; i < count; i++) { const iy = initialPositions.getY(i); positions.setXYZ(i, positions.getX(i), iy <= dist ? iy : dist, positions.getZ(i)); } //通知材质的着色器,点要更新 positions.needsUpdate = true; } }
注意:赋值改变点位置后,一定要
positions.needsUpdate = true;
通知点位置属性要更新
5.出场方式2:让凌乱的点汇聚
- 逻辑跟第一种出场方式类似,不过这里需要存储一份随机偏移值,用来生成凌乱的点
js
const positions = geometry.attributes.position; geometry.setAttribute('initialPosition', positions.clone()); const pos = positions.clone(); const count = pos.count; const displacement = newFloat32Array(count); for (let i = 0; i < count; i++) { //随机偏移值 displacement[i] = that.minDistance + that.distance * Math.random(); } pos.needsUpdate = true; geometry.setAttribute('position', pos);//偏移值赋值 geometry.setAttribute('displacement', newTHREE.BufferAttribute(displacement, 1)); geometry.attributes.position.setUsage(THREE.DynamicDrawUsage);
动画逻辑:
- 初始点根据时间变换,逐渐减少偏移,最终回调原始的点,形成3D形状
- 当偏移距离减少至零,全部点恢复位置,动画结束,出场完成!
this.time
时间增长值
注意:偏移距离要乘以法向量,这样才能让点四面八方地分布
js
animateAction() { if (this.mesh && this.time >= 0) { this.speed += this.speed1; this.time += this.speed; const positions = this.mesh.geometry.attributes.position; const normal = this.mesh.geometry.attributes.normal; const initialPositions = this.mesh.geometry.attributes.initialPosition; const displacement = this.mesh.geometry.attributes.displacement; const count = positions.count; let t = 2.0 - this.time; for (let i = 0; i < count; i++) { //计算该时间的偏移距离 const d = displacement.getX(i) * t; const ix = initialPositions.getX(i); const iy = initialPositions.getY(i); const iz = initialPositions.getZ(i); const nx = normal.getX(i); const ny = normal.getY(i); const nz = normal.getZ(i); //初始点减去偏移距离 positions.setXYZ(i, ix - nx * d, iy - ny * d, iz - nz * d); } positions.needsUpdate = true; if (this.time >= 2) {//结束动画 this.time = -1; } } }
6.出场方式3:中心爆炸点
- 这个与上面的出场方式类似,不过是全部点从一个点出发,然后回到原来的位置,于是就要设置初始点为同一个位置
js
const positions = geometry.attributes.position;const b = geometry.boundingBox; this.max = Math.max( Math.abs(b.min.x), Math.abs(b.min.y), Math.abs(b.min.z), Math.abs(b.max.x), Math.abs(b.max.y), Math.abs(b.max.z) ); geometry.setAttribute('initialPosition', positions.clone()); const pos = positions.clone(); const count = pos.count; for (let i = 0; i < count; i++) { //全部点设置为原点 pos.setXYZ(i, 0, 0, 0); } pos.needsUpdate = true;是是 geometry.setAttribute('position', pos); geometry.attributes.position.setUsage(THREE.DynamicDrawUsage);
动画逻辑:
- 半径随着时间增大,当点原始位置与原点的距离小于半径则回归原位,否则跟着扩张半径运动,直至全部点回到原始位置,形成最终的3D形状!
- 半径扩展与时间是比例相乘关系,当时间为1时,则恢复原状,动画结束,出场完成!
注意:每个点与原点的距离有正负值之分,对比时要用绝对值!
js
animateAction() { if (this.mesh && this.time >= 0) { this.speed += this.speed1; this.time += this.speed; const positions = this.mesh.geometry.attributes.position; const normal = this.mesh.geometry.attributes.normal; const initialPositions = this.mesh.geometry.attributes.initialPosition; const count = positions.count; const radius = this.time * this.max; for (let i = 0; i < count; i++) { const nx = normal.getX(i); const ny = normal.getY(i); const nz = normal.getZ(i); const ix = initialPositions.getX(i); const iy = initialPositions.getY(i); const iz = initialPositions.getZ(i); positions.setXYZ( i, radius >= Math.abs(ix) ? ix : radius * nx, radius >= Math.abs(iy) ? iy : radius * ny, radius >= Math.abs(iz) ? iz : radius * nz ); } positions.needsUpdate = true; if (this.time >= 1) { this.time = -1; } } }
7.给光点开启布灵布灵的效果
- 传入每个点的颜色值,这里采用的是随机颜色,范围是[0,1]
js
let colors = []; for (let i = 0; i < positions.count; i++) { colors.push(Math.random(), Math.random(), Math.random()); } geometry.setAttribute('color', newTHREE.Float32BufferAttribute(colors, 3));
- 开启材质的顶点颜色
js
let material = newTHREE.PointsMaterial({ size: 1, color: newTHREE.Color(that.color), vertexColors: true,//顶点颜色 transparent: true, depthTest: false });
- 完成出场后,动画帧中添加给每个点不停赋值随机色,就能形成一闪一闪的效果。
js
const colors = this.mesh.geometry.attributes.color; const count = colors.count; for (let i = 0; i < count; i++) { colors.setXYZ(i, Math.random(), Math.random(), Math.random()); } colors.needsUpdate = true;
注意:点颜色值改变要通知颜色属性要更新
为什么开启点颜色后可以有不同颜色深度的效果呢?
- 我们可以改一下编译前的片元着色器,去掉一个分号,让它报错,打印一下编译后的着色器结果
js
material.onBeforeCompile = (shader) => { //修改片元着色器,使其变成发光圆点 shader.fragmentShader = shader.fragmentShader.replace( `gl_FragColor = vec4( outgoingLight, diffuseColor.a );`, `gl_FragColor = vec4( outgoingLight, diffuseColor.a )` ); };
- 代码真的好多,看得好头大!是时候展现你的着色器常识了!这个点颜色值肯定是从顶点着色器那边穿过来的,搜一下
varying
全局变量,果不其然,可以发现一下代码!
c++
173: #ifdef USE_COLOR174: varying vec3 vColor;175: #endif229: #ifdef USE_COLOR230: diffuseColor.rgb *= vColor;231: #endif
- 破案!
diffuseColor
是点显示的颜色,默认的时候是材质的color属性值,如果开启vertexColors
后,vColor
传过来,会执行颜色值相乘,即颜色值叠加,就会出现这样不同的深度的颜色。
总结
以上点的运动都是通过计算传入最终位置结果,但其实可以通过修改顶点着色器也能实现同样的效果。
three.js真的封装很全,大家可以弄点报错,看看人家的着色器代码,学习一下,也方便以后修改着色器代码,自定义效果!
GitHub地址
https://github.com/xiaolidan00/my-three
参考:https://threejs.org/examples/?q=point#webgl_points_dynamic