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

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

展示点云

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

<template>
  <div
    ref="plyContainer"
    style="position: relative; width: 100%; height: 100%; margin: 0; border: 0; padding: 0"
  >
    <canvas
      ref="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 { TrackballControls } from 'three/examples/jsm/controls/TrackballControls'; // 只用这个Controls 不然某一个轴旋转只能180度
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 | TrackballControls = 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 TrackballControls(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;
  geometry.computeBoundingSphere();
  const cloudObject = new Points(geometry, new PointsMaterial()); // 是否使用顶点着色 使颜色显示的关键 { vertexColors: VertexColors }
  addObject(cloudObject);
};

const addObject = (cloudObject: any) => {
  const center = getCenter(cloudObject);
  // correction position
  group.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);
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值