webxr_vr_handinput_pointerclick.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <title>three.js webxr hands - point and click</title>
  5. <meta charset="utf-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  7. <link type="text/css" rel="stylesheet" href="main.css">
  8. </head>
  9. <body>
  10. <div id="info">
  11. <a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> vr - handinput - point and click<br />
  12. (Oculus Browser with #webxr-hands flag enabled)
  13. </div>
  14. <script type="module">
  15. import * as THREE from '../build/three.module.js';
  16. import { VRButton } from './jsm/webxr/VRButton.js';
  17. import { XRControllerModelFactory } from './jsm/webxr/XRControllerModelFactory.js';
  18. import { OculusHandModel } from './jsm/webxr/OculusHandModel.js';
  19. import { OculusHandPointerModel } from './jsm/webxr/OculusHandPointerModel.js';
  20. import { createText } from './jsm/webxr/Text2D.js';
  21. import { World, System, Component, TagComponent, Types } from "https://ecsy.io/build/ecsy.module.js";
  22. class Object3D extends Component { }
  23. Object3D.schema = {
  24. object: { type: Types.Ref }
  25. };
  26. class Button extends Component { }
  27. Button.schema = {
  28. // button states: [none, hovered, pressed]
  29. currState: { type: Types.String, default: 'none' },
  30. prevState: { type: Types.String, default: 'none' },
  31. action: { type: Types.Ref, default: () => { } }
  32. }
  33. class ButtonSystem extends System {
  34. execute(delta, time) {
  35. this.queries.buttons.results.forEach(entity => {
  36. var button = entity.getMutableComponent(Button);
  37. var buttonMesh = entity.getComponent(Object3D).object;
  38. if (button.currState == 'none') {
  39. buttonMesh.scale.set(1, 1, 1);
  40. } else {
  41. buttonMesh.scale.set(1.1, 1.1, 1.1);
  42. }
  43. if (button.currState == "pressed" && button.prevState != "pressed") {
  44. button.action();
  45. }
  46. // preserve prevState, clear currState
  47. // HandRaySystem will update currState
  48. button.prevState = button.currState;
  49. button.currState = 'none';
  50. });
  51. }
  52. }
  53. ButtonSystem.queries = {
  54. buttons: {
  55. components: [Button]
  56. }
  57. }
  58. class Intersectable extends TagComponent { }
  59. class HandRaySystem extends System {
  60. init(attributes) {
  61. this.handPointers = attributes.handPointers;
  62. }
  63. execute(delta, time) {
  64. this.handPointers.forEach(hp => {
  65. var distance = null;
  66. var intersectingEntity = null;
  67. this.queries.intersectable.results.forEach(entity => {
  68. var object = entity.getComponent(Object3D).object;
  69. let intersections = hp.intersectObject(object);
  70. if (intersections && intersections.length > 0) {
  71. if (distance == null || intersections[0].distance < distance) {
  72. distance = intersections[0].distance;
  73. intersectingEntity = entity;
  74. }
  75. }
  76. });
  77. if (distance) {
  78. hp.setCursor(distance);
  79. if (intersectingEntity.hasComponent(Button)) {
  80. let button = intersectingEntity.getMutableComponent(Button);
  81. if (hp.isPinched()) {
  82. button.currState = 'pressed';
  83. } else if (button.currState != 'pressed') {
  84. button.currState = 'hovered';
  85. }
  86. }
  87. } else {
  88. hp.setCursor(1.5);
  89. }
  90. });
  91. }
  92. }
  93. HandRaySystem.queries = {
  94. intersectable: {
  95. components: [Intersectable]
  96. }
  97. };
  98. class Rotating extends TagComponent { }
  99. class RotatingSystem extends System {
  100. execute(delta, time) {
  101. this.queries.rotatingObjects.results.forEach(entity => {
  102. var object = entity.getComponent(Object3D).object;
  103. object.rotation.x += 0.4 * delta;
  104. object.rotation.y += 0.4 * delta;
  105. });
  106. }
  107. }
  108. RotatingSystem.queries = {
  109. rotatingObjects: {
  110. components: [Rotating]
  111. }
  112. }
  113. class HandsInstructionText extends TagComponent { }
  114. class InstructionSystem extends System {
  115. init(attributes) {
  116. this.controllers = attributes.controllers;
  117. }
  118. execute(delta, time) {
  119. let visible = false;
  120. this.controllers.forEach(controller => {
  121. if (controller.visible) {
  122. visible = true;
  123. }
  124. })
  125. this.queries.instructionTexts.results.forEach(entity => {
  126. var object = entity.getComponent(Object3D).object;
  127. object.visible = visible;
  128. });
  129. }
  130. }
  131. InstructionSystem.queries = {
  132. instructionTexts: {
  133. components: [HandsInstructionText]
  134. }
  135. }
  136. class OffsetFromCamera extends Component { }
  137. OffsetFromCamera.schema = {
  138. x: { type: Types.Number, default: 0 },
  139. y: { type: Types.Number, default: 0 },
  140. z: { type: Types.Number, default: 0 },
  141. }
  142. class NeedCalibration extends TagComponent { }
  143. class CalibrationSystem extends System {
  144. init(attributes) {
  145. this.camera = attributes.camera;
  146. this.renderer = attributes.renderer;
  147. }
  148. execute(delta, time) {
  149. this.queries.needCalibration.results.forEach(entity => {
  150. if (this.renderer.xr.getSession()) {
  151. let offset = entity.getComponent(OffsetFromCamera);
  152. let object = entity.getComponent(Object3D).object;
  153. let xrCamera = renderer.xr.getCamera(this.camera);
  154. object.position.x = xrCamera.position.x + offset.x;
  155. object.position.y = xrCamera.position.y + offset.y;
  156. object.position.z = xrCamera.position.z + offset.z;
  157. entity.removeComponent(NeedCalibration);
  158. }
  159. });
  160. }
  161. }
  162. CalibrationSystem.queries = {
  163. needCalibration: {
  164. components: [NeedCalibration]
  165. }
  166. }
  167. let world = new World();
  168. var clock = new THREE.Clock();
  169. let camera, scene, renderer;
  170. init();
  171. animate();
  172. function makeButtonMesh(x, y, z, color) {
  173. const geometry = new THREE.BoxGeometry(x, y, z);
  174. const material = new THREE.MeshPhongMaterial({ color: color });
  175. const buttonMesh = new THREE.Mesh(geometry, material);
  176. return buttonMesh;
  177. }
  178. function init() {
  179. let container = document.createElement('div');
  180. document.body.appendChild(container);
  181. scene = new THREE.Scene();
  182. scene.background = new THREE.Color(0x444444);
  183. camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 10);
  184. camera.position.set(0, 1.2, 0.3);
  185. scene.add(new THREE.HemisphereLight(0x808080, 0x606060));
  186. const light = new THREE.DirectionalLight(0xffffff);
  187. light.position.set(0, 6, 0);
  188. light.castShadow = true;
  189. light.shadow.camera.top = 2;
  190. light.shadow.camera.bottom = - 2;
  191. light.shadow.camera.right = 2;
  192. light.shadow.camera.left = - 2;
  193. light.shadow.mapSize.set(4096, 4096);
  194. scene.add(light);
  195. renderer = new THREE.WebGLRenderer({ antialias: true });
  196. renderer.setPixelRatio(window.devicePixelRatio);
  197. renderer.setSize(window.innerWidth, window.innerHeight);
  198. renderer.outputEncoding = THREE.sRGBEncoding;
  199. renderer.shadowMap.enabled = true;
  200. renderer.xr.enabled = true;
  201. container.appendChild(renderer.domElement);
  202. document.body.appendChild(VRButton.createButton(renderer));
  203. // controllers
  204. let controller1 = renderer.xr.getController(0);
  205. scene.add(controller1);
  206. let controller2 = renderer.xr.getController(1);
  207. scene.add(controller2);
  208. const controllerModelFactory = new XRControllerModelFactory();
  209. // Hand 1
  210. let controllerGrip1 = renderer.xr.getControllerGrip(0);
  211. controllerGrip1.add(controllerModelFactory.createControllerModel(controllerGrip1));
  212. scene.add(controllerGrip1);
  213. let hand1 = renderer.xr.getHand(0);
  214. hand1.add(new OculusHandModel(hand1));
  215. let handPointer1 = new OculusHandPointerModel(hand1, controller1);
  216. hand1.add(handPointer1);
  217. scene.add(hand1);
  218. // Hand 2
  219. let controllerGrip2 = renderer.xr.getControllerGrip(1);
  220. controllerGrip2.add(controllerModelFactory.createControllerModel(controllerGrip2));
  221. scene.add(controllerGrip2);
  222. let hand2 = renderer.xr.getHand(1);
  223. hand2.add(new OculusHandModel(hand2));
  224. let handPointer2 = new OculusHandPointerModel(hand2, controller2);
  225. hand2.add(handPointer2);
  226. scene.add(hand2);
  227. // setup objects in scene and entities
  228. const floorGeometry = new THREE.PlaneGeometry(4, 4);
  229. const floorMaterial = new THREE.MeshPhongMaterial({ color: 0x222222 });
  230. const floor = new THREE.Mesh(floorGeometry, floorMaterial);
  231. floor.rotation.x = - Math.PI / 2;
  232. scene.add(floor);
  233. const menuGeometry = new THREE.PlaneGeometry(0.24, 0.5);
  234. const menuMaterial = new THREE.MeshPhongMaterial({
  235. opacity: 0,
  236. transparent: true,
  237. });
  238. let menuMesh = new THREE.Mesh(menuGeometry, menuMaterial);
  239. menuMesh.position.set(0.4, 1, -1);
  240. menuMesh.rotation.y = - Math.PI / 12;
  241. scene.add(menuMesh);
  242. let orangeButton = makeButtonMesh(0.2, 0.1, 0.01, 0xffd3b5);
  243. orangeButton.position.set(0, 0.18, 0);
  244. menuMesh.add(orangeButton);
  245. let pinkButton = makeButtonMesh(0.2, 0.1, 0.01, 0xe84a5f);
  246. pinkButton.position.set(0, 0.06, 0);
  247. menuMesh.add(pinkButton);
  248. let resetButton = makeButtonMesh(0.2, 0.1, 0.01, 0x355c7d);
  249. let resetButtonText = createText("reset", 0.06);
  250. resetButton.add(resetButtonText);
  251. resetButtonText.position.set(0, 0, 0.0051);
  252. resetButton.position.set(0, -0.06, 0);
  253. menuMesh.add(resetButton);
  254. let exitButton = makeButtonMesh(0.2, 0.1, 0.01, 0xff0000);
  255. let exitButtonText = createText("exit", 0.06);
  256. exitButton.add(exitButtonText);
  257. exitButtonText.position.set(0, 0, 0.0051);
  258. exitButton.position.set(0, -0.18, 0);
  259. menuMesh.add(exitButton);
  260. let tkGeometry = new THREE.TorusKnotGeometry(0.5, 0.2, 200, 32);
  261. let tkMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff });
  262. tkMaterial.metalness = 0.8;
  263. let torusKnot = new THREE.Mesh(tkGeometry, tkMaterial);
  264. torusKnot.position.set(0, 1, -5);
  265. scene.add(torusKnot);
  266. let instructionText = createText("This is a WebXR Hands demo, please explore with hands.", 0.04);
  267. instructionText.position.set(0, 1.6, -0.6);
  268. scene.add(instructionText);
  269. let exitText = createText("Exiting session...", 0.04);
  270. exitText.position.set(0, 1.5, -0.6);
  271. exitText.visible = false;
  272. scene.add(exitText);
  273. world
  274. .registerComponent(Object3D)
  275. .registerComponent(Button)
  276. .registerComponent(Intersectable)
  277. .registerComponent(Rotating)
  278. .registerComponent(HandsInstructionText)
  279. .registerComponent(OffsetFromCamera)
  280. .registerComponent(NeedCalibration);
  281. world
  282. .registerSystem(RotatingSystem)
  283. .registerSystem(InstructionSystem, { controllers: [controllerGrip1, controllerGrip2] })
  284. .registerSystem(CalibrationSystem, { renderer: renderer, camera: camera })
  285. .registerSystem(ButtonSystem)
  286. .registerSystem(HandRaySystem, { handPointers: [handPointer1, handPointer2] });
  287. var menuEntity = world.createEntity();
  288. menuEntity.addComponent(Intersectable);
  289. menuEntity.addComponent(OffsetFromCamera, { x: 0.4, y: 0, z: -1 });
  290. menuEntity.addComponent(NeedCalibration);
  291. menuEntity.addComponent(Object3D, { object: menuMesh });
  292. var obEntity = world.createEntity();
  293. obEntity.addComponent(Intersectable);
  294. obEntity.addComponent(Object3D, { object: orangeButton });
  295. let obAction = function () { torusKnot.material.color.setHex(0xffd3b5); };
  296. obEntity.addComponent(Button, { action: obAction });
  297. var pbEntity = world.createEntity();
  298. pbEntity.addComponent(Intersectable);
  299. pbEntity.addComponent(Object3D, { object: pinkButton });
  300. let pbAction = function () { torusKnot.material.color.setHex(0xe84a5f); };
  301. pbEntity.addComponent(Button, { action: pbAction });
  302. var rbEntity = world.createEntity();
  303. rbEntity.addComponent(Intersectable);
  304. rbEntity.addComponent(Object3D, { object: resetButton });
  305. let rbAction = function () { torusKnot.material.color.setHex(0xffffff); };
  306. rbEntity.addComponent(Button, { action: rbAction });
  307. var ebEntity = world.createEntity();
  308. ebEntity.addComponent(Intersectable);
  309. ebEntity.addComponent(Object3D, { object: exitButton });
  310. let ebAction = function () {
  311. exitText.visible = true;
  312. setTimeout(function () { exitText.visible = false; renderer.xr.getSession().end(); }, 2000);
  313. };
  314. ebEntity.addComponent(Button, { action: ebAction });
  315. var tkEntity = world.createEntity();
  316. tkEntity.addComponent(Rotating);
  317. tkEntity.addComponent(Object3D, { object: torusKnot });
  318. var itEntity = world.createEntity();
  319. itEntity.addComponent(HandsInstructionText);
  320. itEntity.addComponent(Object3D, { object: instructionText });
  321. window.addEventListener('resize', onWindowResize);
  322. }
  323. function onWindowResize() {
  324. camera.aspect = window.innerWidth / window.innerHeight;
  325. camera.updateProjectionMatrix();
  326. renderer.setSize(window.innerWidth, window.innerHeight);
  327. }
  328. function animate() {
  329. renderer.setAnimationLoop(render);
  330. }
  331. function render() {
  332. var delta = clock.getDelta();
  333. var elapsedTime = clock.elapsedTime;
  334. world.execute(delta, elapsedTime);
  335. renderer.render(scene, camera);
  336. }
  337. </script>
  338. </body>
  339. </html>