webxr_vr_handinput_pointerdrag.html 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <title>three.js webxr hands - point and drag</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 drag<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 Draggable extends Component { }
  59. Draggable.schema = {
  60. // draggable states: [detached, hovered, to-be-attached, attached, to-be-detached]
  61. state: { type: Types.String, default: 'none' },
  62. originalParent: { type: Types.Ref, default: null },
  63. attachedPointer: { type: Types.Ref, default: null }
  64. }
  65. class DraggableSystem extends System {
  66. execute(delta, time) {
  67. this.queries.draggable.results.forEach(entity => {
  68. let draggable = entity.getMutableComponent(Draggable);
  69. let object = entity.getComponent(Object3D).object;
  70. if (draggable.originalParent == null) {
  71. draggable.originalParent = object.parent;
  72. }
  73. switch (draggable.state) {
  74. case 'to-be-attached':
  75. draggable.attachedPointer.children[0].attach(object);
  76. draggable.state = 'attached';
  77. break;
  78. case 'to-be-detached':
  79. draggable.originalParent.attach(object);
  80. draggable.state = 'detached';
  81. break;
  82. default:
  83. object.scale.set(1, 1, 1);
  84. }
  85. });
  86. }
  87. }
  88. DraggableSystem.queries = {
  89. draggable: {
  90. components: [Draggable]
  91. }
  92. }
  93. class Intersectable extends TagComponent { }
  94. class HandRaySystem extends System {
  95. init(attributes) {
  96. this.handPointers = attributes.handPointers;
  97. }
  98. execute(delta, time) {
  99. this.handPointers.forEach(hp => {
  100. var distance = null;
  101. var intersectingEntity = null;
  102. this.queries.intersectable.results.forEach(entity => {
  103. let object = entity.getComponent(Object3D).object;
  104. let intersections = hp.intersectObject(object);
  105. if (intersections && intersections.length > 0) {
  106. if (distance == null || intersections[0].distance < distance) {
  107. distance = intersections[0].distance;
  108. intersectingEntity = entity;
  109. }
  110. }
  111. });
  112. if (distance) {
  113. hp.setCursor(distance);
  114. if (intersectingEntity.hasComponent(Button)) {
  115. let button = intersectingEntity.getMutableComponent(Button);
  116. if (hp.isPinched()) {
  117. button.currState = 'pressed';
  118. } else if (button.currState != 'pressed') {
  119. button.currState = 'hovered';
  120. }
  121. }
  122. if (intersectingEntity.hasComponent(Draggable)) {
  123. let draggable = intersectingEntity.getMutableComponent(Draggable);
  124. let object = intersectingEntity.getComponent(Object3D).object;
  125. object.scale.set(1.1, 1.1, 1.1);
  126. if (hp.isPinched()) {
  127. if (!hp.isAttached() && draggable.state != 'attached') {
  128. draggable.state = 'to-be-attached';
  129. draggable.attachedPointer = hp;
  130. hp.setAttached(true);
  131. }
  132. } else {
  133. if (hp.isAttached() && draggable.state == 'attached') {
  134. console.log('hello');
  135. draggable.state = 'to-be-detached';
  136. draggable.attachedPointer = null;
  137. hp.setAttached(false);
  138. }
  139. }
  140. }
  141. } else {
  142. hp.setCursor(1.5);
  143. }
  144. });
  145. }
  146. }
  147. HandRaySystem.queries = {
  148. intersectable: {
  149. components: [Intersectable]
  150. }
  151. };
  152. class HandsInstructionText extends TagComponent { }
  153. class InstructionSystem extends System {
  154. init(attributes) {
  155. this.controllers = attributes.controllers;
  156. }
  157. execute(delta, time) {
  158. let visible = false;
  159. this.controllers.forEach(controller => {
  160. if (controller.visible) {
  161. visible = true;
  162. }
  163. })
  164. this.queries.instructionTexts.results.forEach(entity => {
  165. var object = entity.getComponent(Object3D).object;
  166. object.visible = visible;
  167. });
  168. }
  169. }
  170. InstructionSystem.queries = {
  171. instructionTexts: {
  172. components: [HandsInstructionText]
  173. }
  174. }
  175. class OffsetFromCamera extends Component { }
  176. OffsetFromCamera.schema = {
  177. x: { type: Types.Number, default: 0 },
  178. y: { type: Types.Number, default: 0 },
  179. z: { type: Types.Number, default: 0 },
  180. }
  181. class NeedCalibration extends TagComponent { }
  182. class CalibrationSystem extends System {
  183. init(attributes) {
  184. this.camera = attributes.camera;
  185. this.renderer = attributes.renderer;
  186. }
  187. execute(delta, time) {
  188. this.queries.needCalibration.results.forEach(entity => {
  189. if (this.renderer.xr.getSession()) {
  190. let offset = entity.getComponent(OffsetFromCamera);
  191. let object = entity.getComponent(Object3D).object;
  192. let xrCamera = renderer.xr.getCamera(this.camera);
  193. object.position.x = xrCamera.position.x + offset.x;
  194. object.position.y = xrCamera.position.y + offset.y;
  195. object.position.z = xrCamera.position.z + offset.z;
  196. entity.removeComponent(NeedCalibration);
  197. }
  198. });
  199. }
  200. }
  201. CalibrationSystem.queries = {
  202. needCalibration: {
  203. components: [NeedCalibration]
  204. }
  205. }
  206. class Randomizable extends TagComponent { }
  207. class RandomizerSystem extends System {
  208. init(attributes) {
  209. this.needRandomizing = true;
  210. }
  211. execute(delta, time) {
  212. if (!this.needRandomizing) { return; }
  213. this.queries.randomizable.results.forEach(entity => {
  214. let object = entity.getComponent(Object3D).object;
  215. object.material.color.setHex(Math.random() * 0xffffff);
  216. object.position.x = Math.random() * 2 - 1;
  217. object.position.y = Math.random() * 2;
  218. object.position.z = Math.random() * 2 - 1;
  219. object.rotation.x = Math.random() * 2 * Math.PI;
  220. object.rotation.y = Math.random() * 2 * Math.PI;
  221. object.rotation.z = Math.random() * 2 * Math.PI;
  222. object.scale.x = Math.random() + 0.5;
  223. object.scale.y = Math.random() + 0.5;
  224. object.scale.z = Math.random() + 0.5;
  225. this.needRandomizing = false;
  226. });
  227. }
  228. }
  229. RandomizerSystem.queries = {
  230. randomizable: {
  231. components: [Randomizable]
  232. }
  233. }
  234. let world = new World();
  235. var clock = new THREE.Clock();
  236. let camera, scene, renderer;
  237. init();
  238. animate();
  239. function makeButtonMesh(x, y, z, color) {
  240. const geometry = new THREE.BoxGeometry(x, y, z);
  241. const material = new THREE.MeshPhongMaterial({ color: color });
  242. const buttonMesh = new THREE.Mesh(geometry, material);
  243. return buttonMesh;
  244. }
  245. function init() {
  246. let container = document.createElement('div');
  247. document.body.appendChild(container);
  248. scene = new THREE.Scene();
  249. scene.background = new THREE.Color(0x444444);
  250. camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 10);
  251. camera.position.set(0, 1.2, 0.3);
  252. scene.add(new THREE.HemisphereLight(0x808080, 0x606060));
  253. const light = new THREE.DirectionalLight(0xffffff);
  254. light.position.set(0, 6, 0);
  255. light.castShadow = true;
  256. light.shadow.camera.top = 2;
  257. light.shadow.camera.bottom = - 2;
  258. light.shadow.camera.right = 2;
  259. light.shadow.camera.left = - 2;
  260. light.shadow.mapSize.set(4096, 4096);
  261. scene.add(light);
  262. renderer = new THREE.WebGLRenderer({ antialias: true });
  263. renderer.setPixelRatio(window.devicePixelRatio);
  264. renderer.setSize(window.innerWidth, window.innerHeight);
  265. renderer.outputEncoding = THREE.sRGBEncoding;
  266. renderer.shadowMap.enabled = true;
  267. renderer.xr.enabled = true;
  268. container.appendChild(renderer.domElement);
  269. document.body.appendChild(VRButton.createButton(renderer));
  270. // controllers
  271. let controller1 = renderer.xr.getController(0);
  272. scene.add(controller1);
  273. let controller2 = renderer.xr.getController(1);
  274. scene.add(controller2);
  275. const controllerModelFactory = new XRControllerModelFactory();
  276. // Hand 1
  277. let controllerGrip1 = renderer.xr.getControllerGrip(0);
  278. controllerGrip1.add(controllerModelFactory.createControllerModel(controllerGrip1));
  279. scene.add(controllerGrip1);
  280. let hand1 = renderer.xr.getHand(0);
  281. hand1.add(new OculusHandModel(hand1));
  282. let handPointer1 = new OculusHandPointerModel(hand1, controller1);
  283. hand1.add(handPointer1);
  284. scene.add(hand1);
  285. // Hand 2
  286. let controllerGrip2 = renderer.xr.getControllerGrip(1);
  287. controllerGrip2.add(controllerModelFactory.createControllerModel(controllerGrip2));
  288. scene.add(controllerGrip2);
  289. let hand2 = renderer.xr.getHand(1);
  290. hand2.add(new OculusHandModel(hand2));
  291. let handPointer2 = new OculusHandPointerModel(hand2, controller2);
  292. hand2.add(handPointer2);
  293. scene.add(hand2);
  294. // setup objects in scene and entities
  295. const floorGeometry = new THREE.PlaneGeometry(4, 4);
  296. const floorMaterial = new THREE.MeshPhongMaterial({ color: 0x222222 });
  297. let floor = new THREE.Mesh(floorGeometry, floorMaterial);
  298. floor.rotation.x = - Math.PI / 2;
  299. scene.add(floor);
  300. const menuGeometry = new THREE.PlaneGeometry(0.24, 0.5);
  301. const menuMaterial = new THREE.MeshPhongMaterial({
  302. opacity: 0,
  303. transparent: true,
  304. });
  305. let menuMesh = new THREE.Mesh(menuGeometry, menuMaterial);
  306. menuMesh.position.set(0.4, 1, -1);
  307. menuMesh.rotation.y = - Math.PI / 12;
  308. scene.add(menuMesh);
  309. let resetButton = makeButtonMesh(0.2, 0.1, 0.01, 0x355c7d);
  310. let resetButtonText = createText("reset", 0.06);
  311. resetButton.add(resetButtonText);
  312. resetButtonText.position.set(0, 0, 0.0051);
  313. resetButton.position.set(0, -0.06, 0);
  314. menuMesh.add(resetButton);
  315. let exitButton = makeButtonMesh(0.2, 0.1, 0.01, 0xff0000);
  316. let exitButtonText = createText("exit", 0.06);
  317. exitButton.add(exitButtonText);
  318. exitButtonText.position.set(0, 0, 0.0051);
  319. exitButton.position.set(0, -0.18, 0);
  320. menuMesh.add(exitButton);
  321. let instructionText = createText("This is a WebXR Hands demo, please explore with hands.", 0.04);
  322. instructionText.position.set(0, 1.6, -0.6);
  323. scene.add(instructionText);
  324. let exitText = createText("Exiting session...", 0.04);
  325. exitText.position.set(0, 1.5, -0.6);
  326. exitText.visible = false;
  327. scene.add(exitText);
  328. world
  329. .registerComponent(Object3D)
  330. .registerComponent(Button)
  331. .registerComponent(Intersectable)
  332. .registerComponent(HandsInstructionText)
  333. .registerComponent(OffsetFromCamera)
  334. .registerComponent(NeedCalibration)
  335. .registerComponent(Randomizable)
  336. .registerComponent(Draggable);
  337. world
  338. .registerSystem(RandomizerSystem)
  339. .registerSystem(InstructionSystem, { controllers: [controllerGrip1, controllerGrip2] })
  340. .registerSystem(CalibrationSystem, { renderer: renderer, camera: camera })
  341. .registerSystem(ButtonSystem)
  342. .registerSystem(DraggableSystem)
  343. .registerSystem(HandRaySystem, { handPointers: [handPointer1, handPointer2] });
  344. for (let i = 0; i < 20; i++) {
  345. const object = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.15, 0.15), new THREE.MeshLambertMaterial({ color: 0xffffff }));
  346. scene.add(object);
  347. let entity = world.createEntity();
  348. entity.addComponent(Intersectable);
  349. entity.addComponent(Randomizable);
  350. entity.addComponent(Object3D, { object: object });
  351. entity.addComponent(Draggable);
  352. }
  353. var menuEntity = world.createEntity();
  354. menuEntity.addComponent(Intersectable);
  355. menuEntity.addComponent(OffsetFromCamera, { x: 0.4, y: 0, z: -1 });
  356. menuEntity.addComponent(NeedCalibration);
  357. menuEntity.addComponent(Object3D, { object: menuMesh });
  358. var rbEntity = world.createEntity();
  359. rbEntity.addComponent(Intersectable);
  360. rbEntity.addComponent(Object3D, { object: resetButton });
  361. let rbAction = function () { world.getSystem(RandomizerSystem).needRandomizing = true; };
  362. rbEntity.addComponent(Button, { action: rbAction });
  363. var ebEntity = world.createEntity();
  364. ebEntity.addComponent(Intersectable);
  365. ebEntity.addComponent(Object3D, { object: exitButton });
  366. let ebAction = function () {
  367. exitText.visible = true;
  368. setTimeout(function () { exitText.visible = false; renderer.xr.getSession().end(); }, 2000);
  369. };
  370. ebEntity.addComponent(Button, { action: ebAction });
  371. var itEntity = world.createEntity();
  372. itEntity.addComponent(HandsInstructionText);
  373. itEntity.addComponent(Object3D, { object: instructionText });
  374. window.addEventListener('resize', onWindowResize);
  375. }
  376. function onWindowResize() {
  377. camera.aspect = window.innerWidth / window.innerHeight;
  378. camera.updateProjectionMatrix();
  379. renderer.setSize(window.innerWidth, window.innerHeight);
  380. }
  381. function animate() {
  382. renderer.setAnimationLoop(render);
  383. }
  384. function render() {
  385. var delta = clock.getDelta();
  386. var elapsedTime = clock.elapsedTime;
  387. world.execute(delta, elapsedTime);
  388. renderer.render(scene, camera);
  389. }
  390. </script>
  391. </body>
  392. </html>