前言
最近研究了一个风力发电机监控平台的GitHub项目,然后找了使用three.js的场景功能,将关键代码和页面效果进行展示并说明。一个是实现物体的动态显示与隐藏,一个是实现物体点击交互事件,可以作为参考。
物体动态显隐
官网示例:https://threejs.org/examples/#webgl_clipping_intersection
gitHub项目实例
效果:
功能代码:
// 定义一个Map来存储渲染混合器,这些混合器可以处理场景中的动画效果
const renderMixins = new Map()
/**
* 执行地面和风机骨架的隐藏动画
* 该函数通过修改 clippingPlanes 属性来实现模型的隐藏效果
*/
const groundAndSkeletonHideAnimation = () => {
// 创建一个裁剪平面,用于隐藏模型
const clippingPlane = new THREE.Plane(new THREE.Vector3(0, -1, 0), 3.5)
// 遍历风机骨架模型,设置每个网格的裁剪平面
modelSkeleton.value?.traverse((mesh) => {
// 进行类型检查,避免对非网格对象(如光源、相机或其他对象)进行不适用的操作,导致错误
if (!(mesh instanceof THREE.Mesh)) return undefined
mesh.material.clippingPlanes = [clippingPlane]
return undefined
})
// 遍历地面模型,设置每个网格的裁剪平面
modelPlane.value?.traverse((mesh) => {
if (!(mesh instanceof THREE.Mesh)) return undefined
mesh.material.clippingPlanes = [clippingPlane]
return undefined
})
// 生成一个唯一的标识符,用于跟踪渲染过程
const uid = uuid()
// 注册一个渲染回调,逐步修改裁剪平面的位置,实现动画效果
renderMixins.set(uid, () => {
// 如果裁剪平面的位置达到一定阈值,则移除回调,结束动画
if (clippingPlane.constant <= -0.1) renderMixins.delete(uid)
// 每帧降低裁剪平面的位置,实现模型逐渐隐藏的效果
clippingPlane.constant -= 0.04
})
}
/**
* 地面和风机骨架显示动画
* 该函数通过调整裁剪平面的位置来实现地面和风机骨架的显示动画效果
* 使用THREE.js库中的Plane对象创建一个裁剪平面,并将其应用到模型的骨架和地面网格上
* 随着时间的推移,逐渐调整裁剪平面的位置,以达到动画效果
*/
const groundAndSkeletonShowAnimation = () => {
// 创建一个裁剪平面,初始位置略低于模型底部
const clippingPlane = new THREE.Plane(new THREE.Vector3(0, -1, 0), -0.1)
// 遍历骨架模型,将裁剪平面应用到所有的Mesh对象上
modelSkeleton.value?.traverse((mesh) => {
if (!(mesh instanceof THREE.Mesh)) return undefined
mesh.material.clippingPlanes = [clippingPlane]
return undefined
})
// 遍历地面模型,将裁剪平面应用到所有的Mesh对象上
modelPlane.value?.traverse((mesh) => {
if (!(mesh instanceof THREE.Mesh)) return undefined
mesh.material.clippingPlanes = [clippingPlane]
return undefined
})
// 生成一个唯一的标识符,用于后续的动画渲染
const uid = uuid()
// 注册一个渲染回调,用于在每一帧更新裁剪平面的位置
renderMixins.set(uid, () => {
// 当裁剪平面的位置达到一定高度时,移除回调,停止动画
if (clippingPlane.constant >= 3.5) renderMixins.delete(uid)
// 每一帧增加裁剪平面的高度,以达到逐渐显示模型的效果
clippingPlane.constant += 0.04
})
}
/**
* 渲染函数,用于持续更新和渲染三维场景
*/
const render = () => {
// 计算自上次渲染以来的时间差,用于动画更新
const delta = new THREE.Clock().getDelta()
// 使用Three.js渲染器对当前场景和相机进行渲染
renderer.value!.render(scene.value!, camera.value!)
// 计算mixer的更新时间差,用于动画混合
const mixerUpdateDelta = clock.getDelta()
// 遍历所有动画混合器,更新动画
mixers.forEach((mixer: any) => mixer.update(mixerUpdateDelta))
// 遍历所有后期处理特效组合,执行渲染
composers.forEach((composer) => composer.render(delta))
// 遍历所有自定义渲染混合函数,如果它们是函数则执行
renderMixins.forEach((mixin) => isFunction(mixin) && mixin())
// 使用CSS渲染器对当前场景和相机进行CSS相关的渲染
CSSRenderer.value!.render(scene.value!, camera.value!)
// 更新所有动画
TWEEN.update()
// 请求下一帧渲染
requestAnimationFrame(() => render())
}
练习实例
效果:
代码:
<script setup lang="ts">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
//导入tween
import * as TWEEN from "three/examples/jsm/libs/tween.module.js";
import { onMounted } from "vue";
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
1,
1000
);
scene.add(camera);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
// 定义渲染器是否考虑对象级剪切平面,需设置否则没办法进行剪切
renderer.localClippingEnabled = true;
document.body.appendChild(renderer.domElement);
camera.position.set(12, 12, 20);
const controls = new OrbitControls(camera, renderer.domElement);
// 监听屏幕变化,更新渲染画面
window.addEventListener("resize", () => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
});
const sphere = new THREE.SphereGeometry(3,32,32)
const material1 = new THREE.MeshStandardMaterial({
color: 0xfc9826,
metalness: 0.5, // 增加金属感
roughness: 0.5, // 适度的粗糙度
});
const material2 = new THREE.MeshStandardMaterial({color:0xfcecde})
const sphereMesh = new THREE.Mesh(sphere,material1)
sphereMesh.position.set(0,3,0)
scene.add(sphereMesh)
const planeGeometry = new THREE.PlaneGeometry( 25, 25 )
const plane = new THREE.Mesh(planeGeometry,material2)
plane.position.set(0,-2,0)
// 旋转使平面朝上
plane.rotateX(-Math.PI/2)
scene.add(plane)
// 添加光源以增强材质效果
const ambientLight = new THREE.AmbientLight(0x404040,10); // 环境光
scene.add(ambientLight);
const pointLight = new THREE.PointLight(0xffffff, 100, 100); // 点光源
pointLight.position.set(5, 7, 7);
scene.add(pointLight);
// 创建一个裁剪平面,用于隐藏模型
const clippingPlane = new THREE.Plane(new THREE.Vector3(0, -1, 0), 7)
sphereMesh.material.clippingPlanes = [clippingPlane]
let direction = -1; // 用于控制方向
const updateClippingPlane = () => {
// 更新剪切平面常量
clippingPlane.constant += direction * 0.06;
// 检查常量值并反转方向
if (clippingPlane.constant >= 7 || clippingPlane.constant <= -0.1) {
direction *= -1; // 反转方向
}
};
const animate = () => {
renderer.render(scene, camera);
requestAnimationFrame(animate);
controls.update();
TWEEN.update();
updateClippingPlane()
};
onMounted(()=>{
animate()
})
</script>
<template>
<div></div>
</template>
<style scoped></style>
物体点击交互
gitHub项目实例
效果:
功能代码:
// 风机设备点击事件处理函数
const onEquipmentClick = () => {
// 初始化设备列表数组
const equipmentList: any = []
// 遍历模型中的所有网格,寻找可交互的设备
modelEquipment.value?.traverse((mesh) => {
// 忽略不是THREE.Mesh实例的网格
if (!(mesh instanceof THREE.Mesh)) return undefined
// 复制网格的材质,以避免修改原始材质
const { material } = mesh
mesh.material = material.clone()
// 将网格添加到设备列表中
equipmentList.push(mesh)
return undefined
})
// 定义鼠标点击事件的处理函数
const handler = (event: MouseEvent) => {
// 获取容器元素
const el = container.value as HTMLElement
// 将鼠标坐标转换为标准化设备坐标
const mouse = new THREE.Vector2(
(event.clientX / el.offsetWidth) * 2 - 1,
-(event.clientY / el.offsetHeight) * 2 + 1
)
// 创建光线投射器,用于检测鼠标点击的物体
const raycaster = new THREE.Raycaster()
// 设置光线投射器的起点和方向
raycaster.setFromCamera(mouse, camera.value!) // 通过摄像机和鼠标位置更新射线
// 检测点击的设备
// 计算物体和射线的焦点
const intersects = raycaster.intersectObject(modelEquipment.value!, true)
// 如果没有检测到设备,则不执行任何操作
if (size(intersects) <= 0) return undefined
// console.log('intersects',intersects);
// 获取点击的第一个设备
const equipment = <any>intersects[0].object
// 如果设备无效,则不执行任何操作
if (!equipment) return undefined
// 恢复所有设备的材质颜色
equipmentList.forEach((child: any) => {
// emissive 属性代表材质的自发光颜色。这种颜色使得对象即使在没有光照的情况下也能发光
child.material.emissive.setHex(child.currentHex) // setHex(child.currentHex) 是调用 setHex 方法,将 currentHex 这个十六进制颜色值设置为材质的发光颜色
})
// 保存当前设备的材质颜色
equipment.currentHex =
equipment.currentHex ?? equipment.material.emissive.getHex()
// 将当前设备的材质颜色设置为红色,以突出显示
equipment.material.emissive.setHex(0xff0000)
// 返回 undefined,表示函数执行完毕
return undefined
}
// 添加全局鼠标点击事件监听器
document.addEventListener('click', handler)
// 在组件卸载时移除事件监听器
onUnmounted(() => document.removeEventListener('click', handler))
}
Three.js官方文档:光线投射Raycaster
练习实例
效果:
代码:
<script setup lang="ts">
import * as THREE from "three";
import { onMounted } from "vue";
// 定义全局变量
let camera: any, scene: any, raycaster: any, renderer: any;
let INTERSECTED: any;
let theta = 0;
const pointer = new THREE.Vector2();
const radius = 5;
// 初始化函数,用于设置场景、相机、光源等
function init() {
// 初始化相机
camera = new THREE.PerspectiveCamera(
70,
window.innerWidth / window.innerHeight,
0.1,
100
);
// 初始化场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);
// 初始化光源
const light = new THREE.DirectionalLight(0xffffff, 3);
light.position.set(1, 1, 1).normalize();
scene.add(light);
// 初始化几何体
const geometry = new THREE.BoxGeometry();
// 创建多个立方体并添加到场景中
for (let i = 0; i < 2000; i++) {
const object = new THREE.Mesh(
geometry,
new THREE.MeshLambertMaterial({ color: Math.random() * 0xffffff })
);
object.position.x = Math.random() * 40 - 20;
object.position.y = Math.random() * 40 - 20;
object.position.z = Math.random() * 40 - 20;
object.rotation.x = Math.random() * 2 * Math.PI;
object.rotation.y = Math.random() * 2 * Math.PI;
object.rotation.z = Math.random() * 2 * Math.PI;
object.scale.x = Math.random() + 0.5;
object.scale.y = Math.random() + 0.5;
object.scale.z = Math.random() + 0.5;
scene.add(object);
}
// 初始化射线投射器
raycaster = new THREE.Raycaster();
// 初始化渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
//通过调用 setAnimationLoop,我们确保了动画可以平滑地进行
//与传统的 requestAnimationFrame 不同,setAnimationLoop 会自动处理浏览器的刷新率,并优化性能,因此是推荐在使用 THREE.js 时进行动画循环的方法
renderer.setAnimationLoop(animate);
document.body.appendChild(renderer.domElement);
// 监听鼠标移动事件
document.addEventListener("mousemove", onPointerMove);
// 监听窗口大小变化事件
window.addEventListener("resize", onWindowResize);
}
// 窗口大小变化时的处理函数
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// 鼠标移动时的处理函数
function onPointerMove(event: any) {
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
// 动画循环函数
function animate() {
render();
}
// 渲染函数
// 更新相机位置和渲染场景
function render() {
// 增加角度以旋转相机位置
theta += 0.1;
// 根据角度计算相机的新位置
camera.position.x = radius * Math.sin(THREE.MathUtils.degToRad(theta));
camera.position.y = radius * Math.sin(THREE.MathUtils.degToRad(theta));
camera.position.z = radius * Math.cos(THREE.MathUtils.degToRad(theta));
// 让相机看向场景的中心
camera.lookAt(scene.position);
// 更新相机的世界矩阵
camera.updateMatrixWorld();
// 通过摄像机和鼠标位置更新射线
raycaster.setFromCamera(pointer, camera);
// 检查光线投射是否与场景中的任何对象相交
const intersects = raycaster.intersectObjects(scene.children, false);
// 如果有相交的对象
if (intersects.length > 0) {
// 如果当前相交的对象与之前的相交对象不同
if (INTERSECTED != intersects[0].object) {
// 如果之前有相交的对象,恢复其颜色
if (INTERSECTED)
INTERSECTED.material.emissive.setHex(INTERSECTED.currentHex);
// 更新当前相交的对象
INTERSECTED = intersects[0].object;
// 保存当前相交对象的原始颜色
INTERSECTED.currentHex = INTERSECTED.material.emissive.getHex();
// 将当前相交对象的颜色改为红色
INTERSECTED.material.emissive.setHex(0xff0000);
}
} else {
// 如果没有相交的对象,且之前有相交对象,恢复其颜色
if (INTERSECTED)
INTERSECTED.material.emissive.setHex(INTERSECTED.currentHex);
// 重置当前相交对象
INTERSECTED = null;
}
// 使用渲染器渲染场景
renderer.render(scene, camera);
}
onMounted(() => {
init();
});
</script>