

中高端软件定制开发服务商

13245491521 13245491521
threejs——没什么意义的大屏 点击关注公众号,“技术干货”及时达! 前言本文是一次尝试性的创新,代码是作者写的,但是下面的文章内容是把代码扔给AI,让AI写的,作者整理的,感觉比作者之前的文章写的细致,但是有点太细了,不知各位看官是否习惯这样的文章,欢迎大家提意见。五万多个字呢,比以往所有的内容加起来都多 效果图局部动图 1 局部动图.gif文件目录??content │ │ ├─ ??GUI.ts-------------------控制器 │ │ ├─ ??constData.ts-------------告警信息坐标 │ │ ├─ ??grid.ts------------------网格辅助线 │ │ ├─ ??light.ts-----------------灯光 │ │ ├─ ??main.ts------------------入口文件 │ │ ├─ ??map.ts-------------------绘制线条形状 │ │ ├─ ??materials.ts-------------材质 │ │ ├─ ??request.ts---------------线条形状点位请求 │ │ ├─ ??scene.ts-----------------场景必要元素 │ │ ├─ ??tag.ts-------------------告警标签 │ │ ├─ ??tagLine.ts---------------告警标签连接线 │ │ ├─ ??tagPanel.ts--------------告警面板 │ │ └─ ??tornado.ts---------------粒子运动(龙卷风) │ ├─ ??utils ----------------------工具库 │ │ ├─ ??IntervalTime.ts----------定时器class类 │ │ ├─ ??index.ts-----------------常用工具 │ │ └─ ??unreal.ts----------------场景发光 技术栈"three": "0.167.0","typescript": "^5.0.2","vite": "^4.3.2"正文辅助线创建坐标格辅助线,当你的场景背景太单调时,可以用做于背景图,接受四个参数,很简单,官网有详细案例。grid.userData.isLight = true用于标记是否接受发光场景的影响,如不需要发光,可不进行标记,后续需要发光的元素都会进行标记,不在后文赘述。 const grid = new THREE.GridHelper(200, 500, 0x1c252d, 0x1c252d);grid.userData.isLight = truescene.add(grid);形状及线条https://geo.datav.aliyun.com/areas_v3/bound/330000_full.json从该网站获取到形状的顶点信息,过滤编号为330100的形状,因为这个形状要做挤压缓冲几何体,也是为了减少浏览器的渲染压力,接口返回的顶点信息为2d。x和y的坐标,所以需要处理一下,将顶点信息改为x和z,y设置为0。 绘制线条请求数据并进行绘制// 获取杭州市区地图数据fetchHZJSapData().then((data) = { const features = data.features features.forEach((feature: any) = { const arcs = feature.geometry.coordinates[0][0]; // 排除adcode为330100 if (feature.properties.adcode !== 330100) { const positions = getPositions(arcs) // 创建较暗的边界线 const line = createLine(positions, 0x323748, .8); cityGroup.add(line) } })})首先调用fetchHZJSapData()函数来获取数据,该函数返回一个Promise。当Promise成功 resolve 后,会将获取到的数据传入后续的回调函数中进行处理。 在回调函数中,从获取到的数据对象中提取出features属性值,并将其赋值给features变量,以便后续对每个特征数据进行遍历操作。 处理数据getPositions方法// 将二维坐标转换为三维坐标数组export const getPositions = (arcs: number[][]) = { let positions: number[] = []; arcs.forEach((v2Arr: number[]) = { const x = v2Arr[0]; const z = v2Arr[1]; positions.push(x, 0, z); // y坐标设为0 }); return positions;}创建Line2,并添加到组中createLine首先创建线条几何体(LineGeometry)对象,通过调用new LineGeometry()完成,该对象用于定义线条的几何形状,随后使用geometry.setPositions(positions)设置线条的顶点位置,即将传入的顶点坐标数组应用到几何体上。 接着创建线条对象,通过new Line2(geometry, lineMaterial(color, opacity, width, dashed))来实现,其中Line2是用于渲染线条的类,而lineMaterial函数用于生成线条的材质,材质的属性由传入的color、opacity、width和dashed等参数确定。 如果dashed参数为true,即线条是虚线的情况下,会调用line.computeLineDistances()来计算线段的距离,以实现虚线效果。 // 创建线条对象/** * 创建线条对象 * @param positions 顶点坐标数组 * @param color 线条颜色 * @param opacity 线条透明度 * @param width 线条宽度 * @param isLight 是否为光源 * @param dashed 是否为虚线 * @returns */export function createLine(positions: number[], color: number, opacity = 1, width = 1, isLight = false, dashed = false) { // 创建线条几何体 const geometry = new LineGeometry(); // LineGeometry用于定义线条的几何形状 geometry.setPositions(positions); // 设置线条的顶点位置,positions是一个包含坐标的数组 // 创建线条对象,使用lineMaterial函数生成材质 const line = new Line2(geometry, lineMaterial(color, opacity, width, dashed)); // Line2是用于渲染线条的类 line.userData.isLight = isLight; // 将isLight属性存储在userData中,方便后续使用 if (dashed) { // 如果线条是虚线 line.computeLineDistances(); // 计算线段的距离,用于虚线效果 } return line; // 返回创建的线条对象}线条材质通过调用new LineMaterial({...})创建一个线条材质对象,传入一个包含多个属性的配置对象。 在配置对象中,设置了以下属性: color:传入的线条颜色值。 linewidth:传入的线条宽度值。 opacity:传入的线条透明度值。 transparent:设置为true,表示线条是透明的,结合opacity属性可以实现不同程度的透明效果。 vertexColors:设置为false,表示不使用顶点颜色,可能是用于控制线条颜色的一种方式(具体效果取决于渲染引擎的实现)。 dashed:传入的布尔值,决定线条是否为虚线。 dashSize:当线条为虚线时,设置虚线的线段长度。 gapSize:当线条为虚线时,设置虚线的间隔长度。 函数返回值最后,函数返回创建好的LineMaterial对象,这个对象可以在创建线条对象时被使用,以设置线条的材质属性,从而实现特定的线条渲染效果。 export const lineMaterial = (color: number, opacity: number, width = 1,dashed=false) = { const material = new LineMaterial({ color, linewidth: width, opacity, transparent: true, vertexColors: false, dashed, dashSize: 0.05, gapSize: 0.05 }) return material};接下来所有的线条都基于此方法进行创建。 效果图4 装饰线条.jpg挤压缓冲几何体fetchHZSMapData().then(...),用同样的方法请求到需要制作挤压缓冲几何体的顶点信息,或者直接用刚才过滤出来的顶点信息制作缓冲几何体 const createShape = (positions: number[]) = { const shape = new THREE.Shape(); // 绘制形状轮廓 shape.moveTo(positions[0], positions[2]); for (let i = 3; i positions.length; i += 3) { shape.lineTo(positions[i], positions[i + 2]); } shape.lineTo(positions[0], positions[2]); // 闭合路径 // 设置挤压参数 const extrudeSettings = { steps: 1, // 挤压的分段数 depth: 0.04, // 挤压的深度 bevelEnabled: false, // 是否启用斜角 }; // 创建挤压几何体和网格 const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); const shapeMesh = new THREE.Mesh(geometry, [extrudeMaterial(0x1c212c), extrudeMaterial(0x12141c)]); shapeMesh.rotation.x = Math.PI / 2; // 旋转使其与路径方向一致 shapeMesh.userData.isLight = false return shapeMesh} 定义一个createShape的函数,其主要目的是基于传入的顶点坐标数组创建一个三维形状的网格对象。这个三维形状是通过先绘制二维形状轮廓,再对其进行挤压操作得到的,最后返回设置好相关属性(如旋转角度、用户自定义数据等)的三维网格对象,可用于三维图形渲染等相关场景。 positions:从请求中获取的二维顶点信息并通过前文提到的getPositions方法处理过的数组,数组中的元素按每三个一组的方式代表各个顶点在三维空间中的坐标(这里实际使用时主要关注每组中的第一个和第三个元素来绘制二维形状)。 首先创建一个THREE.Shape对象,用于绘制二维形状。 通过shape.moveTo(positions[0], positions[2])将绘制起点设置为传入坐标数组中第一个顶点的x和z坐标(这里似乎是在xz平面上绘制二维形状)。 接着使用for循环,从索引为3开始,每次递增3遍历坐标数组。在循环中,通过shape.lineTo(positions[i], positions[i + 2])依次连接各个顶点,绘制出二维形状的轮廓。 最后通过shape.lineTo(positions[0], positions[2])将路径闭合,形成完整的二维形状。 创建一个名为extrudeSettings的对象,用于设置挤压操作的相关参数。 其中steps属性设置为1,表示挤压过程的分段数为1;depth属性设置为0.04,确定了挤压的深度;bevelEnabled属性设置为false,表示不启用斜角效果。 利用之前创建的二维形状对象shape和设置好的挤压参数extrudeSettings,通过调用new THREE.ExtrudeGeometry(shape, extrudeSettings)创建一个挤压几何体。 然后创建一个THREE.Mesh对象,即三维网格对象。将创建好的挤压几何体作为其几何属性,同时传入两个材质对象,作为其材质属性。第一个材质为表面材质,第二个为侧面材质。 挤压缓冲几何体材质 export const extrudeMaterial = (color: number) = new THREE.MeshLambertMaterial({ color,});效果图5 挤压缓冲几何体.jpg在开发过程中由于3d场景的复杂性,以及灯光和环境对模型颜色的影响,如果直接应用UI提供设计稿的颜色,并不能完全的复刻UI稿,所以就需要一个GUI参数来实时调整材质的颜色,手动将颜色调为和UI稿一致 所以我们现在创建一个GUI并添加到dom中。 图形用户界面(GUI创建一个图形用户界面(GUI)来配置与挤压缓冲结合体相关的一些参数,并提供了对几何体材质颜色的可调节功能。通过这个 GUI,用户可以直观地修改诸如形状的顶面颜色、侧面颜色等参数,以便实时看到挤压体相关元素外观的变 const gui = new GUI({ container: document.getElementById('gui') as HTMLElement, width: 300, title: '地图配置' });export const guiParams = { shapeColor: 0xffffff, shapeColor2: 0xffffff, ...}; const shapeColor = gui.addColor(guiParams, 'shapeColor')shapeColor.name('顶面颜色')export { shapeColor }const shapeColor2 = gui.addColor(guiParams, 'shapeColor2')shapeColor2.name('侧面颜色')export { shapeColor2 }创建一个名为guiParams的对象,并将其导出,以便在其他模块中也能使用这个对象来获取或修改相关参数。 添加颜色调节控件(针对顶面颜色)使用gui.addColor方法(这是 GUI 库提供的用于添加颜色调节控件的方法),从guiParams对象中获取shapeColor属性的值,并在 GUI 上创建一个颜色调节控件。这个控件允许用户修改guiParams对象中shapeColor属性的值。 该方法返回一个对象(赋值给shapeColor变量),这个对象可以进一步进行一些设置操作,比如设置控件的名称等 下面是调整挤压缓冲几何体材质颜色的方法 // 设置GUI控制挤压体的颜色guiParams.shapeColor = shape.material[0].color.getHex();shapeColor.updateDisplay()shapeColor.onChange(function (val) { shape.material[0].color.setHex(val)});guiParams.shapeColor2 = shape.material[1].color.getHex();shapeColor2.updateDisplay()shapeColor2.onChange(function (val) { shape.material[1].color.setHex(val)});设置 GUI 参数的初始值guiParams.shapeColor = shape.material[0].color.getHex();shapeColor.updateDisplay();color.getHex()是调用该材质对象的color属性的getHex()方法,其作用是获取当前材质颜色的十六进制表示值。然后将这个值赋给guiParams对象的shapeColor属性,guiParams是用于集中管理 GUI 相关参数的对象,这样就将挤压体第一种材质的当前颜色设置为了 GUI 中对应颜色控制参数的初始值。 定义颜色修改的回调函数shapeColor.onChange(function (val) { shape.material[0].color.setHex(val)});shapeColor.onChange()是为shapeColor对象在 GUI 中的颜色控制组件注册一个onChange回调函数。当用户在 GUI 界面上通过与该颜色控制组件相关的操作,修改颜色值时,这个回调函数就会被触发。 调节颜色效果图6 改变表面颜色.gif通过前面绘制线条的方法,如法炮制绘制出其他的装饰线条并在GUI中修改到合适的颜色 效果图7 添加其他装饰线条.jpg告警标记下面代码以最外层带动画的线条举例,从创建到动画的过程。从前文效果图可以看出 这里的线组成了一个六边形,并在运动过程中通过改变六边形的角,而向外扩散。 warn && (() = { const length = 0.018; // 假设每条线的长度为0.018 const distance = length * 2; // 移动的距离 const maxDistance = distance * 2.5; // 移动的距离 const { group: tagLineGroup, tween: tagLineTween } = tagLine(color, distance, maxDistance, length, 4, warn) group.add(tagLineGroup) tweenGroup.add(tagLineTween)})();这里使用了逻辑与(&&)运算符的短路特性。如果warn变量的值为false,那么整个表达式就会立即返回false,后面的匿名函数就不会被执行;只有当warn为true时,才会执行后面的匿名函数,进入到具体的操作流程中。 首先定义了一个常量length,并将其值设置为0.018 然后根据length计算出distance,它是线初始距离,通过length乘以2得到。最终组成一个完整而不重叠的六边形 最后计算出maxDistance,移动的距离,通过distance乘以2.5得到。移动后的效果即展开六边形的角 tagLine 方法 const tagLine = (color: number, distance: number, maxDistance: number, length: number, width: number, warn = false) = { const group = new THREE.Group() const angle = Math.PI / 6; // 60度的弧度值 const x2 = 0; const z2 = 0; const x1 = length * Math.cos(angle); const z1 = length * Math.sin(angle); const x3 = -length * Math.cos(angle); const z3 = length * Math.sin(angle); const positions = [x1, 0, z1, x2, 0, z2, x3, 0, z3]; const line = createLine(positions, color, 1, width, true); for (let i = 0; i i++) { const newLine = line.clone() newLine.rotation.y = i * Math.PI / 3 + Math.PI / 2 newLine.position.x -= distance * Math.cos(i * Math.PI / 3); newLine.position.z += distance * Math.sin(i * Math.PI / 3); newLine.userData.warn = warn group.add(newLine) } const tween = new TWEEN.Tween({ distance }) .to({ distance: maxDistance }, 1000) .easing(TWEEN.Easing.Quadratic.Out) .onUpdate((value) = { // 更新位置 group.children.forEach((child: any, i) = { child.position.x = -value.distance * Math.cos(i * Math.PI / 3); child.position.z = value.distance * Math.sin(i * Math.PI / 3); }) }) .onComplete(() = { // 动画完成后的操作 }) .repeat(Infinity) // 添加重复动画 .yoyo(true) // 使动画往返 return { group, tween }} 代码介绍const group = new THREE.Group();const angle = Math.PI / 6; // 60度的弧度值首先创建了一个THREE.Group对象,将其赋值给变量group。这个对象将作为一个容器,用于存放后续创建的所有线条对象,以便对它们进行统一的管理和操作,比如整体的移动、旋转等。 接着定义了一个常量angle,其值为Math.PI / 6,也就是将角度值60度转换为弧度值,这个角度值在后续计算线条端点坐标等操作中会用到。 计算线条端点坐标并创建初始线条对象const x2 = 0;const z2 = 0;const x1 = length * Math.cos(angle);const z1 = length * Math.sin(angle);const x3 = -length * Math.cos(angle);const z3 = length * Math.sin(angle); const positions = [x1, 0, z1, x2, 0, z2, x3, 0, z3]; const line = createLine(positions, color, 1, width, true);先分别定义了几个变量x2、z2、x1、z1、x3、z3,并通过三角函数(根据前面定义的角度angle)结合length参数计算出它们的值。这些变量的值将作为线条端点的坐标值,其中x2和z2都设置为0,而x1、z1、x3、z3则是根据三角函数计算得到的与length和angle相关的坐标值。 然后将这些坐标值按照一定顺序(每三个一组,分别代表x、y、z坐标)组成一个数组positions,这个数组将作为创建线条对象的顶点坐标数据。 最后调用createLine函数,与前文创建线条方法相同,传入positions、color、透明度值1、width以及布尔值true作为参数,创建出一个初始的线条对象,并将其赋值给变量line。 克隆并设置多条线条对象的属性及添加到组中for (let i = 0; i i++) { const newLine = line.clone(); newLine.rotation.y = i * Math.PI / 3 + Math.PI / 2; newLine.position.x -= distance * Math.cos(i * Math.PI / 3); newLine.position.z += distance * 3 * Math.sin(i * Math.PI / 3); newLine.userData.warn = warn; group.add(newLine);}使用for循环,循环次数为6次。以此作为六边形的六个角 首先通过调用line.clone()方法克隆出一个新的线条对象,并将其赋值给变量newLine。这样就得到了与初始线条对象line具有相同属性(除了后续要设置的新属性外)的新线条对象。 接着设置新线条对象newLine的旋转角度,通过newLine.rotation.y = i * Math.PI / 3 + Math.PI / 2将其绕y轴旋转一定的角度,这个角度是根据循环变量i以及固定的角度值(Math.PI / 3和Math.PI / 2)计算得到的,使得每条新线条对象都有不同的旋转角度。 然后设置新线条对象的位置,通过newLine.position.x -= distance * Math.cos(i * Math.PI / 3)和newLine.position.z += distance * Math.sin(i * Math.PI / 3)来调整其在x和z轴方向上的位置,这里的位置调整也是根据循环变量i和初始的distance参数来计算的,使得每条新线条对象都有不同的位置。 最后将warn参数的值赋给新线条对象的userData.warn属性,即设置每条新线条对象的警告状态,以便后续根据这个状态进行相关操作。 在完成上述属性设置后,将新线条对象newLine添加到之前创建的线条组对象group中,通过group.add(newLine)实现,这样就将所有克隆并设置好属性的线条对象都收集到了group这个容器中。 创建动画控制对象并设置动画相关属性const tween = new TWEEN.Tween({ distance }).to({ distance: maxDistance }, 1000).easing(TWEEN.Easing.Quadratic.Out).onUpdate((value) = { // 更新位置 group.children.forEach((child: any, i) = { child.position.x = -value.distance * Math.cos(i * Math.PI / 3); child.position.z = value.distance * Math.sin(i * Math.PI / 3); })}).onComplete(() = { // 动画完成后的操作}).repeat(Infinity) // 添加重复动画.yoyo(true) // 使动画往返首先创建一个TWEEN.Tween对象,传入一个包含distance属性的对象作为初始状态。这个TWEEN.Tween对象将用于控制前面创建的线条组对象group的动画效果。 接着通过.to({ distance: maxDistance }, 1000)设置动画的目标状态,即要将distance属性的值在1000毫秒(也就是1秒)内变化到maxDistance的值,从而实现线条在动画过程中的位置变化。 通过.easing(TWEEN.Easing.Quadratic.Out)选择了一种缓动函数,这里选择的是二次函数的输出型缓动函数(Quadratic.Out),它决定了动画过程中速度的变化规律,使得动画效果更加自然。 定义了一个.onUpdate回调函数,当动画在更新过程中(也就是在1000毫秒的动画时间内),这个回调函数会被调用。在回调函数内部,通过遍历group.children(也就是线条组对象group的所有子对象,即之前创建的所有线条对象),并根据当前动画的distance值(通过value.distance获取)以及循环变量i(与之前设置线条位置时的循环变量一致),重新计算并设置每条线条对象的位置,即child.position.x = -value.distance * Math.cos(i * Math.PI / 3)和child.position.z = value.distance * Math.sin(i * Math.PI / 3),从而实现了在动画过程中线条位置的实时更新。 定义了一个.onComplete回调函数,当动画完成(也就是distance的值达到maxDistance)后,这个回调函数会被调用,这里虽然没有具体的操作内容,但可以在后续根据具体需求添加相关操作,比如重置动画状态等。 通过.repeat(Infinity)设置动画为无限重复,使得动画会不断地循环播放,呈现出持续的动态效果。 通过.yoyo(true)设置动画为往返式动画,即动画到达目标状态后会按照相反的顺序返回初始状态,然后再重复播放,这样可以增加动画的趣味性和动态性。 函数返回值解析最后,函数返回一个包含两个属性的对象,即{ group, tween }。其中group是包含了所有创建好的线条对象的THREE.Group对象,可以在其他地方对这个组对象进行进一步的操作,比如添加到三维场景中、进行整体的移动或旋转等。 而tween是用于控制这些线条对象动画的TWEEN.Tween对象,将动画导出,并添加到TWEEN.Group中进行统一操作。比如开始,暂停,重置等等。并且可以根据需要进一步修改动画的相关属性。 开始动画tweenGroup.getAll().forEach((tween) = { tween.start()})通过以上代码即可实现一个六边形沿着六个角的方向进行运动的动画 效果图8 六角动画.gif有了这一个例子,其他也就都可以做出来了,通过计算顶点信息,存入positions中,再通过createLine创建出线条并添加到场景中即可。 告警2d标记通过createTagText创建一个2d标记,并添加到告警标记的组中,通过调整y轴位置,让标记显示到合适的位置。 if (warn) { const panel = createTagPanel(); group.add(panel)}createTagPanel 方法创建一个简单的 HTMLdiv元素,为其添加特定的样式类,将该div元素封装成一个CSS2DObject,设置其内部 HTML 内容,最后返回这个CSS2DObject对象,以便在其他代码部分可能用于在三维场景中以二维平面的形式展示相关信息或元素。 export const createTagPanel = () = { const div = document.createElement('div'); div.classList.add('tag-panel'); const label = new CSS2DObject(div); div.innerHTML = ` div ... /div ` const {size} = getMeshInfo(label) console.log('size',size); label.position.set(0,0.3,0); return label;}首先,通过new CSS2DObject(div)创建了一个CSS2DObject对象,并将其赋值给变量label。CSS2DObject是一种特殊的对象类型,常用于在三维场景中以二维平面的方式展示与之关联的 HTML 元素。在这里,就是将前面创建的div元素与这个CSS2DObject相关联,使得后续可以在三维场景的特定位置展示该div元素及其包含的内容。 渲染2d元素css2dObject需要CSS2DRenderer进行渲染。 const labelRenderer = new CSS2DRenderer();labelRenderer.setSize(width, height);labelRenderer.domElement.classList.add('label-renderer');const main = document.querySelector('main')main && main.appendChild( labelRenderer.domElement );这里通过new CSS2DRenderer()创建了一个新的CSS2DRenderer对象,并将其赋值给变量labelRenderer。CSS2DRenderer能够将 CSS 样式应用到相关的 DOM 元素上,使得这些元素可以按照设定的样式在页面或者特定的场景中展示出来。 使用labelRenderer.setSize(width, height)方法为刚刚创建的CSS2DRenderer对象设置尺寸。这里的width和height为3d场景容器的尺寸,分别代表要设置的宽度和高度值。通过设置尺寸,确定了这个渲染器在页面或者相关场景中所占据的空间大小,以便后续能够准确地展示相关的 CSS 样式内容。 labelRenderer.domElement可以获取到与CSS2DRenderer对象关联的 DOM 元素。然后,通过classList.add('label-renderer')方法为这个 DOM 元素添加了一个名为label-renderer的样式类。在项目的 CSS 样式表中,应该对这个样式类有相应的样式定义,用于设置该 DOM 元素的外观特征,比如大小、颜色、边框样式等,使得这个 DOM 元素在页面上能够呈现出特定的视觉效果。 标记动画这里标记用tween也可以实现,文中效果使用css关键帧动画实现的,相对于tween来说,css消耗的资源更少。 @keyframes tag-panel-blink { 0%, 18%, 45%, 60% { opacity: 0; } 5%, 30%, 71%, 100% { opacity: 1; }} .tag-panel { animation: tag-panel-blink 3s infinite;}定义一个名为tag-panel-blink的动画关键帧序列。 在这个关键帧序列中,指定了不同百分比阶段下元素的不透明度(opacity)值的变化: 在动画的起始点(0%)、以及动画进行到18%、45%和60%这些时刻,将元素的不透明度设置为0,这意味着在这些时间点上,具有该动画应用的元素将会是完全透明的,不可见的状态。 而在动画进行到5%、30%、71%和动画结束点(100%)时,将元素的不透明度设置为1,此时元素将会是完全不透明的,呈现出正常可见的状态。 通过这样的关键帧设置,就定义了一个元素不透明度在动画过程中来回变化的规律,从而实现闪烁效果 效果图 9 2d标记.gif告警连接线无动画效果10 贝塞尔曲线.jpg通过创建一个贝塞尔曲线,并获取它的点位信息,再绘制一个line2,同样使用createLine方法。下面是具体方法 // 将警告位置转换为THREE.Vector3对象const vector3s = warningPos.map((item) = new THREE.Vector3().fromArray(item.position)); // 定义贝塞尔曲线的两个控制手柄const handle1 = new THREE.Vector3(0.352343, 0.02, -0.14313); // 控制贝塞尔曲线的手柄1const handle2 = new THREE.Vector3(-0.352343, 0.02, 0.14313); // 控制贝塞尔曲线的手柄2 // 创建三次贝塞尔曲线const curve = new THREE.CubicBezierCurve3(vector3s[0], handle1, handle2, vector3s[2]); // 获取曲线上的点export const points = curve.getPoints(50); // 获取点的位置数组const array = getPointsFromPosition(points.map((item, index) = index === 0 ? item : points[0])) // 创建线条对象const line = createLine(array, 0xff0000, 1, 4, true, true);line.geometry.setPositions(array); // 获取所有点的位置数组 const allArray = getPointsFromPosition(points)格式转换贝塞尔曲线的两端为告警信息的位置,手柄为自定义,也可以结合告警1和告警2的位置通过三角函数计算出想要的位置,文中只是模拟了一个手柄的位置,这两个向量对象通过指定的x、y、z坐标值来确定其在三维空间中的位置。这些控制手柄将在后续创建贝塞尔曲线时起到调整曲线形状的作用。 // 将警告位置转换为THREE.Vector3对象const vector3s = warningPos.map((item) = new THREE.Vector3().fromArray(item.position));warningPos是一个包含多个对象的数组,每个对象都有一个position属性,该属性的值是一个数字数组,包含[x、y、z]三个信息, 通过map方法遍历warningPos数组中的每个元素item。对于每个元素,创建一个新的THREE.Vector3对象,并使用fromArray方法将item.position数组中的值作为坐标来初始化这个向量对象。最终将所有创建好的THREE.Vector3对象组成一个新的数组vector3s。这样就将原始的警告位置数据转换为了适合在三维场景中使用的THREE.Vector3对象形式。 创建三次贝塞尔曲线// 定义贝塞尔曲线的两个控制手柄const handle1 = new THREE.Vector3(0.352343, 0.02, -0.14313); const handle2 = new THREE.Vector3(-0.352343, 0.02, 0.14313); // 创建三次贝塞尔曲线const curve = new THREE.CubicBezierCurve3(vector3s[0], handle1, handle2, vector3s[2]); 使用new THREE.CubicBezierCurve3构造函数来创建一条三次贝塞尔曲线。这个构造函数需要传入四个参数,分别是曲线的起始点、第一个控制手柄、第二个控制手柄和曲线的终点。 起始点取的是之前转换得到的vector3s数组中的第一个元素vector3s[0],即第一个警告位置对应的THREE.Vector3对象;终点取的是vector3s数组中的第三个元素vector3s[2];中间传入了前面定义好的两个控制手柄handle1和handle2。通过这样的设置,就创建出了一条基于给定的警告位置和控制手柄的三次贝塞尔曲线。 获取曲线上的点// 获取曲线上的点export const points = curve.getPoints(50); // 获取点的位置数组const array = getPointsFromPosition(points.map((item, index) = index === 0? item : points[0]))首先对points数组进行了一次map操作。在这个map操作中,对于points数组中的每个元素item,当索引index等于0时,就直接返回该元素item;否则返回points数组中的第一个元素points[0]。 定义打开动画// 定义打开动画const tweenOpen = new TWEEN.Tween({ index: 0}).to({ index: allArray.length}, timeLine * 1000).onUpdate(({ index }) = { update(Math.floor(index));}) .onComplete(() = { // 重新开始关闭动画 tweenClose.start(); }) .start();创建TWEEN.Tween对象首先创建了一个TWEEN.Tween对象,传入一个初始状态对象{ index: 0 },这里的index变量将用于在动画过程中跟踪进度,初始值设置为0。 设置动画目标状态和时长通过.to({ index: allArray.length }, timeLine * 1000)设置了动画的目标状态,即要在timeLine * 1000毫秒(也就是前面定义的1秒,因为timeLine = 1)内将index的值从初始的0变化到allArray.length,这个过程控制了动画的持续时间和进度范围。 定义动画更新回调函数.onUpdate(({ index }) = { update(Math.floor(index)); })定义了一个在动画更新过程中每次被调用的回调函数。在每次更新时,会获取当前的index值(通过解构赋值得到),并将其向下取整后传入update函数(后续会详细解析update函数),用于根据当前动画进度处理点的位置数据并更新线条位置。 定义动画完成回调函数.onComplete(() = { tweenClose.start(); })定义了一个在动画完成(即index达到allArray.length)时被调用的回调函数。在这个回调函数中,会启动tweenClose动画(后续会详细解析tweenClose动画),实现打开动画完成后紧接着开始关闭动画的效果,从而形成一个循环的动画序列。 启动动画最后通过.start()方法启动了tweenOpen动画,使其开始按照设定的规则进行播放。 定义关闭动画// 定义关闭动画const tweenClose = new TWEEN.Tween({ index: 0}).to({ index: allArray.length}, timeLine * 1000).onUpdate(({ index }) = { update2(Math.floor(index));}).onComplete(() = { // 重新开始打开动画 tweenOpen.start();})与定义打开动画类似,这里也创建了一个TWEEN.Tween对象,初始状态为{ index: 0 },设置了在timeLine * 1000毫秒内将index值从0变化到allArray.length的目标状态。 在动画更新过程中,通过.onUpdate(({ index }) = { update2(Math.floor(index)); })定义了一个回调函数,每次更新时将当前index值向下取整后传入update2函数(后续会详细解析update2函数),用于根据当前动画进度处理点的位置数据并更新线条位置。 在动画完成时,通过.onComplete(() = { tweenOpen.start(); })定义了一个回调函数,当动画完成后会启动tweenOpen动画,实现关闭动画完成后紧接着开始打开动画的效果,与打开动画形成循环播放的关系。 定义用于动画更新的update函数(打开动画更新逻辑)// 更新函数,用于动画更新const update = (index: number) = { if (index % 3 === 0) { const newArray = [...allArray]; const newArr = newArray.slice(0, index); const lastArr = newArr.slice(-3); const oldArr = array.slice(index); for (let i = 0; i oldArr.length; i++) { if (i % 3 === 0) { newArr.push(...lastArr); } } // 更新线条位置 if (newArr.length === allArray.length) { updateLine(newArr); } }}条件判断首先通过if (index % 3 === 0)进行条件判断,只有当index除以3的余数为0时才会执行后续的操作,每三个点及是点位信息的x,y,z,以实现特定的动画效果。 数据处理创建了一个新数组newArray,并通过[...allArray]将allArray中的所有元素复制到newArray中,作为后续处理的基础数据。 通过newArray.slice(0, index)获取newArray中从开头到index位置(不包括index)的子数组,并赋值给newArr。 通过newArr.slice(-3)获取newArr的最后三个元素,赋值给lastArr,这三个元素后续的处理中起到关键作用。 通过array.slice(index)获取从index位置开始的array数组中的元素赋值给oldArr。 然后通过一个循环,遍历oldArr数组,当i % 3 === 0时,将lastArr中的元素添加到newArr中,这样就根据特定规则对新 Arr 进行了扩展处理。 更新线条位置最后通过if (newArr.length === allArray.length)进行条件判断,当处理后的newArr数组长度与allArray数组长度相等时,调用updateLine函数(后续会详细解析updateLine函数),并传入newArr,用于更新线条的位置,实现根据动画进度调整线条位置的效果。 定义用于动画更新的update2函数(关闭动画更新逻辑)// 更新函数,用于动画更新const update2 = (index: number) = { if (index % 3 === 0) { const oldArr = allArray.slice(index); const lastArr = oldArr.slice(0, 3); const newArr = allArray.slice(0, index); const preArr = []; for (let i = 0; i newArr.length; i++) { if (i % 3 === 0) { preArr.push(...lastArr); } } const points = [...preArr,...oldArr]; if (points.length === allArray.length) { updateLine(points); } }}条件判断同样通过if (index % 3 === 0)进行条件判断,只有满足该条件时才执行后续操作。 数据处理通过allArray.slice(index)获取从index位置开始的allArray数组中的元素,赋值给oldArr。 通过oldArr.slice(0, 3)获取oldArr的前三个元素,赋值给lastArr。 通过allArray.slice(0, index)获取从开头到index位置(不包括index)的allArray数组中的元素,赋值给newArr。 创建一个空数组preArr,然后通过一个循环遍历newArr数组,当i % 3 === 0时,将lastArr中的元素添加到preArr中,这样就构建了一个新的数组preArr。 通过[...preArr,...oldArr]将preArr和oldArr中的元素合并成一个新数组points。 更新线条位置最后通过if (points.length === allArray.length)进行条件判断,当合并后的points数组长度与allArray数组长度相等时,调用updateLine函数,传入points,用于更新线条的位置,实现根据动画进度调整线条位置的效果。 定义用于更新线条点位信息的updateLine函数// 更新线条点位信息const updateLine = (points: number[]) = { line.geometry.setPositions(points); line.computeLineDistances();}这个函数接收一个包含数字的数组points作为参数,它的主要作用是更新线条对象的位置信息。 通过line.geometry.setPositions(points)将传入的points数组设置为线条对象,从而改变线条的形状。 通过line.computeLineDistances()计算线条的距离相关信息,进一步完善线条的相关属性更新,以确保线条在动画过程中的正确显示。 在更新动画中一定要保证组成线的点位至少为两个,不然会报缓存不足,感兴趣的童鞋可以查看一下源码。node_modules/three/examples/jsm/lines/LineGeometry.js 报错详情11 空间不足.jpg效果图12 连接线动画.gif模拟龙卷风创建一个粒子系统来模拟类似龙卷风的效果,包括生成粒子的初始位置、速度和颜色信息,设置粒子的材质以便正确渲染颜色,通过更新函数实现粒子的动态运动(如向上移动、围绕中心旋转等),以及利用TWEEN.Tween实现龙卷风位置的动其画效果,使能够在一系列给定的位置间移动并带有倾斜等姿态变化。 const group = new THREE.Group();scene.add(group);const particleCount = 5000;const particles = new THREE.BufferGeometry();const positions = new Float32Array(particleCount * 3);const velocities = new Float32Array(particleCount * 3);const colors = new Float32Array(particleCount * 3); // 新增颜色数组 const xzExpansionRange = 0.6; // 提取x和z的扩散范围变量const maxHeight = 0.8; // 提取高度变量 const curveFactor = 1.2; // 提取曲线弧度变量const curveCalculationFactor = Math.PI; // 提取曲线弧度计算方式为变量 const whiteColor = new THREE.Color(0xffffff); // 设置粒子颜色为白色 for (let i = 0; i particleCount; i++) { const theta = Math.random() * 2 * Math.PI; const height = Math.random() * maxHeight / 2; const expansionFactor = height / maxHeight + Math.random() / 5; // 根据y值计算扩散因子 const xs = height * Math.random(); // 使用曲线差值计算半径,增加曲线弧度 const radius = Math.sin(height / maxHeight * curveCalculationFactor * curveFactor * xs) * xzExpansionRange * expansionFactor; positions[i * 3] = radius * Math.cos(theta); positions[i * 3 + 1] = height; positions[i * 3 + 2] = radius * Math.sin(theta); velocities[i * 3] = -Math.sin(theta) * 0.01; velocities[i * 3 + 1] = 0.02; velocities[i * 3 + 2] = Math.cos(theta) * 0.01; // 设置颜色为白色 colors[i * 3] = whiteColor.r; // R colors[i * 3 + 1] = whiteColor.g; // G colors[i * 3 + 2] = whiteColor.b; // B} particles.setAttribute('position', new THREE.BufferAttribute(positions, 3));particles.setAttribute('velocity', new THREE.BufferAttribute(velocities, 3));particles.setAttribute('color', new THREE.BufferAttribute(colors, 3)); // 设置颜色属性 const material = new THREE.ShaderMaterial({ vertexShader: ` varying vec3 vColor; attribute vec3 color; // 声明颜色属性 void main() { vColor = color; // 使用传入的颜色 vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_PointSize = 5.0; // 固定点大小 gl_Position = projectionMatrix * mvPosition; } `, fragmentShader: ` varying vec3 vColor; void main() { gl_FragColor = vec4(vColor, 1.0); } `, blending: THREE.NormalBlending, // 取消扩散小托 depthTest: true, transparent: true}); const particleSystem = new THREE.Points(particles, material);group.add(particleSystem); let rotationFactor = 0.09; // 控制转动速度的因子,可以根据需要调整rotationFactorValue.onChange((value: number) = { rotationFactor = value;})export const animateParticles = () = { const time = Date.now() * 0.00001; const centerX = 0; // 龙卷风中心在x轴的位置,这里设为0,可根据实际需求调整 const centerZ = 0; // 龙卷风中心在z轴的位置,这里设为0,可根据实际需求调整 for (let i = 0; i particleCount; i++) { const index = i * 3; // 获取当前粒子的位置和速度信息 const x = positions[index]; const z = positions[index + 2]; // 更新粒子的y坐标,使其向上移动 // positions[index + 1] = y + velocities[index + 1]; // 向上移动 // 如果粒子超过maxHeight/2,则重置到底部 if (positions[index + 1] maxHeight / 2) { positions[index + 1] = 0; // 重置y坐标 } // 计算粒子相对于龙卷风中心的位置 const relativeX = x - centerX; const relativeZ = z - centerZ; // 根据时间和一些参数来更新粒子位置,实现向上缠绕的效果 const newRelativeX = relativeX * Math.cos(rotationFactor * time) - relativeZ * Math.sin(rotationFactor * time); const newRelativeZ = relativeX * Math.sin(rotationFactor * time) + relativeZ * Math.cos(rotationFactor * time); // 更新粒子的x和z坐标 positions[index] = newRelativeX + centerX; positions[index + 2] = newRelativeZ + centerZ; } // 标记位置属性需要更新 particles.attributes.position.needsUpdate = true;}; export const animateTornado = (positions: THREE.Vector3[]) = { new TWEEN.Tween({ index: 0 }) .to({ index: positions.length - 1 }, 5000) // 动画持续时间为5000毫秒 .onUpdate(({ index }) = { const currentIndex = Math.floor(index); const nextIndex = currentIndex + 1 positions.length ? currentIndex + 1 : 0; // 更新龙卷风的位置 particleSystem.position.copy(positions[currentIndex]); particleSystem.lookAt(positions[nextIndex]); // 使粒子系统朝向下一个位置 const tiltAngle = Math.PI / 18; // 10度的倾斜角度 const tiltAxis = new THREE.Vector3(1, 0, 0); // 沿着x轴倾斜 const quaternion = new THREE.Quaternion().setFromAxisAngle(tiltAxis, tiltAngle); particleSystem.quaternion.multiplyQuaternions(quaternion, particleSystem.quaternion); }) .repeat(Infinity) // 添加重复动画 .start();}; 初始化粒子相关参数和数据结构const particleCount = 5000;const particles = new THREE.BufferGeometry();const positions = new Float32Array(particleCount * 3);const velocities = new Float32Array(particleCount * 3);const colors = new Float32Array(particleCount * 3); // 新增颜色数组 const xzExpansionRange = 0.6; // 提取x和z的扩散范围变量const maxHeight = 0.8; // 提取高度变量 const curveFactor = 1.2; // 提取曲线弧度变量const curveCalculationFactor = Math.PI; // 提取曲线弧度计算方式为变量 const whiteColor = new THREE.Color(0xffffff); // 设置粒子颜色为白色首先定义了一系列常量和数组来准备生成粒子系统所需的数据: particleCount:指定了要生成的粒子数量为 5000 个。 particles:创建了一个THREE.BufferGeometry对象,用于存储粒子的几何信息,后续会将粒子的位置、速度、颜色等属性设置到这个对象中。 positions、velocities、colors:分别创建了长度为particleCount * 3的Float32Array数组,用于存储每个粒子的三维位置坐标、速度向量以及颜色信息(每个粒子的颜色信息也是以三维向量的形式存储,分别对应红、绿、蓝三个颜色通道)。 xzExpansionRange、maxHeight、curveFactor、curveCalculationFactor:这些常量分别定义了与粒子分布和运动相关的一些参数,如x和z方向的扩散范围、粒子所能达到的最大高度、用于计算曲线弧度的因子以及曲线弧度的计算方式(这里以Math.PI为基础)。 whiteColor:创建了一个表示白色的THREE.Color对象,用于后续设置粒子的颜色。 生成粒子的初始位置、速度和颜色信息for (let i = 0; i particleCount; i++) { const theta = Math.random() * 2 * Math.PI; const height = Math.random() * maxHeight / 2; const expansionFactor = height / maxHeight + Math.random() / 5; // 根据y值计算扩散因子 const xs = height * Math.random(); // 使用曲线差值计算半径,增加曲线弧度 const radius = Math.sin(height / maxHeight * curveCalculationFactor * curveFactor * xs) * xzExpansionRange * expansionFactor; positions[i * 3] = radius * Math.cos(theta); positions[i * 3 + 1] = height; positions[i * 3 + 2] = radius * Math.sin(theta); velocities[i * 3] = -Math.sin(theta) * 0.01; velocities[i * 3 + 1] = 0.02; velocities[i * 3 + 2] = Math.cos(theta) * 0.01; // 设置颜色为白色 colors[i * 3] = whiteColor.r; // R colors[i * 3 + 1] = whiteColor.g; // G colors[i * 3 + 2] = whiteColor.b; // B}通过一个循环遍历particleCount次,为每个粒子生成初始的位置、速度和颜色信息: 位置信息生成首先生成一个随机角度theta,范围是从 0 到2 * Math.PI,用于确定粒子在水平面上的角度位置。 随机生成一个粒子的高度height,范围是从 0 到maxHeight / 2。 根据height计算一个扩散因子expansionFactor,它与粒子在x和z方向的扩散程度有关。 通过一些计算(涉及到曲线弧度相关参数)得到粒子在水平面上的半径radius,然后根据三角函数计算出粒子的三维位置坐标,并分别存储到positions数组中对应的位置(每三个元素一组,分别对应x、y、z坐标)。 速度信息生成同样基于theta以及一些固定的速度系数(如0.01和0.02),计算出粒子在三维空间中的速度向量,并存储到velocities数组中对应的位置。 颜色信息设置将之前创建的白色THREE.Color对象的红、绿、蓝三个颜色通道的值分别赋给colors数组中对应粒子的颜色信息位置,从而将所有粒子的颜色都设置为白色。 设置粒子的属性到BufferGeometry对象particles.setAttribute('position', new THREE.BufferAttribute(positions, 3));particles.setAttribute('velocity', new THREE.BufferAttribute(velocities, 3));particles.setAttribute('color', new THREE.BufferAttribute(colors, 3)); // 设置颜色属性使用particles.setAttribute方法将之前生成的粒子位置、速度和颜色信息数组分别设置为particles对象的对应属性。每个属性都通过创建一个新的THREE.BufferAttribute对象,并传入相应的数组和每个属性的维度(这里都是 3,因为是三维空间的信息)来完成设置。 创建粒子的材质并设置相关属性const material = new THREE.ShaderMaterial({ vertexShader: ` varying vec3 vColor; attribute vec3 color; // 声明颜色属性 void main() { vColor = color; // 使用传入的颜色 vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_PointSize = 5.0; // 固定点大小 gl_Position = projectionMatrix * mvPosition; } `, fragmentShader: ` varying vec3 vColor; void main() { gl_FragColor = vec4(vColor, 1.0); } `, blending: THREE.NormalBlending, // 取消扩散小托 depthTest: true, transparent: true});创建了一个THREE.ShaderMaterial对象作为粒子的材质,并设置了相关的属性: 顶点着色器(vertexShader)声明了一个varying变量vColor,用于在顶点着色器和片段着色器之间传递颜色信息。 声明了一个attribute变量color,用于接收从BufferGeometry对象传递过来的粒子颜色属性。 在main函数中,将接收到的color属性值赋给vColor,以便后续在片段着色器中使用。同时,进行了一些常规的坐标变换操作(如通过modelViewMatrix和projectionMatrix对粒子的位置进行变换),并设置了固定的点大小为5.0。 片段着色器(fragmentShader)接收从顶点着色器传递过来的vColor变量,并将其转换为gl_FragColor,用于设置最终渲染的颜色,这里将颜色的透明度设置为1.0,表示完全不透明(因为在材质的其他属性中已经设置了transparent: true,所以这里可以根据需要调整透明度)。 其他材质属性blending: THREE.NormalBlending:设置了混合模式为正常混合,用于处理粒子之间以及粒子与背景之间的颜色混合情况,这里可能是为了避免一些不必要的颜色扩散效果(具体效果需结合实际场景确定)。 depthTest: true:开启深度测试,确保粒子在三维空间中的渲染顺序正确,即离观察者近的粒子会遮挡住离观察者远的粒子。 transparent: true:设置材质为透明的,这与片段着色器中设置的颜色透明度可以配合使用,以便在需要时实现半透明等效果。 设置粒子系统转动速度因子及更新函数let rotationFactor = 0.09; // 控制转动速度的因子,可以根据需要调整rotationFactorValue.onChange((value: number) = { rotationFactor = value;})export const animateParticles = () = { const time = Date.now() * 0.00001; const centerX = 0; // 龙卷风中心在x轴的位置,这里设为 0,可根据实际需求调整 const centerZ = 0; // 龙卷风中心在z轴的位置,这里设为 0,可根据实际需求调整 for (let i = 0; i particleCount; i++) { const index = i * 3; // 获取当前粒子的位置和速度信息 const x = positions[index]; const z = positions[index + 2]; // 更新粒子的y坐标,使其向上移动 // positions[index + 1] = y + velocities[index + 1]; // 向上移动 // 如果粒子超过maxHeight/2,则重置到底部 if (positions[index + 1] maxHeight / 2) { positions[index + 1] = 0; // 重置y坐标 } // 计算粒子相对于龙卷风中心的位置 const relativeX = x - centerX; const relativeZ = z - centerZ; // 根据时间和一些参数来更新粒子位置,实现向上缠绕的效果 const newRelativeX = relativeX * Math.cos(rotationFactor * time) - relativeZ * Math.sin(rotationFactor * time); const newRelativeZ = relativeX * Math.sin(rotationFactor * time) + relativeZ * Math.cos(rotationFactor * time); // 更新粒子的x和z坐标 positions[index] = newRelativeX + centerX; positions[index + 2] = newRelativeZ + centerZ; } // 标记位置属性需要更新 particles.attributes.position.needsUpdate = true;};粒子动画更新函数(animateParticles)首先获取当前时间time,并进行了一定的单位换算(乘以0.00001),以便在后续的计算中使用合适的时间尺度。 定义了龙卷风中心在x轴和z轴的位置,这里都设置为0,可根据实际需求调整。 通过一个循环遍历所有粒子,对于每个粒子: 获取当前粒子的位置信息(x和z坐标),并根据粒子的y坐标情况进行处理: 如果粒子的y坐标超过了maxHeight / 2,则将其y坐标重置为0,实现粒子在达到一定高度后重新回到底部的效果。 计算粒子相对于龙卷风中心的位置(relativeX和relativeZ)。 根据时间time和转动速度因子rotationFactor,通过三角函数计算出更新后的粒子相对于龙卷风中心的位置(newRelativeX和newRelativeZ),从而实现粒子围绕龙卷风中心转动并向上缠绕的效果。 更新粒子的x和z坐标,将其设置为相对于龙卷风中心更新后的位置加上龙卷风中心的坐标。 最后,为了确保粒子的位置属性更新能够被正确渲染,将particles.attributes.position.needsUpdate设置为true,告诉渲染引擎需要更新粒子的位置信息。 龙卷风位置动画函数export const animateTornado = (positions: THREE.Vector3[]) = { new TWEEN.Tween({ index: 0 }) .to({ index: positions.length - 1 }, 5000) // 动画持续时间为5000毫秒 .onUpdate(({ index }) = { const currentIndex = Math.floor(index); const nextIndex = currentIndex + 1 positions.length? currentIndex + 1 : 0; // 更新龙卷风的位置 particleSystem.position.copy(positions[currentIndex]); particleSystem.lookAt(positions[nextIndex]); // 使粒子系统朝向下一个位置 const tiltAngle = Math.PI / 18; // 10度的倾斜角度 const tiltAxis = new THREE.Vector3(1, 0, 0); // 沿着x轴倾斜 const quaternion = new THREE.Quaternion().setFromAxisAngle(tiltAxis, tiltAngle); particleSystem.quaternion.multiplyQuaternions(quaternion, particleSystem.quaternion); }) .repeat(Infinity) // 添加重复动画 .start();};这个函数接受一个包含THREE.Vector3对象的数组positions作为参数,用于实现龙卷风在一系列给定位置间移动的动画效果: 创建了一个TWEEN.Tween对象,初始状态设置为{ index: 0 },并设置目标状态为{ index: positions.length - 1 },动画持续时间为 5000 毫秒。这意味着在 5000 毫秒内,index的值会从 0 变化到positions.length - 1,用于控制动画的进度。 在onUpdate回调函数中: 获取当前的index值(通过向下取整得到currentIndex),并根据currentIndex确定下一个位置的索引nextIndex,如果当前索引已经是最后一个位置,则下一个位置索引设置为 0,实现循环遍历给定位置的效果。 更新粒子系统的位置,将其设置为当前位置positions[currentIndex]。 使粒子系统朝向下一个位置positions[nextIndex],通过lookAt方法实现粒子系统的朝向调整。 设置一个倾斜角度tiltAngle为Math.PI / 18(即 10 度),并定义倾斜轴为沿着x轴方向的THREE.Vector3(1, 0, 0)。通过创建一个THREE.Quaternion对象,并使用setFromAxisAngle方法根据倾斜轴和倾斜角度设置其值,然后将这个四元数与粒子系统现有的四元数通过multiplyQuaternions方法相乘,实现粒子系统在移动过程中的倾斜效果。 通过repeat(Infinity)设置动画为无限重复,使得龙卷风会在给定的位置序列中不断循环移动并带有倾斜等姿态变化。最后通过start()启动动画。 效果图13 龙卷风效果图.gif发光体import * as THREE from "three";import { EffectComposer, OutputPass, RenderPass, ShaderPass, UnrealBloomPass } from "three/examples/jsm/Addons.js";import { scene, camera, width, height, renderer } from "../content/scene";import { guiParams } from "../content/GUI"; const darkMaterial = new THREE.MeshBasicMaterial({ color: 'black',opacity: 0,transparent: true }); const createParams = { threshold: 0, strength: 0.39, // 强度 radius: 0.1,// 半径 exposure: 0.5 // 扩散}; const materials: any = {} const bloomLayer = new THREE.Layers();const BLOOM_SCENE = 1;bloomLayer.set(BLOOM_SCENE); // 渲染器通道,将场景全部加入渲染器const renderScene = new RenderPass(scene, camera);// 添加虚幻发光通道const bloomPass = new UnrealBloomPass(new THREE.Vector2(width, height), 1.5, 0.4, 0.85);bloomPass.threshold = createParams.threshold;bloomPass.strength = createParams.strength;bloomPass.radius = createParams.radius; // 创建合成器const bloomComposer = new EffectComposer(renderer);bloomComposer.renderToScreen = false;// 将渲染器和场景结合到合成器中bloomComposer.addPass(renderScene);bloomComposer.addPass(bloomPass); // 着色器通道const mixPass = new ShaderPass( // 着色器 new THREE.ShaderMaterial({ uniforms: { baseTexture: { value: null }, bloomTexture: { value: bloomComposer.renderTarget2.texture } }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); } `, fragmentShader: ` uniform sampler2D baseTexture; uniform sampler2D bloomTexture; varying vec2 vUv; void main() { gl_FragColor = ( texture2D( baseTexture, vUv ) + vec4( 1.0 ) * texture2D( bloomTexture, vUv ) ); } `, defines: {} }), 'baseTexture');mixPass.needsSwap = true; // 合成器输出通道const outputPass = new OutputPass(); const finalComposer = new EffectComposer(renderer);finalComposer.addPass(renderScene);finalComposer.addPass(mixPass);finalComposer.addPass(outputPass); function darkenNonBloomed(obj: any) { if (bloomLayer) { if (!obj.userData.isLight && bloomLayer.test(obj.layers) === false) { materials[obj.uuid] = obj.material; obj.material = darkMaterial; } } } function restoreMaterial(obj: any) { if (materials[obj.uuid]) { obj.material = materials[obj.uuid]; // 用于删除没必要的渲染 delete materials[obj.uuid]; }} const render = () = { if (guiParams.isLight) { if (bloomComposer) { scene.traverse(darkenNonBloomed); bloomComposer.render(); } if (finalComposer) { scene.traverse(restoreMaterial); finalComposer.render(); } }} export { render as unrealRender} 这段代码可以直接复制引用,并在render中调用unrealRender 方法,如果需要场景中某个物体发光,设置mesh.userData.isLight = true,即可,下面是详细讲解 定义发光效果相关参数和图层设置const createParams = { threshold: 0, strength: 0.39, // 强度 radius: 0.1,// 半径 exposure: 0.5 // 扩散}; const materials: any = {} const bloomLayer = new THREE.Layers();const BLOOM_SCENE = 1;bloomLayer.set(BLOOM_SCENE);定义发光效果参数创建了一个名为createParams的对象,其中包含了用于控制虚幻发光效果的几个参数,如threshold(发光阈值,决定哪些部分开始发光)、strength(发光强度)、radius(发光半径,影响发光范围)、exposure(扩散程度,可能与发光的整体扩散效果有关)。 创建并设置图层创建了一个THREE.Layers对象bloomLayer,并设置了一个图层标识BLOOM_SCENE为1,通过bloomLayer.set(BLOOM_SCENE)将该图层标记为与发光效果相关的特定图层。这个图层将在后续用于区分场景中的物体是否应该产生发光效果。 创建渲染通道和设置发光效果通道参数// 渲染器通道,将场景全部加入渲染器const renderScene = new RenderPass(scene, camera);// 添加虚幻发光通道const bloomPass = new UnrealBloomPass(new THREE.Vector2(width, height), 1.5, 0.4, 0.85);bloomPass.threshold = createParams.threshold;bloomPass.strength = createParams.strength;bloomPass.radius = createParams.radius;创建渲染场景通道创建了一个RenderPass对象renderScene,它的作用是将整个场景(由传入的scene和camera确定)渲染到某个目标(通常是后续合成器中的一个中间目标),这是整个渲染流程的基础步骤,确保场景能够被正确渲染。 创建并设置虚幻发光通道创建了一个UnrealBloomPass对象bloomPass,用于实现虚幻发光效果。在创建时传入了一些参数,如根据场景的宽度和高度创建的THREE.Vector2(width, height),以及一些默认发光效果相关的参数。然后,将之前定义的createParams对象中的相关参数(threshold、strength、radius)分别设置给bloomPass,以定制化发光效果。 创建合成器并添加渲染通道// 创建合成器const bloomComposer = new EffectComposer(renderer);bloomComposer.renderToScreen = false;// 将渲染器和场景结合到合成器中bloomComposer.addPass(renderScene);bloomComposer.addPass(bloomPass);创建发光效果合成器创建了一个EffectComposer对象bloomComposer,并将渲染器(renderer)作为参数传入,它的作用是将多个渲染通道组合在一起,实现更复杂的渲染效果。通过设置bloomComposer.renderToScreen = false,表示这个合成器的输出不会直接显示到屏幕上,而是作为后续进一步处理的中间结果。 添加渲染通道到合成器将之前创建的renderScene和bloomPass两个渲染通道添加到bloomComposer中,这样在后续渲染时,会先通过renderScene将场景渲染到中间目标,然后再通过bloomPass在这个基础上添加虚幻发光效果。 创建着色器通道并设置相关参数// 着色器通道const mixPass = new ShaderPass( // 着色器 new THREE.ShaderMaterial({ ...... }), 'baseTexture');mixPass.needsSwap = true;创建着色器通道对象创建了一个ShaderPass对象mixPass,它用于应用一个自定义的着色器来处理渲染结果。 设置着色器相关参数在创建ShaderPass时,传入了一个THREE.ShaderMaterial对象作为着色器的具体实现。这个着色器材料有以下特点: 定义 uniform 变量在uniforms部分,定义了两个uniform变量,baseTexture和bloomTexture(值为bloomComposer.renderTarget2.texture,即之前创建的bloomComposer合成器的第二个渲染目标的纹理,这个纹理包含了经过发光效果处理后的内容)。 顶点着色器(vertexShader)主要进行了一些常规的坐标变换操作,将顶点的uv坐标赋值给vUv变量,并通过modelViewMatrix和projectionMatrix对顶点位置进行变换,以确定在屏幕上的最终位置。 片段着色器(fragmentShader)在这个着色器中,通过texture2D函数分别获取baseTexture和bloomTexture在当前vUv坐标下的纹理颜色值,并将它们相加(其中bloomTexture的值还乘以了vec4(1.0),可能是为了调整发光效果在最终颜色中的权重),得到最终的片段颜色gl_FragColor。 通过设置mixPass.needsSwap = true,表示在渲染过程中需要进行纹理交换操作 创建合成器输出通道并构建最终合成器// 合成器输出通道const outputPass = new OutputPass(); const finalComposer = new EffectComposer(renderer);finalComposer.addPass(renderScene);finalComposer.addPass(mixPass);finalComposer.addPass(outputPass);创建输出通道创建了一个OutputPass对象outputPass,它的作用是将最终的渲染结果输出到屏幕上。 构建最终合成器创建了一个新的EffectComposer对象finalComposer,同样将渲染器(renderer)作为参数传入。然后将之前创建的renderScene、mixPass和outputPass三个渲染通道添加到finalComposer中,这样在渲染时,会先通过renderScene将场景渲染到中间目标,然后通过mixPass应用着色器处理,最后通过outputPass将最终结果输出到屏幕上,实现了整个复杂的渲染流程,包括场景渲染、发光效果处理以及最终的颜色混合等操作。 定义处理非发光物体材质的函数function darkenNonBloomed(obj: any) { if (bloomLayer) { if (!obj.userData.isLight && bloomLayer.test(obj.layers) === false) { materials[obj.uuid] = obj.material; obj.material = darkMaterial; } } } function restoreMaterial(obj: any) { if (materials[obj.uuid]) { obj.material = materials[obj.uuid]; // 用于删除没必要的渲染 delete materials[obj.uuid]; }}这个函数用于处理场景中那些不应该产生发光效果的物体材质。它首先检查bloomLayer是否存在,然后判断物体(由传入的obj表示)是否满足两个条件:一是物体的userData.isLight属性为false(可能表示物体本身不是光源相关的物体),二是通过bloomLayer.test(obj.layers)判断物体所在的图层不属于之前设置的发光相关图层。如果满足这两个条件,就将物体当前的材质保存到materials数组中(以物体的uuid作为索引),然后将物体的材质替换为darkMaterial,这样就实现了将非发光物体的材质变暗的效果。 定义渲染函数并控制渲染流程这个函数用于恢复之前被替换材质的物体的原始材质。它首先检查materials数组中是否存在以物体的uuid为索引的元素,如果存在,就将物体的材质设置回原来的材质,并删除materials数组中对应的元素,以避免不必要的内存占用和后续可能出现的问题。 const render = () = { if (guiParams.isLight) { if (bloomComposer) { scene.traverse(darkenNonBloomed); bloomComposer.render(); } if (finalComposer) { scene.traverse(restoreMaterial); finalComposer.render(); } }}定义了一个名为render的函数,它用于控制整个渲染流程。首先判断guiParams.isLight的值,如果为true,则进行以下操作: 如果bloomComposer存在,就通过scene.traverse(darkenNonBloomed)遍历整个场景中的物体,并调用darkenNonBloomed函数对每个物体进行材质处理,将非发光物体的材质变暗。然后调用bloomComposer.render()进行发光效果的渲染,即在经过材质处理后的场景基础上添加虚幻发光效果。 如果finalComposer存在,就通过scene.traverse(restoreMaterial)遍历整个场景中的物体,并调用restoreMaterial函数对之前被替换材质的物体进行恢复操作。然后调用finalComposer.render()进行最终的渲染,将经过发光效果处理、材质恢复等操作后的场景最终输出到屏幕上。 render中渲染导出的方法unrealRender 可直接在render函数中调用 renderer.setAnimationLoop(animate); function animate() { renderer.render(scene, camera); TWEEN.update(); intervalTime.update() controls.update(); // 更新控制器 // 更新虚幻系统 unrealRender() ...} 效果图14 发光效果关闭开启.gif通过点击GUI控制器中的是否发光选项来观察虚幻引擎对场景和物体的影响 可算看完了,感觉还可以不?欢迎提意见,兄弟们,撤! 点击关注公众号,“技术干货”及时达! 阅读原文
| 上一篇:2024-07-04_关键点检测标注文件解析(姿态估计)——COCO数据集 | 下一篇:2025-07-27_共生伙伴:2025人工智能十大趋势|2025 WAIC报告重磅发布(附下载) |
TAG标签: |
23 |
|
我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!
|
|
不达标就退款 高性价比建站 免费网站代备案 1对1原创设计服务 7×24小时售后支持 |
|
|
