经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 软件/图像 » WebGL » 查看文章
WebGL模型拾取——射线法
来源:cnblogs  作者:ccentry  时间:2018/11/17 14:42:24  对本文有异议

  今天要把WebGL中一个非常重要的算法记录下来——raycaster射线法拾取模型。首先我们来了解一下为什么要做模型拾取,我们在做webgl场景交互的时候经常要选中场景中的某个模型,比如鼠标拖拽旋转,平移。为了能做到鼠标交互,就首先要能选中场景中的模型对象,这就要用到模型拾取算法,本文仅讨论射线法模型拾取raycaster。

  所谓射线法就是利用一根射线去和场景中的模型进行碰撞,撞到的模型对象就是被拾取到的模型。请看下图

  我逐个来解释一下上图中的元素。首先解释相机(camera),这就是人眼的抽象,代表用户在屏幕前的眼睛位置。人眼看到的世界是透视的(perspective),因此我们构造的视棱台(frustum)基于透视投影。整个视棱台区域介于场景近截面(near)和远截面(far)之间,这个区间内的空间就是我们可以看到的场景空间。需要说明一下,near近截面我们这里紧贴屏幕(screen),即距离很小约等于0.1,far远截面就是我们认为的视线最远能看到的距离,我们这里设置为1000。屏幕screen在近截面前0.1的位置上,也是离人眼最近的截面,也是鼠标交互的界面,这是要事先解释明白的。理解了这个空间结构以后我们就开始讲解raycaster的算法原理。

  首先我们来看一下鼠标在屏幕上的位置点P0,我们可以看到P0点(鼠标),这个就是鼠标在屏幕上的位置。我们再来看看triangle1三角形1,这就是透视空间中triangle2三角形2在屏幕上的投影。我们可以明显看到鼠标位置P0点在屏幕triangle1三角形1内部,即鼠标点选中triangle1三角形1。这在屏幕上可以看的很清楚,但是问题来了,在空间中鼠标是没有深度概念的,即鼠标只有XY坐标,没有Z坐标,那我们在视棱台的空间坐标系中如何表示鼠标的三维空间位置呢,如果没有鼠标的3维空间坐标,如何判断在视棱台空间中鼠标是否选中triangle2三角形2这个模型对象呢?也许有同学会说,triangle1就是triangle2的投影嘛,选中投影就是选中模型了不是,我就这么说,非常正确,能说出这样的话就已经完全理解了模型在屏幕上的投影的原理,但是新的问题随之又来了,如何获取鼠标点选模型的坐标呢,即如何得到鼠标点在模型上的那个点的三维空间坐标呢,如果仅仅判断是否选中,那投影就够用了,但要计算鼠标点选模型上的点坐标,就远远不够用了。为了解决这个问题,raycaster算法应运而生。

  raycaster顾名思义就是射线投射。他的原理其实非常简单,就是用一根射线去交有限平面,获得交点。射线是有起点的,起点就是我们的眼睛。我们做一根起于camera,通过鼠标在屏幕上的位置P0,继续延伸,交视棱台近截面于P1,继续延伸,交视棱台远截面于P3,射线截止,我们得到了一根线段P1-P3。这根线段P1-P3就是我们眼睛能看到的鼠标发出的射线在透视空间中的部分,凡是这根线段碰到的模型,都是鼠标点选中的空间模型。而这根线段和模型的交点就是鼠标点选模型的交点,这个交点坐标就是鼠标点选模型的交点空间三维坐标。这样就顺利解决了上面我们的问题,即求鼠标点选空间三维模型的交点坐标。在上图中我们看得很清楚,这个交点就是P2,接下来我们就来讲解怎么求这个P2的空间坐标。

  做图形学的同学们都非常清楚。如何求线段和平面的交点,这里我截取一部分代码,以供叙述方便,以下就是求线段截取平面交点的函数。

  1. /*
  2.  
  3. */
  4. let Intersector = require('./Intersector');
  5. let LineSegmentIntersection = require('./Intersection').LineSegmentIntersection;
  6. let Vec3 = require('./Vec3');
  7. let Mat4 = require('./Mat4');
  8. let Algorithm = require('./Algorithm');
  9. let LineSegmentIntersector = function () {
  10. Intersector.call(this);
  11. //原始的起始点和临界值,初始化设置的数据,保留作为参照,设置后不再变动
  12. this._orginStart = Vec3.new();//线段起点
  13. this._orginEnd = Vec3.new();//线段终点
  14. this._orginThreshold = 0.0;//点和线求相交时的临界值,完全相交是很难求到的
  15. //临时存储,每次求交都可能会变动的数据
  16. //对于有变换的几何求交,不会变换几何顶点而是变换起始点和临界值
  17. this._start = Vec3.new();//线段起点
  18. this._end = Vec3.new();//线段终点
  19. this._threshold = 0.0;//点和线求相交时的临界值,完全相交是很难求到的
  20.  
  21. this._direction = Vec3.new();
  22. this._length = 0;
  23. this._inverseLength = 0;
  24. this._matrix = Mat4.new();
  25. };
  26. LineSegmentIntersector.prototype = Object.create(Intersector.prototype);
  27. LineSegmentIntersector.prototype.constructor = LineSegmentIntersector;
  28. Object.assign(LineSegmentIntersector.prototype, {
  29. init: function (start, end, threshold) {
  30. Vec3.copy(this._orginStart, start);
  31. Vec3.copy(this._orginEnd, end);
  32. Vec3.copy(this._start, start);
  33. Vec3.copy(this._end, end);
  34. if (threshold !== undefined) {
  35. this._orginThreshold = threshold;
  36. this._threshold = threshold;
  37. }
  38. },
  39. intersect: function (drawable) {
  40. //先使用包围盒子
  41. if (!drawable.getBoundingBox().intersectLineSegment(this._orginStart, this._orginEnd)) {
  42. return;
  43. }
  44. this._drawable = drawable;
  45. let geometry = drawable.getGeometry();
  46. let vertexbuffer = geometry.getBufferArray('Vertex');
  47. this._vertices = vertexbuffer.getArrayBuffer();
  48. //没有顶点数据不处理直接返回
  49. if (!this._vertices) return;
  50. //没有图元不处理直接返回
  51. let primitive = geometry.getPrimitive();
  52. if (!primitive) return;
  53. //初始化求相交的各种数据
  54. let matrix = drawable.getTransform();
  55. if (this._transform !== matrix) {//如果不一样,需要计算新的起始点以及各种临时数据
  56. this._transform = matrix;
  57. Mat4.invert(this._matrix, matrix);
  58. //根据矩阵计算新的临界值
  59. if (this._orginThreshold > 0.0) {
  60. let tmp = this._start;
  61. Mat4.getScale(tmp, this._matrix);
  62. let x = tmp[0];
  63. let y = tmp[1];
  64. let z = tmp[2];
  65. this._threshold = this._orginThreshold * (x > y ? (x > z ? x : z) : y > z ? y : z);
  66. }
  67. //根据矩阵计算新的起始点
  68. Vec3.transformMat4(this._start, this._orginStart, this._matrix);
  69. Vec3.transformMat4(this._end, this._orginEnd, this._matrix);
  70. //根据新的起始点计算各种临时数据
  71. Vec3.sub(this._direction, this._end, this._start);
  72. this._length = Vec3.length(this._direction);//长度
  73. this._inverseLength = this._length <= Algorithm.EPSILON ? 0.0 : 1.0 / this._length;
  74. Vec3.scale(this._direction, this._direction, this._inverseLength);//求单位向量
  75. }//如果变换与上次一样,直接使用上次的数据求相交
  76. //求相交
  77. primitive.operate(this);
  78. },
  79. intersectPoint: function (vertex) {
  80. // https://www.geometrictools.com/GTEngine/Include/Mathematics/GteDistPointSegment.h
  81. //起点指向绘制点,向量M
  82. let m = Vec3.MemoryPool.alloc();
  83. Vec3.sub(m, vertex, this._start);
  84. //起点指向终点,向量N
  85. let n = Vec3.MemoryPool.alloc();
  86. Vec3.sub(n, this._end, this._start);
  87. //求M在N上的投影比例值
  88. //|m|*|n|*cos / \n\*\n\ = |m|*cos/\n\
  89. let r = Vec3.dot(m, n) * this._inverseLength * this._inverseLength;
  90. //计算绘制点到线段的距离
  91. let sqrdist = 1.0;
  92. if (r < 0.0) {//夹角超过90度,绘制点在当前线段起点后面,求绘制点与起点的距离
  93. sqrdist = Vec3.sqrLen(m);
  94. } else if (r > 1.0) {//绘制点在当前线段终点后面,求绘制点与终点的距离
  95. sqrdist = Vec3.sqrDist(vertex, this._end);
  96. } else {//在0到1之间
  97. //m - n * r 如果平行或者接近于平行,结果接近于0,相交
  98. sqrdist = Vec3.sqrLen(Vec3.scaleAndAdd(m, m, n, -r));
  99. }
  100. let intersection = undefined;
  101. if (sqrdist > this._threshold * this._threshold) {//超过了临界值,没有相交返回
  102. } else {
  103. //相交
  104. intersection = new LineSegmentIntersection();
  105. //intersection._i1 = index;
  106. //intersection._r1 = 1.0;
  107. Vec3.scaleAndAdd(intersection._point, this._start, n, r);
  108. intersection._ratio = r;
  109. }
  110. Vec3.MemoryPool.free(m);
  111. Vec3.MemoryPool.free(n);
  112. return intersection;
  113. },
  114. intersectLine: function (vertex0, vertex1) {
  115. // https://www.geometrictools.com/GTEngine/Samples/Geometrics/DistanceSegments3/DistanceSegments3.cpp
  116. //let epsilon = 0.00000001;
  117. //起点到终点的向量
  118. let u = Vec3.MemoryPool.alloc();
  119. Vec3.sub(u, vertex1, vertex0);
  120. let v = Vec3.MemoryPool.alloc();
  121. Vec3.sub(v, this._end, this._start);
  122. let w = Vec3.MemoryPool.alloc();
  123. Vec3.sub(w, vertex0, this._start);
  124. let a = Vec3.dot(u, u);
  125. let b = Vec3.dot(u, v);
  126. let c = Vec3.dot(v, v);
  127. let d = Vec3.dot(u, w);
  128. let e = Vec3.dot(v, w);
  129. let D = a * c - b * b;
  130. let sN;
  131. let tN;
  132. let sD = D;
  133. let tD = D;
  134. // compute the line parameters of the two closest points
  135. if (D < Algorithm.EPSILON) {//平行
  136. // the lines are almost parallel
  137. sN = 0.0; // force using point P0 on segment S1
  138. sD = 1.0; // to prevent possible division by 0.0 later
  139. tN = e;
  140. tD = c;
  141. } else {
  142. // get the closest points on the infinite lines
  143. sN = b * e - c * d;
  144. tN = a * e - b * d;
  145. if (sN < 0.0) {
  146. // sc < 0 => the s=0 edge is visible
  147. sN = 0.0;
  148. tN = e;
  149. tD = c;
  150. } else if (sN > sD) {
  151. // sc > 1 => the s=1 edge is visible
  152. sN = sD;
  153. tN = e + b;
  154. tD = c;
  155. }
  156. }
  157. if (tN < 0.0) {
  158. // tc < 0 => the t=0 edge is visible
  159. tN = 0.0;
  160. // recompute sc for this edge
  161. if (-d < 0.0) sN = 0.0;
  162. else if (-d > a) sN = sD;
  163. else {
  164. sN = -d;
  165. sD = a;
  166. }
  167. } else if (tN > tD) {
  168. // tc > 1 => the t=1 edge is visible
  169. tN = tD;
  170. // recompute sc for this edge
  171. if (-d + b < 0.0) sN = 0;
  172. else if (-d + b > a) sN = sD;
  173. else {
  174. sN = -d + b;
  175. sD = a;
  176. }
  177. }
  178. // finally do the division to get sc and tc
  179. let sc = Math.abs(sN) < Algorithm.EPSILON ? 0.0 : sN / sD;
  180. let tc = Math.abs(tN) < Algorithm.EPSILON ? 0.0 : tN / tD;
  181. // get the difference of the two closest points
  182. let closest0 = Vec3.MemoryPool.alloc();
  183. let closest1 = Vec3.MemoryPool.alloc();
  184. Vec3.scaleAndAdd(closest0, vertex0, u, sc);
  185. Vec3.scaleAndAdd(closest1, this._start, v, tc);
  186. let sqrDistance = Vec3.sqrDist(closest0, closest1);
  187. Vec3.MemoryPool.free(closest0);
  188. Vec3.MemoryPool.free(closest1);
  189. let intersection = undefined;
  190. if (sqrDistance > this._threshold * this._threshold) {
  191. } else {
  192. //相交
  193. intersection = new LineSegmentIntersection();
  194. // intersection._i1 = index0;
  195. // intersection._i2 = index1;
  196. // intersection._r1 = 1.0 - tc;
  197. // intersection._r2 = tc;
  198. Vec3.copy(intersection._point, closest1);
  199. intersection._ratio = tc;
  200. }
  201. Vec3.MemoryPool.free(u);
  202. Vec3.MemoryPool.free(v);
  203. Vec3.MemoryPool.free(w);
  204. return intersection;
  205. },
  206. intersectTriangle: function (vertex0, vertex1, vertex2) {
  207. let e2 = Vec3.MemoryPool.alloc();
  208. Vec3.sub(e2, vertex2, vertex0);
  209. let e1 = Vec3.MemoryPool.alloc();
  210. Vec3.sub(e1, vertex1, vertex0);
  211. let pvec = Vec3.MemoryPool.alloc();
  212. Vec3.cross(pvec, this._direction, e2);
  213. let intersection = undefined;
  214. //线段与三角面点积
  215. let det = Vec3.dot(pvec, e1);
  216. //判断三角形所在的平面与线段是否平行,如果平行铁定不相交,面片没有厚度
  217. if (Math.abs(det) < Algorithm.EPSILON) {
  218. //return undefined;
  219. }else{
  220. let invDet = 1.0 / det;
  221. let tvec = Vec3.MemoryPool.alloc();
  222. Vec3.sub(tvec, this._start, vertex0);
  223. let u = Vec3.dot(pvec, tvec) * invDet;
  224. //三角面超出了线段两个点范围外面,铁定不相交
  225. if (u < 0.0 || u > 1.0) {
  226. //return undefined;
  227. }else{
  228. let qvec = Vec3.MemoryPool.alloc();
  229. Vec3.cross(qvec, tvec, e1);
  230. let v = Vec3.dot(qvec, this._direction) * invDet;
  231. //
  232. if (v < 0.0 || u + v > 1.0) {
  233. //return undefined;
  234. }else{
  235. let t = Vec3.dot(qvec, e2) * invDet;
  236. if (t < Algorithm.EPSILON || t > this._length) {
  237. //return undefined;
  238. }else{
  239. //相交
  240. intersection = new LineSegmentIntersection();
  241. //求相交点
  242. let r0 = 1.0 - u - v;
  243. let r1 = u;
  244. let r2 = v;
  245. let r = t * this._inverseLength;
  246. let interX = vertex0[0] * r0 + vertex1[0] * r1 + vertex2[0] * r2;
  247. let interY = vertex0[1] * r0 + vertex1[1] * r1 + vertex2[1] * r2;
  248. let interZ = vertex0[2] * r0 + vertex1[2] * r1 + vertex2[2] * r2;
  249. // intersection._i1 = index0;
  250. // intersection._i2 = index1;
  251. // intersection._i3 = index2;
  252. // intersection._r1 = r0;
  253. // intersection._r2 = r1;
  254. // intersection._r3 = r2;
  255. //这里的点没有经过变换,不是真实的世界坐标点
  256. Vec3.set(intersection._point, interX, interY, interZ);
  257. Vec3.transformMat4(intersection._point, intersection._point, this._transform);
  258. //求法向量,法向量未变换,如果有用途也要变换
  259. let normal = intersection._normal;
  260. Vec3.cross(normal, e1, e2);
  261. Vec3.normalize(normal, normal);
  262. //比例,在相交线段上的比例,不需要变换
  263. intersection._ratio = r;
  264. }
  265. }
  266. Vec3.MemoryPool.free(qvec);
  267. }
  268. Vec3.MemoryPool.free(tvec);
  269. }
  270. Vec3.MemoryPool.free(e1);
  271. Vec3.MemoryPool.free(e2);
  272. Vec3.MemoryPool.free(pvec);
  273. return intersection;
  274. // http://gamedev.stackexchange.com/questions/54505/negative-scale-in-matrix-4x4
  275. // https://en.wikipedia.org/wiki/Determinant#Orientation_of_a_basis
  276. // you can't exactly extract scale of a matrix but the determinant will tell you
  277. // if the orientation is preserved
  278. //intersection._backface = mat4.determinant(intersection._matrix) * det < 0;
  279. },
  280. intersectBoundingBox: function (box) {
  281. return box.intersectLineSegment(this._orginStart, this._orginEnd);
  282. },
  283. });
  284. module.exports = LineSegmentIntersector;
  285. // setDrawable: function (drawable) {
  286. // this._geometry = drawable.getGeometry();
  287. // this._vertices = this._geometry.getBufferArray('Vertex');
  288. //
  289. // let matrix = drawable.getTransform();
  290. // if (this._transform === matrix) {//如果与上次的一样,不再处理
  291. // return;
  292. // }
  293. //
  294. // //如果不一样,需要计算新的起始点已经各种临时数据
  295. // this._transform = matrix;
  296. // Mat4.invert(this._matrix, matrix);
  297. //
  298. // //根据矩阵计算新的临界值
  299. // if (this._orginThreshold > 0.0) {
  300. // let tmp = this._start;
  301. // Mat4.getScale(tmp, this._matrix);
  302. // let x = tmp[0];
  303. // let y = tmp[1];
  304. // let z = tmp[2];
  305. // this._threshold = this._orginThreshold * (x > y ? (x > z ? x : z) : y > z ? y : z);
  306. // }
  307. // //根据矩阵计算新的起始点
  308. // Vec3.transformMat4(this._start, this._orginStart, this._matrix);
  309. // Vec3.transformMat4(this._end, this._orginEnd, this._matrix);
  310. //
  311. // //根据新的起始点计算各种临时数据
  312. // Vec3.sub(this._direction, this._end, this._start);
  313. // this._length = Vec3.length(this._direction);//长度
  314. // this._inverseLength = this._length <= Algorithm.EPSILON ? 1.0 / this._length : 0.0;
  315. // Vec3.scale(this._direction, this._direction, this._inverseLength);//求单位向量
  316. // },
  317. // setGeometry: function (geometry, matrix) {
  318. // Intersector.prototype.setGeometry.call(this, geometry, matrix);
  319. //
  320. // //如果不一样,需要计算新的起始点已经各种临时数据
  321. // Mat4.invert(this._matrix, matrix);
  322. //
  323. // //根据矩阵计算新的临界值
  324. // if (this._orginThreshold > 0.0) {
  325. // let tmp = this._start;
  326. // Mat4.getScale(tmp, this._matrix);
  327. // let x = tmp[0];
  328. // let y = tmp[1];
  329. // let z = tmp[2];
  330. // this._threshold = this._orginThreshold * (x > y ? (x > z ? x : z) : y > z ? y : z);
  331. // }
  332. // //根据矩阵计算新的起始点
  333. // Vec3.transformMat4(this._start, this._orginStart, this._matrix);
  334. // Vec3.transformMat4(this._end, this._orginEnd, this._matrix);
  335. //
  336. // //根据新的起始点计算各种临时数据
  337. // Vec3.sub(this._direction, this._end, this._start);
  338. // this._length = Vec3.length(this._direction);//长度
  339. // this._inverseLength = this._length <= Algorithm.EPSILON ? 1.0 / this._length : 0.0;
  340. // Vec3.scale(this._direction, this._direction, this._inverseLength);//求单位向量
  341. // },
  342. // setGeometry: function (geometry) {
  343. // //没有顶点数据不处理直接返回
  344. // let vertexbuffer = geometry.getBufferArray('Vertex');
  345. // if(!vertexbuffer) return;
  346. //
  347. // //没有图元不处理直接返回
  348. // let primitive = geometry.getPrimitive();
  349. // if (primitive)
  350. // primitive.operate(this);
  351. // },

  以上的LineSegmentIntersector就是计算线段和平面交点的类,具体算法不再赘述,请自行参考《WebGL编程指南》。好了,我们接下来就看一个项目中的具体案例,请看下图

  我们在pick事件中使用了LineSegmentIntersector对场景中的包围盒和坐标系模型进行了raycaster射线碰撞检测,结果我们得到了一系列的返回对象,其中包括包围盒的2个面,坐标系的一根坐标轴的geometry,这就另我们觉得难办了,鼠标射线碰到了不止一个模型,我们该怎么办呢,这里就要说明一下,一般我们都取离near近截面最近的一个模型作为我们pick选中的模型,因为其他模型都被处于前方的该模型遮挡住了。

  好了,今天对raycaster的解释就结束了,只是初步了解一下,raycaster还有很多应用场景,这里和我们的鼠标拾取不相关的就不介绍了,谢谢大家阅读,欢迎大家一起留言探讨,再次感谢。转载本文请注明出处:https://www.cnblogs.com/ccentry/p/9973165.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号