光线投射法
使用 three.JS 自带的光线投射器 (Raycaster) 选取物体非常简单, 代码如下所示:
- var raycaster = new THREE.Raycaster();
- var mouse = new THREE.Vector2();
- function onMouseMove(event) {
- // 计算鼠标所在位置的设备坐标
- // 三个坐标分量都是 - 1 到 1
- mouse.x = event.clientX / Windows.innerWidth * 2 - 1;
- mouse.y = - (event.clientY / Windows.innerHeight) * 2 + 1;
- }
- function pick() {
- // 使用相机和鼠标位置更新选取光线
- raycaster.setFromCamera(mouse, camera);
- // 计算与选取光线相交的物体
- var intersects = raycaster.intersectObjects(scene.children);
- }
它是采用包围盒过滤, 计算投射光线与每个三角面元是否相交实现的.
但是, 当模型非常大, 比如说有 40 万个面, 通过遍历的方法选取物体和计算碰撞点位置将非常慢, 用户体验不好.
但是使用 gpu 选取物体不存在这个问题. 无论场景和模型有多大, 都可以在一帧内获取到鼠标所在点的物体和交点的位置.
使用 GPU 选取物体
实现方法很简单:
1. 创建选取材质, 将场景中的每个模型的材质替换成不同的颜色.
2. 读取鼠标位置像素颜色, 根据颜色判断鼠标位置的物体.
具体实现代码:
1. 创建选取材质, 遍历场景, 将场景中每个模型替换为不同的颜色.
- let maxHexColor = 1;
- // 更换选取材质
- scene.traverseVisible(n => {
- if (!(n instanceof THREE.Mesh)) {
- return;
- }
- n.oldMaterial = n.material;
- if (n.pickMaterial) { // 已经创建过选取材质了
- n.material = n.pickMaterial;
- return;
- }
- let material = new THREE.ShaderMaterial({
- vertexShader: PickVertexShader,
- fragmentShader: PickFragmentShader,
- uniforms: {
- pickColor: {
- value: new THREE.Color(maxHexColor)
- }
- }
- });
- n.pickColor = maxHexColor;
- maxHexColor++;
- n.material = n.pickMaterial = material;
- });
2. 将场景绘制在 webGLRenderTarget 上, 读取鼠标所在位置的颜色, 判断选取的物体.
- let renderTarget = new THREE.WebGLRenderTarget(width, height);
- let pixel = new Uint8Array(4);
- // 绘制并读取像素
- renderer.setRenderTarget(renderTarget);
- renderer.clear();
- renderer.render(scene, camera);
- renderer.readRenderTargetPixels(renderTarget, offsetX, height - offsetY, 1, 1, pixel); // 读取鼠标所在位置颜色
- // 还原原来材质, 并获取选中物体
- const currentColor = pixel[0] * 0xffff + pixel[1] * 0xff + pixel[2];
- let selected = null;
- scene.traverseVisible(n => {
- if (!(n instanceof THREE.Mesh)) {
- return;
- }
- if (n.pickMaterial && n.pickColor === currentColor) { // 颜色相同
- selected = n; // 鼠标所在位置的物体
- }
- if (n.oldMaterial) {
- n.material = n.oldMaterial;
- delete n.oldMaterial;
- }
- });
说明: offsetX 和 offsetY 是鼠标位置, height 是画布高度. readRenderTargetPixels 一行的含义是选取鼠标所在位置(offsetX, height - offsetY), 宽度为 1, 高度为 1 的像素的颜色.
pixel 是 Uint8Array(4), 分别保存 rgba 颜色的四个通道, 每个通道取值范围是 0~255.
完整实现代码:
使用 GPU 获取交点位置
实现方法也很简单:
1. 创建深度着色器材质, 将场景深度渲染到 WebGLRenderTarget 上.
2. 计算鼠标所在位置的深度, 根据鼠标位置和深度计算交点位置.
具体实现代码:
1. 创建深度着色器材质, 将深度信息以一定的方式编码, 渲染到 WebGLRenderTarget 上.
深度材质:
- const depthMaterial = new THREE.ShaderMaterial({
- vertexShader: DepthVertexShader,
- fragmentShader: DepthFragmentShader,
- uniforms: {
- far: {
- value: camera.far
- }
- }
- });
- DepthVertexShader:
- precision highp float;
- uniform float far;
- varying float depth;
- void main() {
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
- depth = gl_Position.z / far;
- }
- DepthFragmentShader:
- precision highp float;
- varying float depth;
- void main() {
- float hex = abs(depth) * 16777215.0; // 0xffffff
- float r = floor(hex / 65535.0);
- float g = floor((hex - r * 65535.0) / 255.0);
- float b = floor(hex - r * 65535.0 - g * 255.0);
- float a = sign(depth)>= 0.0 ? 1.0 : 0.0; // depth 大于等于 0, 为 1.0; 小于 0, 为 0.0.
- gl_FragColor = vec4(r / 255.0, g / 255.0, b / 255.0, a);
- }
重要说明:
a. gl_Position.z 是相机空间中的深度, 是线性的, 范围从 cameraNear 到 cameraFar. 可以直接使用着色器 varying 变量进行插值.
b. gl_Position.z / far 的原因是, 将值转换到 0~1 范围内, 便于作为颜色输出.
c. 不能使用屏幕空间中的深度, 透视投影后, 深度变为 - 1~1, 大部分非常接近 1(0.9 多), 不是线性的, 几乎不变, 输出的颜色几乎不变, 非常不准确.
d. 在片元着色器中获取深度方法: 相机空间深度为 gl_FragCoord.z, 屏幕空间深度为 gl_FragCoord.z / gl_FragCoord.w.
e. 上述描述都是针对透视投影, 正投影中 gl_Position.w 为 1, 使用相机空间和屏幕空间深度都是一样的.
f. 为了尽可能准确输出深度, 采用 rgb 三个分量输出深度. gl_Position.z/far 范围在 0~1, 乘以 0xffffff, 转换为一个 rgb 颜色值, r 分量 1 表示 65535,g 分量 1 表示 255,b 分量 1 表示 1.
完整实现代码:
2. 读取鼠标所在位置的颜色, 将读取到的颜色值还原为相机空间深度值.
a. 将 "加密" 处理后的深度绘制在 WebGLRenderTarget 上. 读取颜色方法
- let renderTarget = new THREE.WebGLRenderTarget(width, height);
- let pixel = new Uint8Array(4);
- scene.overrideMaterial = this.depthMaterial;
- renderer.setRenderTarget(renderTarget);
- renderer.clear();
- renderer.render(scene, camera);
- renderer.readRenderTargetPixels(renderTarget, offsetX, height - offsetY, 1, 1, pixel);
说明: offsetX 和 offsetY 是鼠标位置, height 是画布高度. readRenderTargetPixels 一行的含义是选取鼠标所在位置(offsetX, height - offsetY), 宽度为 1, 高度为 1 的像素的颜色.
pixel 是 Uint8Array(4), 分别保存 rgba 颜色的四个通道, 每个通道取值范围是 0~255.
b. 将 "加密" 后的相机空间深度值 "解密", 得到正确的相机空间深度值.
- if (pixel[2] !== 0 || pixel[1] !== 0 || pixel[0] !== 0) {
- let hex = (this.pixel[0] * 65535 + this.pixel[1] * 255 + this.pixel[2]) / 0xffffff;
- if (this.pixel[3] === 0) {
- hex = -hex;
- }
- cameraDepth = -hex * camera.far; // 相机坐标系中鼠标所在点的深度(注意: 相机坐标系中的深度值为负值)
- }
3. 根据鼠标在屏幕上的位置和相机空间深度, 插值反算交点世界坐标系中的坐标.
- let nearPosition = new THREE.Vector3(); // 鼠标屏幕位置在 near 处的相机坐标系中的坐标
- let farPosition = new THREE.Vector3(); // 鼠标屏幕位置在 far 处的相机坐标系中的坐标
- let world = new THREE.Vector3(); // 通过插值计算世界坐标
- // 设备坐标
- const deviceX = this.offsetX / width * 2 - 1;
- const deviceY = - this.offsetY / height * 2 + 1;
- // 近点
- nearPosition.set(deviceX, deviceY, 1); // 屏幕坐标系:(0, 0, 1)
- nearPosition.applyMatrix4(camera.projectionMatrixInverse); // 相机坐标系:(0, 0, -far)
- // 远点
- farPosition.set(deviceX, deviceY, -1); // 屏幕坐标系:(0, 0, -1)
- farPosition.applyMatrix4(camera.projectionMatrixInverse); // 相机坐标系:(0, 0, -near)
- // 在相机空间, 根据深度, 按比例计算出相机空间 x 和 y 值.
- const t = (cameraDepth - nearPosition.z) / (farPosition.z - nearPosition.z);
- // 将交点从相机空间中的坐标, 转换到世界坐标系坐标.
- world.set(
- nearPosition.x + (farPosition.x - nearPosition.x) * t,
- nearPosition.y + (farPosition.y - nearPosition.y) * t,
- cameraDepth
- );
- world.applyMatrix4(camera.matrixWorld);
完整代码:
相关应用
使用 gpu 选取物体并计算交点位置, 多用于需要性能非常高的情况. 例如:
1. 鼠标移动到三维模型上的 hover 效果.
2. 添加模型时, 模型随着鼠标移动, 实时预览模型放到场景中的效果.
3. 距离测量, 面积测量等工具, 线条和多边形随着鼠标在平面上移动, 实时预览效果, 并计算长度和面积.
4. 场景和模型非常大, 光线投射法选取速度很慢, 用户体验非常不好.
这里给一个使用 gpu 选取物体和实现鼠标 hover 效果的图片. 红色边框是选取效果, 黄色半透明效果是鼠标 hover 效果.
看不明白? 可能你不太熟悉 three.JS 中的各种投影运算. 下面给出 three.JS 中的投影运算公式.
three.JS 中的投影运算
- 1. modelViewMatrix = camera.matrixWorldInverse * object.matrixWorld
- 2. viewMatrix = camera.matrixWorldInverse
- 3. modelMatrix = object.matrixWorld
- 4. project = applyMatrix4( camera.matrixWorldInverse ).applyMatrix4( camera.projectionMatrix )
- 5. unproject = applyMatrix4( camera.projectionMatrixInverse ).applyMatrix4( camera.matrixWorld )
- 6. gl_Position = projectionMatrix * modelViewMatrix * position
- = projectionMatrix * camera.matrixWorldInverse * matrixWorld * position
- = projectionMatrix * viewMatrix * modelMatrix * position
参考资料:
1. 完整实现代码:
2. OpenGL 中使用着色器绘制深度值: https://stackoverflow.com/questions/6408851/draw-the-depth-value-in-opengl-using-shaders
3. 在 glsl 中, 获取真实的片元着色器深度值: https://gamedev.stackexchange.com/questions/93055/getting-the-real-fragment-depth-in-glsl
来源: https://www.cnblogs.com/tengge/p/11924663.html