threejs 判断点击的位置是否在点云中

编程入门 行业动态 更新时间:2024-10-23 01:29:56

threejs 判断点击的位置是否在点<a href=https://www.elefans.com/category/jswz/34/1754491.html style=云中"/>

threejs 判断点击的位置是否在点云中

我的点云文件格式是ply,需求是实现点云的测量,标注两个点之后连起来,计算他们的距离;

展示点云

首先我们需要明白 展示点云 必须要创建场景,相机,渲染器
参考代码 vue-3d-model
vue-3d-model是支持3d预览的一个插件 但是这个插件并不能满足我们的需求 所以我们就自己写了一个

<template><divref="plyContainer"style="position: relative; width: 100%; height: 100%; margin: 0; border: 0; padding: 0"><canvasref="canvasRef"style="width: 100% !important; height: 100% !important"/></div>
</template>
<script setup lang="ts">
/* eslint-disable */
import {Object3D,Vector2,Vector3,Color,Scene,Group,Light,Raycaster,WebGLRenderer,PerspectiveCamera,AmbientLight,PointLight,HemisphereLight,DirectionalLight,LinearEncoding,WebGLRendererParameters,Float32BufferAttribute,PointsMaterial,Points,TextureEncoding,ColorRepresentation
} from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { getSize, getCenter } from './util';
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader';
import { ElLoading } from 'element-plus';const DEFAULT_GL_OPTIONS = {antialias: true,alpha: true
};type EmitType = {(e: 'mousedown', event: MouseEvent, intersected: any): void;(e: 'mousemove', event: MouseEvent, intersected: any): void;(e: 'mouseup', event: MouseEvent, intersected: any): void;(e: 'click', event: MouseEvent, intersected: any): void;(e: 'progress', progressEvent: ProgressEvent): void;(e: 'error', errEvent: ErrorEvent): void;(e: 'load'): void;(e: 'loaded'): void;(e: 'addObject'): void;
};
const emit = defineEmits<EmitType>();//声明父组件传过来的数据以及类型
interface ModelPlyParams {src: string;width?: number;height?: number;position?: Record<string, any>;rotation?: Record<string, any>;scale?: Record<string, any>;lights?: number[];cameraPosition?: Record<string, any>;cameraRotation?: Record<string, any>;cameraUp?: Record<string, any>;cameraLookAt?: Record<string, any>;backgroundColor?: string;backgroundAlpha?: number;controlsOptions?: Record<string, any>;crossOrigin?: string;requestHeader?: Record<string, any>;outputEncoding?: number;glOptions?: Record<string, any>;
}
//声明默认值的写法
const props = withDefaults(defineProps<ModelPlyParams>(), {position: () => {return { x: 0, y: 0, z: 0 };},rotation: () => {return { x: 0, y: Math.PI, z: 0 };},scale: () => {return { x: 1, y: 1, z: 1 };},lights: () => {return [];},cameraPosition: () => {return { x: 0, y: 0, z: 0 };},cameraRotation: () => {return { x: 1, y: 1, z: 1 };},backgroundColor: 'black',backgroundAlpha: 1,crossOrigin: 'anonymous',requestHeader: () => {return {};},outputEncoding: LinearEncoding
});let object: Object3D | null = null;
let raycaster = new Raycaster();
let mouse = new Vector2();
let camera = new PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 100000); // 透视投影相机 PerspectiveCamera( fov, aspect, near, far )
let scene = new Scene();
let group = new Group(); // 三维对象 Object3D的实例都有一个矩阵matrix来保存该对象的position、rotation以及scale
let renderer: null | WebGLRenderer = null;
let controls: null | OrbitControls = null;
let allLights: Light[] = [];
let clock = typeof performance === 'undefined' ? Date : performance;
let reqId: null | number = null; // requestAnimationFrame id,
let loader = new PLYLoader(); // 会被具体实现的组件覆盖
let cloudGeometry: any = null;
let loading: any;let size = {width: props.width,height: props.height
};
const plyContainer = ref<InstanceType<typeof HTMLDivElement>>();
const canvasRef = ref<InstanceType<typeof HTMLCanvasElement>>();// Object.assign(this, result);
onMounted(() => {if (props.width === undefined || props.height === undefined) {size = {width: plyContainer.value?.offsetWidth,height: plyContainer.value?.offsetHeight};}const options: WebGLRendererParameters = Object.assign({}, DEFAULT_GL_OPTIONS, props.glOptions, {canvas: canvasRef.value});renderer = new WebGLRenderer(options);renderer.shadowMap.enabled = true;renderer.outputEncoding = props.outputEncoding as TextureEncoding;controls = new OrbitControls(camera, plyContainer.value);// this.controls.type = 'orbit';controls.maxDistance = 10000; // 设置最远位置 也就是缩小的最小程度controls.rotateSpeed = 3.0;controls.zoomSpeed = 1.2;controls.panSpeed = 0.8;// this.controls.keys = ['KeyA', 'KeyS', 'KeyD'];scene.add(group);load();update();const element = plyContainer.value as HTMLDivElement;element.addEventListener('mousedown', onMouseDown, false);element.addEventListener('mousemove', onMouseMove, false);element.addEventListener('mouseup', onMouseUp, false);element.addEventListener('click', onClick, false);window.addEventListener('resize', onResize, false);animate();
});onBeforeUnmount(() => {cancelAnimationFrame(reqId!);renderer!.dispose();if (controls) {controls.dispose();}const element = plyContainer.value as HTMLDivElement;element.removeEventListener('mousedown', onMouseDown, false);element.removeEventListener('mousemove', onMouseMove, false);element.removeEventListener('mouseup', onMouseUp, false);element.removeEventListener('click', onClick, false);window.removeEventListener('resize', onResize, false);
});const onResize = () => {if (props.width === undefined || props.height === undefined) {nextTick(() => {size = {width: plyContainer.value?.offsetWidth,height: plyContainer.value?.offsetHeight};});}
};const onMouseDown = (event: MouseEvent) => {emit('mousedown', event, pick(event.clientX, event.clientY));
};const onMouseMove = (event: MouseEvent) => {emit('mousemove', event, pick(event.clientX, event.clientY));
};const onMouseUp = (event: MouseEvent) => {emit('mouseup', event, pick(event.clientX, event.clientY));
};const onClick = (event: MouseEvent) => {emit('click', event, pick(event.clientX, event.clientY));
};const pick = (x: number, y: number) => {// 计算鼠标点击位置的标准化设备坐标  屏幕坐标转换为标准化设备坐标if (!object) return null;if (!plyContainer.value) return;const rect = plyContainer.value?.getBoundingClientRect();x -= rect.left;y -= rect.top;mouse.x = (x / size.width!) * 2 - 1;mouse.y = -(y / size.height!) * 2 + 1;// 通过射线投射来获取鼠标点击位置的真实坐标 可以使用Raycaster来检测在点击位置处是否存在可交互的点云。raycaster.setFromCamera(mouse, camera);const intersects = raycaster.intersectObject(object, true);return (intersects && intersects.length) > 0 ? intersects[0] : null;
};const update = () => {updateRenderer();updateCamera();updateLights();updateControls();
};const updateModel = () => {if (!object) return;const { position } = props;const { rotation } = props;const { scale } = props;object.position.set(position.x, position.y, position.z);object.rotation.set(rotation.x, rotation.y, rotation.z);object.scale.set(scale.x, scale.y, scale.z);
};const updateRenderer = () => {if (!renderer) {return;}renderer!.setSize(size.width!, size.height!);renderer!.setPixelRatio(window.devicePixelRatio || 1);renderer!.setClearColor(new Color(props.backgroundColor as ColorRepresentation).getHex());renderer!.setClearAlpha(props.backgroundAlpha);
};const updateCamera = () => {camera.aspect = size.width! / size.height!;// 更新场景的渲染camera.updateProjectionMatrix();if (!props.cameraLookAt || !props.cameraUp) {if (!object) return;const distance = getSize(object).length();camera.position.set(props.cameraPosition.x, props.cameraPosition.y, props.cameraPosition.z);camera.rotation.set(props.cameraRotation.x, props.cameraRotation.y, props.cameraRotation.z);if (props.cameraPosition.x === 0 && props.cameraPosition.y === 0 && props.cameraPosition.z === 0) {camera.position.z = distance;}// 相机朝向 相机不晓得自己要朝着物体看,他只知道直勾勾的往前看,所以要加上一句 camera.lookAt(); 让相机看向原点 它的参数是一个点camera.lookAt(new Vector3());} else {camera.position.set(props.cameraPosition.x, props.cameraPosition.y, props.cameraPosition.z);camera.rotation.set(props.cameraRotation.x, props.cameraRotation.y, props.cameraRotation.z);camera.up.set(props.cameraUp.x, props.cameraUp.y, props.cameraUp.z);camera.lookAt(new Vector3(props.cameraLookAt.x, props.cameraLookAt.y, props.cameraLookAt.z));}
};const updateLights = () => {scene.remove(...allLights);allLights = [];props.lights.forEach((item: any) => {if (!item || !item.type) return;const type = item.type.toLowerCase();let light: null | Light = null;if (type === 'ambient' || type === 'ambientlight') {const color = item.color === 0x000000 ? item.color : item.color || 0x404040;const intensity = item.intensity === 0 ? item.intensity : item.intensity || 1;// 环境光(AmbientLight)  笼罩在整个空间无处不在的光,不能产生阴影light = new AmbientLight(color, intensity);} else if (type === 'point' || type === 'pointlight') {const color = item.color === 0x000000 ? item.color : item.color || 0xffffff;const intensity = item.intensity === 0 ? item.intensity : item.intensity || 1;const distance = item.distance || 0;const decay = item.decay === 0 ? item.decay : item.decay || 1;// 点光源(PointLight ) 向四面八方发射的单点光源,不能产生阴影light = new PointLight(color, intensity, distance, decay);if (item.position) {light.position.copy(item.position);}} else if (type === 'directional' || type === 'directionallight') {const color = item.color === 0x000000 ? item.color : item.color || 0xffffff;const intensity = item.intensity === 0 ? item.intensity : item.intensity || 1;// 平行光(DirectinalLight) 平行光,类似太阳光,距离很远的光,会产生阴影light = new DirectionalLight(color, intensity);if (item.position) {light.position.copy(item.position);}if (item.target) {(light as DirectionalLight).target.copy(item.target);}} else if (type === 'hemisphere' || type === 'hemispherelight') {const skyColor = item.skyColor === 0x000000 ? item.skyColor : item.skyColor || 0xffffff;const groundColor = item.groundColor === 0x000000 ? item.groundColor : item.groundColor || 0xffffff;const intensity = item.intensity === 0 ? item.intensity : item.intensity || 1;light = new HemisphereLight(skyColor, groundColor, intensity);if (item.position) {light.position.copy(item.position);}}if (light) {allLights.push(light);scene.add(light);}});
};const updateControls = () => {if (props.controlsOptions) {Object.assign(controls!, props.controlsOptions);}
};const load = () => {if (!props.src) return;if (object) {group.remove(object);}loading = ElLoading.service({target: plyContainer.value,lock: true,background: 'rgba(0, 0, 0, 0.05)'});loader.setRequestHeader(props.requestHeader);(loader as any).load(props.src,(...args: any) => {getObject(args[0]);emit('load');},(event: ProgressEvent) => {emit('progress', event);onProgress(event);},(event: ErrorEvent) => {emit('error', event);});
};const getObject = (geometry: any) => {const colorArray: number[] = [];const positionArray = geometry.attributes.position.array;for (let i = 0; i < positionArray.length / 3; i++) {colorArray.push(1, 1, 1);}// this.positionArray = positionArray;geometry.setAttribute('position', new Float32BufferAttribute(positionArray, 3));geometry.setAttribute('color', new Float32BufferAttribute(colorArray, 3));geometry.center();cloudGeometry = geometry;geometryputeBoundingSphere();const cloudObject = new Points(geometry, new PointsMaterial()); // 是否使用顶点着色 使颜色显示的关键 { vertexColors: VertexColors }addObject(cloudObject);
};const addObject = (cloudObject: any) => {const center = getCenter(cloudObject);// correction positiongroup.position.copy(center.negate());object = cloudObject;group.add(cloudObject);// 区域测量 保证group中的第一项是点云emit('addObject');updateCamera();updateModel();
};const getGroupObject = () => {return object;
};const removeObject = () => {if (object) {group.remove(object);}
};const onProgress = (e: ProgressEvent) => {if (e.loaded / e.total === 1) {loading!.close();emit('loaded');}
};const animate = () => {reqId = requestAnimationFrame(animate);render();controls!.update();
};const render = () => {renderer!.render(scene, camera);
};const getGroup = () => group;// 清除出了点云图外的所有
const removeDrawerObject = () => {while (group.children.length > 1) {group.remove(group.children[1]);}
};watch(() => props.src,() => {load();}
);watch(() => props.rotation,(newValue: any) => {if (!object) return;object.rotation.set(newValue.x, newValue.y, newValue.z);},{ deep: true }
);watch(() => props.position,(newValue: any) => {if (!object) return;object.position.set(newValue.x, newValue.y, newValue.z);},{ deep: true }
);watch(() => props.scale,(newValue: any) => {if (!object) return;object.scale.set(newValue.x, newValue.y, newValue.z);},{ deep: true }
);watch(() => props.lights,() => {updateLights();},{ deep: true }
);watch(() => size,() => {updateCamera();updateRenderer();},{ deep: true }
);watch(() => props.controlsOptions,() => {updateControls();},{ deep: true }
);watch(() => props.backgroundAlpha,() => {updateRenderer();}
);watch(() => props.backgroundColor,() => {updateRenderer();}
);defineExpose({getGroup,getGroupObject,removeObject,addObject,onResize,updateCamera,updateRenderer,removeDrawerObject
});
</script>

按照上面的代码,我们是吧点云添加到group中,然后在添加到场景中显示;那么测量的思路就是添加两个点,一个直线;

那么我们需要思考:怎么看鼠标点击的坐标是否在点云中:

  1. 我们需要监听鼠标点击事件,获取点击的屏幕坐标
const onMouseDown = (event: MouseEvent) => {emit('mousedown', event, pick(event.clientX, event.clientY));
};
  1. 使用Three.js的射线投射功能,将屏幕坐标转换为Three.js场景中的三维坐标,他会返回一个相交数组,包含射线与点云中的物体相交的点
// 创建射线
let raycaster = new Raycaster();const pick = (x: number, y: number) => {// 计算鼠标点击位置的标准化设备坐标  屏幕坐标转换为标准化设备坐标if (!object) return null;if (!plyContainer.value) return;const rect = plyContainer.value?.getBoundingClientRect();x -= rect.left; // 鼠标事件在页面中的横坐标位置y -= rect.top; // size.width 点云显示的区域大小 (x / size.width!)鼠标事件位置相对于窗口宽度的比例 然后,通过将该比例值乘以2,并减去1,可以将其转换为位于[-1, 1]范围的归一化坐标值。对于水平方向,窗口的左边界对应-1,右边界对应1。mouse.x = (x / size.width!) * 2 - 1;mouse.y = -(y / size.height!) * 2 + 1;// mouse 是一个包含鼠标位置信息的 THREE.Vector2 对象。在这个上下文中,我们使用鼠标的归一化坐标值来表示其位置,这些归一化坐标值已经通过上述的转换过程计算得到。// camera 是用来定义射线起点与方向的相机对象。通过传入相机,raycaster 将使用相机的位置和方向来计算射线。// raycaster 设置的射线起点和方向:将使用相机的属性来确定射线起点,使用鼠标位置信息来确定射线的方向  raycaster.setFromCamera(mouse, camera);// 传入要检测的对象作为参数// 可以检测射线与指定对象之间的相交情况,并获取相交结果的数组const intersects = raycaster.intersectObject(object, true);return (intersects && intersects.length) > 0 ? intersects[0] : null;
};

至此 我们就可以把点添加到场景中了
添加点的关键代码
modelRef.value就是我的点云组件;getGroup就是组件中的group;

const addPoint = (point: any) => {// 创建点的几何体const positions = new Float32Array([point.x, point.y, point.z]); // 添加点的坐标const pointGeometry = new BufferGeometry();pointGeometry.setAttribute('position', new BufferAttribute(positions, 3));// 创建点的材质const material = new PointsMaterial({ color: 0xff0000, size: 10 });// 创建点的对象const pointObject = new Points(pointGeometry, material);// 将点的对象添加到场景中modelRef.value?.getGroup().add(pointObject);
};

添加线的关键代码
clickPoints就是我记录下的点击的点云的点坐标

const addLine = () => {// eslint-disable-next-line spellcheck/spell-checker// 请注意,线条材质的linewidth属性只在部分渲染器中有效,并且在某些浏览器中可能无效或显示为1像素。这是由于底层WebGL规范的限制造成的。// 在大多数情况下,线条的粗细将受限于渲染器和浏览器的支持程度。const material = new LineBasicMaterial({ color: 0xff0000 });const lineGeometry = new BufferGeometry().setFromPoints(clickPoints.value);const line = new Line(lineGeometry, material);modelRef.value?.getGroup().add(line);
};

更多推荐

threejs 判断点击的位置是否在点云中

本文发布于:2023-11-15 23:24:26,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1609287.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:云中   位置   threejs

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!