three.js使用gpu选取物体并计算交点位置
光线投射法
使用three.js自带的光线投射器(Raycaster)选取物体非常简单,代码如下所示:
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2(); function onMouseMove(event) {
// 计算鼠标所在位置的设备坐标
// 三个坐标分量都是-1到1
mouse.x = event.clientX / window.innerWidth * 2 - 1;
mouse.y = - (event.clientY / window.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;
});
PickVertexShader:
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
PickFragmentShader:
uniform vec3 pickColor;
void main() {
gl_FragColor = vec4(pickColor, 1.0);
}
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。
完整实现代码:https://gitee.com/tengge1/ShadowEditor/blob/master/ShadowEditor.Web/src/event/GPUPickEvent.js
使用GPU获取交点位置
实现方法也很简单:
1. 创建深度着色器材质,将场景深度渲染到WebGLRenderTarget上。
2. 计算鼠标所在位置的深度,根据鼠标位置和深度计算交点位置。
具体实现代码:
1. 创建深度着色器材质,将深度信息以一定的方式编码,渲染到WebGLRenderTarget上。
深度材质:
const depthMaterial = new THREE.ShaderMaterial({
vertexShader: DepthVertexShader,
fragmentShader: DepthFragmentShader,
uniforms: {
far: {
value: camera.far
}
}
});
precision highp float;
uniform float far;
varying float depth;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
depth = gl_Position.z / far;
}
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多),不是线性的,几乎不变,输出的颜色几乎不变,非常不准确。
e. 上述描述都是针对透视投影,正投影中gl_Position.w为1,使用相机空间和屏幕空间深度都是一样的。
f. 为了尽可能准确输出深度,采用rgb三个分量输出深度。gl_Position.z/far范围在0~1,乘以0xffffff,转换为一个rgb颜色值,r分量1表示65535,g分量1表示255,b分量1表示1。
完整实现代码:https://gitee.com/tengge1/ShadowEditor/blob/master/ShadowEditor.Web/src/event/GPUPickEvent.js
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);
完整代码:https://gitee.com/tengge1/ShadowEditor/blob/master/ShadowEditor.Web/src/event/GPUPickEvent.js
相关应用
使用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. 完整实现代码:https://gitee.com/tengge1/ShadowEditor/blob/master/ShadowEditor.Web/src/event/GPUPickEvent.js
2. 基于three.js的开源三维场景编辑器:https://github.com/tengge1/ShadowEditor
3. OpenGL中使用着色器绘制深度值:https://stackoverflow.com/questions/6408851/draw-the-depth-value-in-opengl-using-shaders
4. 在glsl中,获取真实的片元着色器深度值:https://gamedev.stackexchange.com/questions/93055/getting-the-real-fragment-depth-in-glsl
three.js使用gpu选取物体并计算交点位置的更多相关文章
- 关于Unity中鼠标选取物体的解决方案
今天修改了之前写的飞机大战的代码,原来的不足之处是点击屏幕的任意一点都可以移动飞机,也就是没有检测鼠标到底有没有点到飞机上. 我先是用之前的3D拾取技术,发现没有反应,才意识到我这个plane飞机节点 ...
- 04-THREE.JS 添加、删除物体,批量操作
<!DOCTYPE html> <html> <head> <title></title> <script src="htt ...
- js实现随机选取[10,100)中的10个整数,存入一个数组,并排序。 另考虑(10,100]和[10,100]两种情况。
1.js实现随机选取[10,100)中的10个整数,存入一个数组,并排序. <!DOCTYPE html> <html lang="en"> <hea ...
- html--offsetLeft,Left,clientLeft的关键--动态获取计算元素位置关系
动态计算元素位置关系的时候,必备... http://www.cnblogs.com/panjun-Donet/articles/1294033.html
- JS控制div跳转到指定的位置的几种解决方案总结
原文:http://www.jb51.net/article/96574.htm 这篇文章主要介绍了JS控制div跳转到指定的位置的几种解决方案总结,小编觉得挺不错的,现在分享给大家,也给大家做个参考 ...
- 08-THREE.JS 点面创建物体,克隆物体,多材质物体
<!DOCTYPE html> <html> <head> <title></title> < <script src=&quo ...
- js中如何通过身份证号计算出生日期和年龄
在html中有如下标签 身份证号:<input type="text" id="Gra_IDCard" onChange="IDCardChan ...
- 为什么GPU可以用于科学计算【转载】
转自:https://blog.csdn.net/xihuanyuye/article/details/81178352 https://www.zhihu.com/question/35063258 ...
- Vue.js(2.x)之计算属性
昨天看完大神的文章后,深感惭愧,硬着头皮继续看官网文档,然而这真的没是没办法,介绍的实在有些敷衍: 1).计算属性:也不说下computed是计算属性关键词,vm实例是可以像代理data一样代理com ...
随机推荐
- markdown数学公式大全
目录 Cmd Markdown 公式指导手册 一.公式使用参考 1.如何插入公式 2.如何输入上下标 3.如何输入括号和分隔符 4.如何输入分数 5.如何输入开方 6.如何输入省略号 7.如何输入矢量 ...
- 嗜血的硅谷与Hygge的欧洲
听到太多抱怨工作累,加班多,996的声音.我们是否从底层来理智的分析这一过程.那些抱怨加班多.996的人有多少离开了那个让他发声的工作,去找一份轻松悠闲的工作度日? 我们是一边抱怨着工作,一边担心着离 ...
- IOT设备的7大安全问题
IOT设备的7大安全问题 串口安全 IOT设备一般包含各类串口,并且这些串口缺乏认证机制.一旦暴露给了hacker,hacker可以很容易的查找敏感信息和dump固件,从而导致各类安全问题.建议厂家在 ...
- The usage of Markdown---杂谈:缩进/换行/分割线/注释/文字颜色
目录 1. 序言 2. 缩进 3. 换行 4. 分割线 5. 注释 6. 改变文字大小和颜色 更新时间:2019.09.14 1. 序言 今天一口气更新了接近10篇博客(虽然只是将我之前基本码好的 ...
- Python装饰器基础
一.Python装饰器引入 讲 Python 装饰器前,我想先举个例子,虽有点污,但跟装饰器这个话题很贴切. 每个人都有的内裤主要功能是用来遮羞,但是到了冬天它没法为我们防风御寒,咋办?我们想到的一个 ...
- sqlmap日常使用
收集的一些技巧资源来之互联网 -u #注入点 -f #指纹判别数据库类型 -b #获取数据库版本信息 -p #指定可测试的参数(?page=1&id=2 -p "page,id&qu ...
- Python实战练习——打印日历教程
很长一段时间没有接触过C语言了,想来做这一行当已经有三两年了. 今天突然想起来以前用C语言在VC6上写代码的日子,想了想以前的一些实战练习. 所以今天打算用Python来写一个C语言以前练习的题目-日 ...
- MinIO 搭建
MinIO 搭建 MinIO 是一个基于 Apache License v2.0 开源协议的对象存储服务.它兼容亚马逊 S3 云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片.视频.日志文 ...
- 如何进行kubernetes问题的排障
排障的前置条件 k8s的成熟度很高,伴随着整个项目的扩增,以及新功能和新流程的不断引入,也伴随这产生了一些问题.虽然自动化测试可以排除掉大部分,但是一些复杂流程以及极端情况却很难做到bug的完全覆盖. ...
- [考试反思]1109csp-s模拟测试107:低能
诶一看这不是水题AK场吗?然后80分钟就拿到了285分. 然后,对拍?还是卡T2常数?还是想T2正解? 于是上述三项我依次进行了. 前两项让我的分数丝毫不变但是吃掉了我一个多小时的时间. 卡常卡的也不 ...