three.js利用gpu选取物体并计算交点位置的方法示例


Posted in Javascript onNovember 25, 2019

光线投射法

使用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;
});

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
 }
 }
});

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。

完整实现代码: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利用gpu选取物体并计算交点位置的方法示例

看不明白?可能你不太熟悉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. 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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
兼容ie、firefox的图片自动缩放的css跟js代码分享
Jan 21 Javascript
JQuery+Ajax无刷新分页的实例代码
Feb 08 Javascript
Validform+layer实现漂亮的表单验证特效
Jan 17 Javascript
[原创]Javascript 实现广告后加载 可加载百度谷歌联盟广告
May 11 Javascript
node.js cookie-parser之parser.js
Jun 06 Javascript
浅谈javascript运算符——条件,逗号,赋值,()和void运算符
Jul 15 Javascript
zTree树形菜单交互选项卡效果的实现方法
Dec 25 Javascript
Node.js爬取豆瓣数据实例分析
Mar 05 Javascript
小程序自定义日历效果
Dec 29 Javascript
js DOM的事件常见操作实例详解
Dec 16 Javascript
jquery实现商品sku多属性选择功能(商品详情页)
Dec 20 jQuery
微信小程序连接服务器展示MQTT数据信息的实现
Jul 14 Javascript
基于javascript实现贪吃蛇小游戏
Nov 25 #Javascript
JavaScript This指向问题详解
Nov 25 #Javascript
简单了解JavaScript sort方法
Nov 25 #Javascript
vue使用swiper实现中间大两边小的轮播图效果
Nov 24 #Javascript
通过GASP让vue实现动态效果实例代码详解
Nov 24 #Javascript
JS控制只能输入数字并且最多允许小数点两位
Nov 24 #Javascript
解决Vue.js应用回退或刷新界面时提示用户保存修改问题
Nov 24 #Javascript
You might like
4种PHP异步执行的常用方式
2015/12/24 PHP
ThinkPHP3.2.3框架实现执行原生SQL语句的方法示例
2019/04/03 PHP
解决laravel5.4下的group by报错的问题
2019/10/16 PHP
关于event.cancelBubble和event.stopPropagation()的区别介绍
2011/12/11 Javascript
JS 对输入框进行限制(常用的都有)
2013/07/30 Javascript
js 一个关于图片onload加载的事
2013/11/10 Javascript
javascript Array.prototype.slice的使用示例
2013/11/14 Javascript
JavaScript实现的背景自动变色代码
2015/10/17 Javascript
jquery获取form表单input元素值的简单实例
2016/05/30 Javascript
Js自定义多选框效果的实例代码
2017/07/05 Javascript
微信小程序“摇一摇”的实例代码
2017/07/20 Javascript
基于Vue2x实现响应式自适应轮播组件插件VueSliderShow功能
2018/05/16 Javascript
mpvue项目中使用第三方UI组件库的方法
2018/09/30 Javascript
AngularJs返回前一页面时刷新一次前面页面的方法
2018/10/09 Javascript
小程序指纹验证的实现代码
2018/12/04 Javascript
JavaScript创建对象的四种常用模式实例分析
2019/01/11 Javascript
vue基础之使用get、post、jsonp实现交互功能示例
2019/03/12 Javascript
详解vuex之store源码简单解析
2019/06/13 Javascript
使用vue实现各类弹出框组件
2019/07/03 Javascript
JavaScript函数重载操作实例浅析
2020/05/02 Javascript
wepy--用vantUI 实现上弹列表并选择相应的值操作
2020/11/03 Javascript
vue实现禁止浏览器记住密码功能的示例代码
2021/02/03 Vue.js
[02:51]2014DOTA2国际邀请赛 IG战队官方纪录片
2014/07/21 DOTA
有关wxpython pyqt内存占用问题分析
2014/06/09 Python
python中virtualenvwrapper安装与使用
2018/05/20 Python
python抓取网页内容并进行语音播报的方法
2018/12/24 Python
python http基本验证方法
2018/12/26 Python
Python实现数据结构线性链表(单链表)算法示例
2019/05/04 Python
Python3 实现减少可调用对象的参数个数
2019/12/20 Python
Python内置方法和属性应用:反射和单例(推荐)
2020/06/19 Python
VIVOBAREFOOT赤脚鞋:让您的脚做自然的事情
2017/06/01 全球购物
Holland & Barrett爱尔兰:英国领先的健康零售商
2019/03/31 全球购物
致200米运动员广播稿
2014/02/06 职场文书
安全生产演讲稿
2014/05/09 职场文书
旷课检讨书范文
2014/10/30 职场文书
思想政治表现评语
2015/01/04 职场文书