webxr-look-to-select.html 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="utf-8">
  5. <title>VR - 用目光进行选择</title>
  6. <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  7. <meta name="twitter:card" content="summary_large_image">
  8. <meta name="twitter:site" content="@threejs">
  9. <meta name="twitter:title" content="Three.js – VR - 用目光进行选择">
  10. <meta property="og:image" content="https://threejs.org/files/share.png">
  11. <link rel="shortcut icon" href="../../files/favicon_white.ico" media="(prefers-color-scheme: dark)">
  12. <link rel="shortcut icon" href="../../files/favicon.ico" media="(prefers-color-scheme: light)">
  13. <link rel="stylesheet" href="../resources/lesson.css">
  14. <link rel="stylesheet" href="../resources/lang.css">
  15. <script type="importmap">
  16. {
  17. "imports": {
  18. "three": "../../build/three.module.js"
  19. }
  20. }
  21. </script>
  22. </head>
  23. <body>
  24. <div class="container">
  25. <div class="lesson-title">
  26. <h1>VR - 用目光进行选择</h1>
  27. </div>
  28. <div class="lesson">
  29. <div class="lesson-main">
  30. <p><strong>注意:本页示例需要支持VR的设备。没有这样的设备则无法运行。参见 <a href="webxr.html">上一篇文章</a> 了解原因</strong></p>
  31. <p>在 <a href="webxr.html">上一篇文章</a> 中,我们介绍了一个使用 three.js 的非常简单的 VR 示例,并讨论了各种类型的 VR 系统。</p>
  32. <p>最简单且可能是最常见的类型是谷歌 Cardboard 风格的 VR,它基本上就是将手机放入一个 5 到 50 美元的面罩中。这种 VR 没有控制器,因此人们必须想出创造性的解决方案来实现用户输入。</p>
  33. <p>最常见的解决方案是“用目光进行选择”,即如果用户将头部对准某个物体一段时间,该物体就会被选中。</p>
  34. <p>让我们来实现“用目光进行选择”功能!我们将从 <a href="webxr.html">上一篇文章中的示例</a> 开始,并添加我们在 <a href="picking.html">拾取文章</a> 中创建的 <code class="notranslate" translate="no">PickHelper</code>。代码如下:</p>
  35. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class PickHelper {
  36. constructor() {
  37. this.raycaster = new THREE.Raycaster();
  38. this.pickedObject = null;
  39. this.pickedObjectSavedColor = 0;
  40. }
  41. pick(normalizedPosition, scene, camera, time) {
  42. // 如果有被选中的物体,则恢复其颜色
  43. if (this.pickedObject) {
  44. this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
  45. this.pickedObject = undefined;
  46. }
  47. // 从视锥体发射一条射线
  48. this.raycaster.setFromCamera(normalizedPosition, camera);
  49. // 获取射线相交的物体列表
  50. const intersectedObjects = this.raycaster.intersectObjects(scene.children);
  51. if (intersectedObjects.length) {
  52. // 选择第一个物体。它是最接近的那个
  53. this.pickedObject = intersectedObjects[0].object;
  54. // 保存其颜色
  55. this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
  56. // 将其自发光颜色设置为闪烁的红/黄色
  57. this.pickedObject.material.emissive.setHex((time * 8) % 2 &gt; 1 ? 0xFFFF00 : 0xFF0000);
  58. }
  59. }
  60. }
  61. </pre>
  62. <p>有关该代码的解释,请参见 <a href="picking.html">拾取文章</a>。</p>
  63. <p>要使用它,我们只需创建一个实例并在渲染循环中调用它:</p>
  64. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const pickHelper = new PickHelper();
  65. ...
  66. function render(time) {
  67. time *= 0.001;
  68. ...
  69. + // 0, 0 是归一化坐标中视图的中心。
  70. + pickHelper.pick({x: 0, y: 0}, scene, camera, time);
  71. </pre>
  72. <p>在原始的拾取示例中,我们将鼠标坐标从 CSS 像素转换为归一化坐标,该坐标在画布上从 -1 到 +1。</p>
  73. <p>但在这种情况下,我们将始终选择相机所对准的位置,即屏幕中心,因此我们为 <code class="notranslate" translate="no">x</code> 和 <code class="notranslate" translate="no">y</code> 都传入 <code class="notranslate" translate="no">0</code>,这在归一化坐标中就是中心。</p>
  74. <p>这样,当我们注视物体时,它们就会闪烁</p>
  75. <p></p><div translate="no" class="threejs_example_container notranslate">
  76. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/webxr-look-to-select.html"></iframe></div>
  77. <a class="threejs_center" href="/manual/examples/webxr-look-to-select.html" target="_blank">点击此处以在新窗口中打开</a>
  78. </div>
  79. <p></p>
  80. <p>通常我们不希望选择是立即发生的。相反,我们要求用户将相机对准他们想要选择的物体几秒钟,以便他们有机会避免意外选择某些东西。</p>
  81. <p>为此,我们需要某种计量器或指示器,或某种方式来传达用户必须持续注视以及需要注视多长时间。</p>
  82. <p>一种简单的方法是制作一个双色纹理,并使用纹理偏移在模型上滑动纹理。</p>
  83. <p>让我们先单独实现这个效果,看看它如何工作,然后再将其添加到 VR 示例中。</p>
  84. <p>首先,我们创建一个 <a href="/docs/#api/en/cameras/OrthographicCamera"><code class="notranslate" translate="no">正交相机</code></a></p>
  85. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const left = -2; // 使用左、右、上、下
  86. const right = 2; // 的值来匹配默认
  87. const top = 1; // 画布大小。
  88. const bottom = -1;
  89. const near = -1;
  90. const far = 1;
  91. const camera = new THREE.OrthographicCamera(left, right, top, bottom, near, far);
  92. </pre>
  93. <p>当然,如果画布大小改变,我们也需要更新它</p>
  94. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  95. time *= 0.001;
  96. if (resizeRendererToDisplaySize(renderer)) {
  97. const canvas = renderer.domElement;
  98. const aspect = canvas.clientWidth / canvas.clientHeight;
  99. + camera.left = -aspect;
  100. + camera.right = aspect;
  101. camera.updateProjectionMatrix();
  102. }
  103. ...
  104. </pre>
  105. <p>现在我们有了一个相机,它显示中心上下各 2 个单位,左右各 aspect 个单位。</p>
  106. <p>接下来,让我们制作一个双色纹理。我们将使用 <a href="/docs/#api/en/textures/DataTexture"><code class="notranslate" translate="no">DataTexture</code></a>,
  107. 它在其他<a href="indexed-textures.html">地方</a>和<a href="post-processing-3dlut.html">示例</a>中也用过。</p>
  108. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function makeDataTexture(data, width, height) {
  109. const texture = new THREE.DataTexture(data, width, height, THREE.RGBAFormat);
  110. texture.minFilter = THREE.NearestFilter;
  111. texture.magFilter = THREE.NearestFilter;
  112. texture.needsUpdate = true;
  113. return texture;
  114. }
  115. const cursorColors = new Uint8Array([
  116. 64, 64, 64, 64, // 深灰色
  117. 255, 255, 255, 255, // 白色
  118. ]);
  119. const cursorTexture = makeDataTexture(cursorColors, 2, 1);
  120. </pre>
  121. <p>然后我们将该纹理应用于一个 <a href="/docs/#api/en/geometries/TorusGeometry"><code class="notranslate" translate="no">TorusGeometry</code></a></p>
  122. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const ringRadius = 0.4;
  123. const tubeRadius = 0.1;
  124. const tubeSegments = 4;
  125. const ringSegments = 64;
  126. const cursorGeometry = new THREE.TorusGeometry(
  127. ringRadius, tubeRadius, tubeSegments, ringSegments);
  128. const cursorMaterial = new THREE.MeshBasicMaterial({
  129. color: 'white',
  130. map: cursorTexture,
  131. transparent: true,
  132. blending: THREE.CustomBlending,
  133. blendSrc: THREE.OneMinusDstColorFactor,
  134. blendDst: THREE.OneMinusSrcColorFactor,
  135. });
  136. const cursor = new THREE.Mesh(cursorGeometry, cursorMaterial);
  137. scene.add(cursor);
  138. </pre>
  139. <p>然后在 <code class="notranslate" translate="no">render</code> 中调整纹理的偏移</p>
  140. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  141. time *= 0.001;
  142. if (resizeRendererToDisplaySize(renderer)) {
  143. const canvas = renderer.domElement;
  144. const aspect = canvas.clientWidth / canvas.clientHeight;
  145. camera.left = -aspect;
  146. camera.right = aspect;
  147. camera.updateProjectionMatrix();
  148. }
  149. + const fromStart = 0;
  150. + const fromEnd = 2;
  151. + const toStart = -0.5;
  152. + const toEnd = 0.5;
  153. + cursorTexture.offset.x = THREE.MathUtils.mapLinear(
  154. + time % 2,
  155. + fromStart, fromEnd,
  156. + toStart, toEnd);
  157. renderer.render(scene, camera);
  158. }
  159. </pre>
  160. <p><code class="notranslate" translate="no">THREE.MathUtils.mapLinear</code> 将一个在 <code class="notranslate" translate="no">fromStart</code> 和 <code class="notranslate" translate="no">fromEnd</code> 之间变化的值映射到 <code class="notranslate" translate="no">toStart</code> 和 <code class="notranslate" translate="no">toEnd</code> 之间的值。在上面的例子中,我们取 <code class="notranslate" translate="no">time % 2</code>,即一个从 0 到 2 变化的值,并将其映射到从 -0.5 到 0.5 变化的值。</p>
  161. <p><a href="textures.html">纹理</a> 使用从 0 到 1 的归一化纹理坐标映射到几何体上。这意味着我们的 2x1 像素图像,设置为默认的 <code class="notranslate" translate="no">THREE.ClampToEdge</code> 包装模式,如果我们调整纹理坐标为 -0.5,则整个网格将显示第一种颜色;如果调整为 +0.5,则整个网格将显示第二种颜色。在两者之间,由于过滤设置为 <code class="notranslate" translate="no">THREE.NearestFilter</code>,我们能够将两种颜色之间的过渡移动通过几何体。</p>
  162. <p>让我们顺便添加一个背景纹理,就像我们在 <a href="backgrounds.html">背景文章</a> 中介绍的那样。我们将只使用一组 2x2 的颜色,但设置纹理的重复属性,使其形成一个 8x8 的网格。这样可以为我们的光标提供一个渲染背景,以便我们检查它在不同颜色上的显示效果。</p>
  163. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const backgroundColors = new Uint8Array([
  164. + 0, 0, 0, 255, // 黑色
  165. + 90, 38, 38, 255, // 深红色
  166. + 100, 175, 103, 255, // 中等绿色
  167. + 255, 239, 151, 255, // 浅黄色
  168. +]);
  169. +const backgroundTexture = makeDataTexture(backgroundColors, 2, 2);
  170. +backgroundTexture.wrapS = THREE.RepeatWrapping;
  171. +backgroundTexture.wrapT = THREE.RepeatWrapping;
  172. +backgroundTexture.repeat.set(4, 4);
  173. const scene = new THREE.Scene();
  174. +scene.background = backgroundTexture;
  175. </pre>
  176. <p>现在如果我们运行它,你会看到我们得到了一个类似圆圈的计量器,并且我们可以设置计量器的位置。</p>
  177. <p></p><div translate="no" class="threejs_example_container notranslate">
  178. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/webxr-look-to-select-selector.html"></iframe></div>
  179. <a class="threejs_center" href="/manual/examples/webxr-look-to-select-selector.html" target="_blank">点击此处以在新窗口中打开</a>
  180. </div>
  181. <p></p>
  182. <p>请注意并尝试以下几点:</p>
  183. <ul>
  184. <li><p>我们设置了 <code class="notranslate" translate="no">cursorMaterial</code> 的 <code class="notranslate" translate="no">blending</code>、<code class="notranslate" translate="no">blendSrc</code> 和 <code class="notranslate" translate="no">blendDst</code> 属性如下:</p>
  185. <pre class="prettyprint showlinemods notranslate notranslate" translate="no"> blending: THREE.CustomBlending,
  186. blendSrc: THREE.OneMinusDstColorFactor,
  187. blendDst: THREE.OneMinusSrcColorFactor,
  188. </pre><p>这产生了一种<em>反相</em>效果。注释掉这三行代码,你就能看到区别。我猜测这种反相效果在这里是最好的,因为这样无论光标在什么颜色上,我们都应该能看到它。</p>
  189. </li>
  190. <li><p>我们使用了 <a href="/docs/#api/en/geometries/TorusGeometry"><code class="notranslate" translate="no">TorusGeometry</code></a> 而不是 <a href="/docs/#api/en/geometries/RingGeometry"><code class="notranslate" translate="no">RingGeometry</code></a></p>
  191. <p>出于某些原因,<a href="/docs/#api/en/geometries/RingGeometry"><code class="notranslate" translate="no">RingGeometry</code></a> 使用了平面的 UV 映射方案。因此,如果我们使用 <a href="/docs/#api/en/geometries/RingGeometry"><code class="notranslate" translate="no">RingGeometry</code></a>,纹理会在环上水平滑动,而不是像上面那样环绕它。</p>
  192. <p>尝试一下,将 <a href="/docs/#api/en/geometries/TorusGeometry"><code class="notranslate" translate="no">TorusGeometry</code></a> 改为 <a href="/docs/#api/en/geometries/RingGeometry"><code class="notranslate" translate="no">RingGeometry</code></a>(在上面的示例中它只是被注释掉了),你就会明白我的意思。</p>
  193. <p>(在某种定义下的)<em>正确</em>做法是:要么使用 <a href="/docs/#api/en/geometries/RingGeometry"><code class="notranslate" translate="no">RingGeometry</code></a> 但修正纹理坐标,使其环绕环形;要么自己生成环形几何体。但是,圆环体效果很好。直接放置在相机前方,使用 <a href="/docs/#api/en/materials/MeshBasicMaterial"><code class="notranslate" translate="no">MeshBasicMaterial</code></a>,它看起来会完全像一个环,并且纹理坐标环绕环形,因此它符合我们的需求。</p>
  194. </li>
  195. </ul>
  196. <p>让我们将它与上面的 VR 代码集成起来。</p>
  197. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class PickHelper {
  198. - constructor() {
  199. + constructor(camera) {
  200. this.raycaster = new THREE.Raycaster();
  201. this.pickedObject = null;
  202. - this.pickedObjectSavedColor = 0;
  203. + const cursorColors = new Uint8Array([
  204. + 64, 64, 64, 64, // 深灰色
  205. + 255, 255, 255, 255, // 白色
  206. + ]);
  207. + this.cursorTexture = makeDataTexture(cursorColors, 2, 1);
  208. +
  209. + const ringRadius = 0.4;
  210. + const tubeRadius = 0.1;
  211. + const tubeSegments = 4;
  212. + const ringSegments = 64;
  213. + const cursorGeometry = new THREE.TorusGeometry(
  214. + ringRadius, tubeRadius, tubeSegments, ringSegments);
  215. +
  216. + const cursorMaterial = new THREE.MeshBasicMaterial({
  217. + color: 'white',
  218. + map: this.cursorTexture,
  219. + transparent: true,
  220. + blending: THREE.CustomBlending,
  221. + blendSrc: THREE.OneMinusDstColorFactor,
  222. + blendDst: THREE.OneMinusSrcColorFactor,
  223. + });
  224. + const cursor = new THREE.Mesh(cursorGeometry, cursorMaterial);
  225. + // 将光标作为相机的子对象添加
  226. + camera.add(cursor);
  227. + // 并将其移动到相机前方
  228. + cursor.position.z = -1;
  229. + const scale = 0.05;
  230. + cursor.scale.set(scale, scale, scale);
  231. + this.cursor = cursor;
  232. +
  233. + this.selectTimer = 0;
  234. + this.selectDuration = 2;
  235. + this.lastTime = 0;
  236. }
  237. pick(normalizedPosition, scene, camera, time) {
  238. + const elapsedTime = time - this.lastTime;
  239. + this.lastTime = time;
  240. - // 如果有被选中的物体,则恢复其颜色
  241. - if (this.pickedObject) {
  242. - this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
  243. - this.pickedObject = undefined;
  244. - }
  245. + const lastPickedObject = this.pickedObject;
  246. + this.pickedObject = undefined;
  247. // 从视锥体发射一条射线
  248. this.raycaster.setFromCamera(normalizedPosition, camera);
  249. // 获取射线相交的物体列表
  250. const intersectedObjects = this.raycaster.intersectObjects(scene.children);
  251. if (intersectedObjects.length) {
  252. // 选择第一个物体。它是最接近的那个
  253. this.pickedObject = intersectedObjects[0].object;
  254. - // 保存其颜色
  255. - this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
  256. - // 将其自发光颜色设置为闪烁的红/黄色
  257. - this.pickedObject.material.emissive.setHex((time * 8) % 2 &gt; 1 ? 0xFFFF00 : 0xFF0000);
  258. }
  259. + // 仅当光标击中物体时才显示
  260. + this.cursor.visible = this.pickedObject ? true : false;
  261. +
  262. + let selected = false;
  263. +
  264. + // 如果我们正在注视的物体与之前相同
  265. + // 则增加选择计时器的时间
  266. + if (this.pickedObject &amp;&amp; lastPickedObject === this.pickedObject) {
  267. + this.selectTimer += elapsedTime;
  268. + if (this.selectTimer &gt;= this.selectDuration) {
  269. + this.selectTimer = 0;
  270. + selected = true;
  271. + }
  272. + } else {
  273. + this.selectTimer = 0;
  274. + }
  275. +
  276. + // 设置光标材质以显示计时器状态
  277. + const fromStart = 0;
  278. + const fromEnd = this.selectDuration;
  279. + const toStart = -0.5;
  280. + const toEnd = 0.5;
  281. + this.cursorTexture.offset.x = THREE.MathUtils.mapLinear(
  282. + this.selectTimer,
  283. + fromStart, fromEnd,
  284. + toStart, toEnd);
  285. +
  286. + return selected ? this.pickedObject : undefined;
  287. }
  288. }
  289. </pre>
  290. <p>你可以看到上面的代码中,我们添加了所有创建光标几何体、纹理和材质的代码,并将其作为相机的子对象添加,因此它将始终位于相机前方。请注意,我们需要将相机添加到场景中,否则光标将不会被渲染。</p>
  291. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+scene.add(camera);
  292. </pre>
  293. <p>然后我们检查这次拾取的物体是否与上次相同。如果是,我们将经过的时间加到计时器中,如果计时器达到其限制,我们就返回选中的项目。</p>
  294. <p>现在让我们使用它来选择立方体。作为一个简单的例子,我们还将添加 3 个球体。当一个立方体被选中时,我们将隐藏该立方体并显示相应的球体。</p>
  295. <p>因此,首先我们创建一个球体几何体</p>
  296. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const boxWidth = 1;
  297. const boxHeight = 1;
  298. const boxDepth = 1;
  299. -const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
  300. +const boxGeometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
  301. +
  302. +const sphereRadius = 0.5;
  303. +const sphereGeometry = new THREE.SphereGeometry(sphereRadius);
  304. </pre>
  305. <p>然后让我们创建 3 对立方体和球体网格。我们将使用 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map"><code class="notranslate" translate="no">Map</code></a>,以便我们可以将每个 <a href="/docs/#api/en/objects/Mesh"><code class="notranslate" translate="no">Mesh</code></a> 与其对应的伙伴关联起来。</p>
  306. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const cubes = [
  307. - makeInstance(geometry, 0x44aa88, 0),
  308. - makeInstance(geometry, 0x8844aa, -2),
  309. - makeInstance(geometry, 0xaa8844, 2),
  310. -];
  311. +const meshToMeshMap = new Map();
  312. +[
  313. + { x: 0, boxColor: 0x44aa88, sphereColor: 0xFF4444, },
  314. + { x: 2, boxColor: 0x8844aa, sphereColor: 0x44FF44, },
  315. + { x: -2, boxColor: 0xaa8844, sphereColor: 0x4444FF, },
  316. +].forEach((info) =&gt; {
  317. + const {x, boxColor, sphereColor} = info;
  318. + const sphere = makeInstance(sphereGeometry, sphereColor, x);
  319. + const box = makeInstance(boxGeometry, boxColor, x);
  320. + // 隐藏球体
  321. + sphere.visible = false;
  322. + // 将球体映射到立方体
  323. + meshToMeshMap.set(box, sphere);
  324. + // 将立方体映射到球体
  325. + meshToMeshMap.set(sphere, box);
  326. +});
  327. </pre>
  328. <p>在 <code class="notranslate" translate="no">render</code> 中,当我们旋转立方体时,需要遍历 <code class="notranslate" translate="no">meshToMeshMap</code> 而不是 <code class="notranslate" translate="no">cubes</code>。</p>
  329. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-cubes.forEach((cube, ndx) =&gt; {
  330. +let ndx = 0;
  331. +for (const mesh of meshToMeshMap.keys()) {
  332. const speed = 1 + ndx * .1;
  333. const rot = time * speed;
  334. - cube.rotation.x = rot;
  335. - cube.rotation.y = rot;
  336. -});
  337. + mesh.rotation.x = rot;
  338. + mesh.rotation.y = rot;
  339. + ++ndx;
  340. +}
  341. </pre>
  342. <p>现在我们可以使用我们新的 <code class="notranslate" translate="no">PickHelper</code> 实现来选择其中一个物体。当物体被选中时,我们隐藏该物体并显示其对应的伙伴物体。</p>
  343. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// 0, 0 是归一化坐标中视图的中心。
  344. -pickHelper.pick({x: 0, y: 0}, scene, camera, time);
  345. +const selectedObject = pickHelper.pick({x: 0, y: 0}, scene, camera, time);
  346. +if (selectedObject) {
  347. + selectedObject.visible = false;
  348. + const partnerObject = meshToMeshMap.get(selectedObject);
  349. + partnerObject.visible = true;
  350. +}
  351. </pre>
  352. <p>有了这些,我们就应该有了一个相当不错的“注视选择”实现。</p>
  353. <p></p><div translate="no" class="threejs_example_container notranslate">
  354. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/webxr-look-to-select-w-cursor.html"></iframe></div>
  355. <a class="threejs_center" href="/manual/examples/webxr-look-to-select-w-cursor.html" target="_blank">点击此处以在新窗口中打开</a>
  356. </div>
  357. <p></p>
  358. <p>希望这个示例能给你一些关于如何实现像 Google Cardboard 级别的“注视选择”用户体验的想法。使用纹理坐标偏移来滑动纹理也是一种常用且有用的技术。</p>
  359. <p>接下来,<a href="webxr-point-to-select.html">让我们允许拥有 VR 控制器的用户指向并移动物体</a>。</p>
  360. </div>
  361. </div>
  362. </div>
  363. <script src="../resources/prettify.js"></script>
  364. <script src="../resources/lesson.js"></script>
  365. </body></html>
粤ICP备19079148号