├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── dist ├── client │ ├── demo1.bundle.js │ ├── demo1.bundle.js.LICENSE.txt │ ├── demo1.html │ ├── demo2.bundle.js │ ├── demo2.bundle.js.LICENSE.txt │ ├── demo2.html │ ├── favicon.ico │ ├── grabvr.d.ts │ ├── grabvr.js │ ├── img │ │ ├── grabvr-1.gif │ │ └── grabvr-2.gif │ └── index.html └── server │ ├── server.js │ └── server.js.map ├── package.json ├── src ├── client │ ├── demo1.html │ ├── demo1.ts │ ├── demo2.html │ ├── demo2.ts │ ├── grabvr.ts │ ├── tsconfig.json │ ├── utils │ │ └── cannonDebugRenderer.ts │ ├── webpack.common.js │ ├── webpack.dev.js │ └── webpack.prod.js └── server │ ├── server.ts │ └── tsconfig.json └── threejs-course-image.png /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "embeddedLanguageFormatting": "auto", 5 | "htmlWhitespaceSensitivity": "css", 6 | "insertPragma": false, 7 | "jsxBracketSameLine": false, 8 | "jsxSingleQuote": false, 9 | "printWidth": 100, 10 | "proseWrap": "preserve", 11 | "quoteProps": "as-needed", 12 | "requirePragma": false, 13 | "semi": false, 14 | "singleQuote": true, 15 | "tabWidth": 4, 16 | "trailingComma": "es5", 17 | "useTabs": false 18 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2023 Sean Bradley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GrabVR 2 | 3 | A module for grabbing objects in WebVR Three.js projects 4 | 5 | You can download the project and view the examples. 6 | 7 | ``` bash 8 | git clone https://github.com/Sean-Bradley/GrabVR.git 9 | cd GrabVR 10 | npm install 11 | npm run dev 12 | ``` 13 | 14 | Visit https://127.0.0.1:3000/ 15 | 16 | ## How to import GrabVR 17 | 18 | ```bash 19 | npm install grabvr 20 | ``` 21 | 22 | Import into your code 23 | 24 | ``` javascript 25 | import GrabVR from 'grabvr' 26 | ``` 27 | 28 | ## Instantiate And Use 29 | 30 | Create a GrabVR object. 31 | 32 | ```javascript 33 | const grabVR = new GrabVR() 34 | ``` 35 | 36 | Create some Object3Ds and add then to the GrabVR grabables. 37 | 38 | ```javascript 39 | const box = new THREE.Mesh( 40 | new THREE.BoxGeometry(1.0, 1.0, 1.0), 41 | new THREE.MeshBasicMaterial({ 42 | color: 0xff0066, 43 | wireframe: true 44 | }) 45 | ) 46 | scene.add(box) 47 | grabVR.grabableObjects().push(box); 48 | ``` 49 | 50 | Add your VR controllers to the scene (see example code for better understanding) 51 | 52 | ```javascript 53 | const controllerGrip0 = renderer.xr.getControllerGrip(0) 54 | controllerGrip0.addEventListener("connected", (e: any) => { 55 | controllerGrip0.add(lefthand) 56 | grabVR.add(0, controllerGrip0, e.data.gamepad) 57 | scene.add(controllerGrip0) 58 | }) 59 | ``` 60 | 61 | Update in the render loop. 62 | 63 | ```javascript 64 | grabVR.update(clock.getDelta()); 65 | renderer.render(scene, camera) 66 | ``` 67 | 68 | ## Example 1 69 | 70 | Basic GrabVR demo. 71 | 72 | [![GrabVR Example 1](./dist/client/img/grabvr-1.gif)](https://sbcode.net/threejs/grabvr-1/) 73 | 74 | ## Example 2 75 | 76 | GrabVR demo using Cannonjs. 77 | 78 | [![GrabVR Example 2](./dist/client/img/grabvr-2.gif)](https://sbcode.net/threejs/grabvr-2/) 79 | 80 | ## GrabVR Source Project 81 | 82 | This is a typescript project consisting of two sub projects with there own *tsconfigs*. 83 | 84 | To edit this example, then modify the files in `./src/client/` or `./src/server/` 85 | 86 | The projects will auto recompile if you started it by using *npm run dev* 87 | 88 | ## Threejs TypeScript Course 89 | 90 | Visit https://github.com/Sean-Bradley/Three.js-TypeScript-Boilerplate for a Threejs TypeScript boilerplate containing many extra branches that demonstrate many examples of Threejs. 91 | 92 | > To help support this Threejs example, please take a moment to look at my official Threejs TypeScript course at 93 | 94 | [![Threejs TypeScript Course](threejs-course-image.png)](https://www.udemy.com/course/threejs-tutorials/?referralCode=4C7E1DE91C3E42F69D0F) 95 | 96 | [Three.js and TypeScript](https://www.udemy.com/course/threejs-tutorials/?referralCode=4C7E1DE91C3E42F69D0F)
97 | Discount Coupons for all my courses can be found at [https://sbcode.net/coupons](https://sbcode.net/coupons) 98 | -------------------------------------------------------------------------------- /dist/client/demo1.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2010-2022 Three.js Authors 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | /** 8 | * @license 9 | * GrabVR library and demos 10 | * Copyright 2018-2023 Sean Bradley https://sbcode.net 11 | * https://github.com/Sean-Bradley/GrabVR/blob/master/LICENSE 12 | */ 13 | 14 | /** 15 | * @license 16 | * StatsVR library and demos 17 | * Copyright 2018-2023 Sean Bradley 18 | * https://github.com/Sean-Bradley/StatsVR/blob/master/LICENSE 19 | */ 20 | -------------------------------------------------------------------------------- /dist/client/demo1.html: -------------------------------------------------------------------------------- 1 | GrabVR - Demo 1 -------------------------------------------------------------------------------- /dist/client/demo2.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2010-2022 Three.js Authors 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | /** 8 | * @license 9 | * GrabVR library and demos 10 | * Copyright 2018-2023 Sean Bradley https://sbcode.net 11 | * https://github.com/Sean-Bradley/GrabVR/blob/master/LICENSE 12 | */ 13 | 14 | /** 15 | * @license 16 | * StatsVR library and demos 17 | * Copyright 2018-2023 Sean Bradley 18 | * https://github.com/Sean-Bradley/StatsVR/blob/master/LICENSE 19 | */ 20 | -------------------------------------------------------------------------------- /dist/client/demo2.html: -------------------------------------------------------------------------------- 1 | GrabVR - Demo 2 -------------------------------------------------------------------------------- /dist/client/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Bradley/GrabVR/d267df987d9bceefffc5d0816cc528fcd1daf4d0/dist/client/favicon.ico -------------------------------------------------------------------------------- /dist/client/grabvr.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * GrabVR library and demos 4 | * Copyright 2018-2023 Sean Bradley https://sbcode.net 5 | * https://github.com/Sean-Bradley/GrabVR/blob/master/LICENSE 6 | */ 7 | import * as THREE from "three"; 8 | export default class GrabVR { 9 | private _controller; 10 | private _raycaster; 11 | private _quaternion; 12 | private _grabbedObject; 13 | private _hasAGrabbedObject; 14 | private _line; 15 | private _grabberHook; 16 | private _gamepad; 17 | private _grabableObjects; 18 | private _direction; 19 | private _eventListeners; 20 | constructor(); 21 | grabableObjects(): THREE.Object3D[]; 22 | add(id: number, o: THREE.Object3D, gamepad: Gamepad): void; 23 | update(dt: number): void; 24 | addEventListener(type: string, eventHandler: any): void; 25 | dispatchEvent(type: string, id: number): void; 26 | } 27 | -------------------------------------------------------------------------------- /dist/client/grabvr.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * @license 4 | * GrabVR library and demos 5 | * Copyright 2018-2023 Sean Bradley https://sbcode.net 6 | * https://github.com/Sean-Bradley/GrabVR/blob/master/LICENSE 7 | */ 8 | Object.defineProperty(exports, "__esModule", { value: true }); 9 | var THREE = require("three"); 10 | var GrabVR = /** @class */ (function () { 11 | function GrabVR() { 12 | this._controller = {}; 13 | this._raycaster = {}; 14 | this._quaternion = {}; 15 | this._grabbedObject = {}; 16 | this._hasAGrabbedObject = {}; 17 | this._line = {}; 18 | this._grabberHook = {}; 19 | this._gamepad = {}; 20 | this._grabableObjects = []; 21 | this._direction = new THREE.Vector3(0, -1, 0); 22 | this._eventListeners = new Array(); 23 | } 24 | GrabVR.prototype.grabableObjects = function () { 25 | return this._grabableObjects; 26 | }; 27 | GrabVR.prototype.add = function (id, o, gamepad) { 28 | this._controller[id] = o; 29 | this._raycaster[id] = new THREE.Raycaster(); 30 | this._quaternion[id] = new THREE.Quaternion(); 31 | this._gamepad[id] = gamepad; 32 | var points = []; 33 | points.push(new THREE.Vector3(0, 0, 0)); 34 | points.push(new THREE.Vector3(0, -100, 0)); 35 | var geometry = new THREE.BufferGeometry().setFromPoints(points); 36 | this._line[id] = new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 0x8888ff })); 37 | this._line[id].visible = false; 38 | o.add(this._line[id]); 39 | this._grabberHook[id] = new THREE.Mesh(new THREE.SphereGeometry(0.1, 6, 6), new THREE.MeshBasicMaterial({ 40 | color: 0x00ff00, 41 | wireframe: false, 42 | depthTest: false, 43 | depthWrite: false, 44 | transparent: true, 45 | opacity: 0.5, 46 | })); 47 | o.add(this._grabberHook[id]); 48 | this._grabberHook[id].visible = false; 49 | }; 50 | GrabVR.prototype.update = function (dt) { 51 | for (var key in Object.keys(this._controller)) { 52 | this._controller[key].getWorldPosition(this._raycaster[key].ray.origin); 53 | this._controller[key].getWorldQuaternion(this._quaternion[key]); 54 | this._raycaster[key].ray.direction 55 | .copy(this._direction) 56 | .applyEuler(new THREE.Euler().setFromQuaternion(this._quaternion[key], "XYZ")); 57 | var intersects = this._raycaster[key].intersectObjects(this._grabableObjects); 58 | if (intersects.length > 0) { 59 | this._line[key].visible = true; 60 | if (this._gamepad[key].buttons[1].pressed) { 61 | if (!this._hasAGrabbedObject[key]) { 62 | this._grabberHook[key].position.copy(this._controller[key].worldToLocal(intersects[0].object.position)); 63 | this._grabbedObject[key] = intersects[0].object; 64 | this._grabbedObject[key].userData.isGrabbed = true; 65 | this._grabberHook[key].visible = true; 66 | this._hasAGrabbedObject[key] = true; 67 | this.dispatchEvent("grabStart", Number(key)); 68 | } 69 | if (this._gamepad[key].axes[3] > 0.25) { 70 | if (this._grabberHook[key].position.y <= -1) { 71 | this._grabberHook[key].translateY(this._gamepad[key].axes[3] * dt * 10); 72 | } 73 | } 74 | else { 75 | this._grabberHook[key].translateY(this._gamepad[key].axes[3] * dt * 10); 76 | } 77 | this.dispatchEvent("grabMove", Number(key)); 78 | } 79 | } 80 | else { 81 | this._line[key].visible = false; 82 | } 83 | if (!this._gamepad[key].buttons[1].pressed) { 84 | if (this._hasAGrabbedObject[key]) { 85 | if (this._hasAGrabbedObject[key]) { 86 | this._hasAGrabbedObject[key] = false; 87 | this._grabberHook[key].visible = false; 88 | this._grabbedObject[key].userData.isGrabbed = false; 89 | this.dispatchEvent("grabEnd", Number(key)); 90 | } 91 | } 92 | } 93 | if (this._hasAGrabbedObject[key]) { 94 | this._grabberHook[key].getWorldPosition(this._grabbedObject[key].position); 95 | } 96 | } 97 | }; 98 | GrabVR.prototype.addEventListener = function (type, eventHandler) { 99 | var listener = { type: type, eventHandler: eventHandler }; 100 | this._eventListeners.push(listener); 101 | }; 102 | GrabVR.prototype.dispatchEvent = function (type, id) { 103 | for (var i = 0; i < this._eventListeners.length; i++) { 104 | if (type === this._eventListeners[i].type) { 105 | this._eventListeners[i].eventHandler(id); 106 | } 107 | } 108 | }; 109 | return GrabVR; 110 | }()); 111 | exports.default = GrabVR; 112 | -------------------------------------------------------------------------------- /dist/client/img/grabvr-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Bradley/GrabVR/d267df987d9bceefffc5d0816cc528fcd1daf4d0/dist/client/img/grabvr-1.gif -------------------------------------------------------------------------------- /dist/client/img/grabvr-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Bradley/GrabVR/d267df987d9bceefffc5d0816cc528fcd1daf4d0/dist/client/img/grabvr-2.gif -------------------------------------------------------------------------------- /dist/client/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | SeanWasEre Youtube - WebVR THREE.js WebGL Demos using GrabVR 6 | 7 | 8 | 42 | 43 | 44 | 45 |
46 |

GrabVR Demos

47 |
48 |
49 |

Demo 1

50 |

GrabVR Basic Usage Demo.

51 |
52 | 53 | 54 | 55 |
56 |
57 | View Demo 1 58 |
59 |
60 |
61 |

Demo 2

62 |

GrabVR Demo using Cannonjs.

63 |
64 | 65 | 66 | 67 |
68 |
69 | View Demo 2 70 |
71 |
72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /dist/server/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const express_1 = __importDefault(require("express")); 7 | const path_1 = __importDefault(require("path")); 8 | const http_1 = __importDefault(require("http")); 9 | const port = 3000; 10 | class App { 11 | constructor(port) { 12 | this.port = port; 13 | const app = express_1.default(); 14 | app.use(express_1.default.static(path_1.default.join(__dirname, '../client'))); 15 | // In the webpack version of the boilerplate, it is not necessary 16 | // to add static references to the libs in node_modules if 17 | // you are using module specifiers in your client.ts imports. 18 | // 19 | // Visit https://sbcode.net/threejs/module-specifiers/ for info about module specifiers 20 | // 21 | // This server.ts is only useful if you are running this on a production server or you 22 | // want to see how the production version of bundle.js works 23 | // 24 | // to use this server.ts 25 | // # npm run build (this creates the production version of bundle.js and places it in ./dist/client/) 26 | // # tsc -p ./src/server (this compiles ./src/server/server.ts into ./dist/server/server.js) 27 | // # npm start (this starts nodejs with express and serves the ./dist/client folder) 28 | // 29 | // visit http://127.0.0.1:3000 30 | this.server = new http_1.default.Server(app); 31 | } 32 | Start() { 33 | this.server.listen(this.port, () => { 34 | console.log(`Server listening on port ${this.port}.`); 35 | }); 36 | } 37 | } 38 | new App(port).Start(); 39 | //# sourceMappingURL=server.js.map -------------------------------------------------------------------------------- /dist/server/server.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":";;;;;AAAA,sDAA6B;AAC7B,gDAAuB;AACvB,gDAAuB;AAEvB,MAAM,IAAI,GAAW,IAAI,CAAA;AAEzB,MAAM,GAAG;IAIL,YAAY,IAAY;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,MAAM,GAAG,GAAG,iBAAO,EAAE,CAAA;QACrB,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,MAAM,CAAC,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC,CAAC,CAAA;QAC1D,kEAAkE;QAClE,2DAA2D;QAC3D,8DAA8D;QAC9D,EAAE;QACF,uFAAuF;QACvF,EAAE;QACF,sFAAsF;QACtF,4DAA4D;QAC5D,GAAG;QACH,wBAAwB;QACxB,4GAA4G;QAC5G,6FAA6F;QAC7F,+FAA+F;QAC/F,GAAG;QACH,8BAA8B;QAE9B,IAAI,CAAC,MAAM,GAAG,IAAI,cAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACvC,CAAC;IAEM,KAAK;QACR,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE;YAC/B,OAAO,CAAC,GAAG,CAAE,4BAA4B,IAAI,CAAC,IAAI,GAAG,CAAE,CAAA;QAC3D,CAAC,CAAC,CAAA;IACN,CAAC;CACJ;AAED,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAA"} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grabvr", 3 | "version": "3.0.5", 4 | "description": "A module for grabbing objects in WebXR Three.js projects", 5 | "main": "./dist/client/grabvr.js", 6 | "scripts": { 7 | "build": "tsc ./src/client/grabvr.ts --outDir ./dist/client/", 8 | "dev": "webpack serve --config ./src/client/webpack.dev.js", 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "start": "node ./dist/server/server.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/Sean-Bradley/GrabVR.git" 15 | }, 16 | "keywords": [ 17 | "grabvr", 18 | "threejs", 19 | "webvr", 20 | "webxr", 21 | "seanwasere", 22 | "statsvr" 23 | ], 24 | "author": "Sean Bradley", 25 | "license": "MIT", 26 | "homepage": "https://github.com/Sean-Bradley/GrabVR#readme", 27 | "devDependencies": { 28 | "@types/express": "^4.17.21", 29 | "@types/node": "^20.11.30", 30 | "@types/three": "^0.162.0", 31 | "cannon-es": "^0.20.0", 32 | "html-webpack-plugin": "^5.6.0", 33 | "statsvr": "latest", 34 | "three": "^0.162.0", 35 | "ts-loader": "^9.5.1", 36 | "typescript": "^5.4.3", 37 | "webpack": "^5.91.0", 38 | "webpack-cli": "^5.1.4", 39 | "webpack-dev-server": "^5.0.4", 40 | "webpack-merge": "^5.10.0" 41 | }, 42 | "dependencies": { 43 | "express": "^4.19.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/client/demo1.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | GrabVR - Demo 1 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/client/demo1.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import StatsVR from 'statsvr' 3 | import { VRButton } from 'three/examples/jsm/webxr/VRButton' 4 | import GrabVR from './grabvr' 5 | 6 | const scene: THREE.Scene = new THREE.Scene() 7 | 8 | const camera: THREE.PerspectiveCamera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000) 9 | camera.position.set(0, 1.6, 3); 10 | 11 | const renderer = new THREE.WebGLRenderer({ antialias: true }) 12 | renderer.setPixelRatio(window.devicePixelRatio) 13 | renderer.setSize(window.innerWidth, window.innerHeight) 14 | renderer.xr.enabled = true 15 | document.body.appendChild(renderer.domElement) 16 | 17 | document.body.appendChild(VRButton.createButton(renderer)) 18 | 19 | window.addEventListener('resize', onWindowResize, false) 20 | 21 | function onWindowResize() { 22 | camera.aspect = window.innerWidth / window.innerHeight 23 | camera.updateProjectionMatrix() 24 | renderer.setSize(window.innerWidth, window.innerHeight) 25 | } 26 | 27 | const grabVR = new GrabVR() 28 | 29 | //grabable objects 30 | // cubes 31 | for (var i = 0; i < 20; i++) { 32 | let grabable = new THREE.Mesh( 33 | new THREE.BoxGeometry(1.0, 1.0, 1.0), 34 | new THREE.MeshBasicMaterial({ 35 | color: 0xff0066, 36 | wireframe: true 37 | }) 38 | ) 39 | grabable.position.x = (Math.random() * 20) - 10 40 | grabable.position.y = (Math.random() * 20) - 10 41 | grabable.position.z = (Math.random() * 20) - 10 42 | 43 | grabVR.grabableObjects().push(grabable); 44 | 45 | scene.add(grabable); 46 | } 47 | // spheres 48 | for (var i = 0; i < 20; i++) { 49 | let grabable = new THREE.Mesh( 50 | new THREE.SphereGeometry(.5, 6, 6), 51 | new THREE.MeshBasicMaterial({ 52 | color: 0x0099ff, 53 | wireframe: true 54 | }) 55 | ) 56 | grabable.position.x = (Math.random() * 20) - 10 57 | grabable.position.y = (Math.random() * 20) - 10 58 | grabable.position.z = (Math.random() * 20) - 10 59 | 60 | grabVR.grabableObjects().push(grabable); 61 | 62 | scene.add(grabable); 63 | } 64 | // cones 65 | for (var i = 0; i < 20; i++) { 66 | let grabable = new THREE.Mesh( 67 | new THREE.CylinderGeometry(0, 1, 1, 5), 68 | new THREE.MeshBasicMaterial({ 69 | color: 0xffcc00, 70 | wireframe: true 71 | }) 72 | ) 73 | grabable.position.x = (Math.random() * 20) - 10 74 | grabable.position.y = (Math.random() * 20) - 10 75 | grabable.position.z = (Math.random() * 20) - 10 76 | 77 | grabVR.grabableObjects().push(grabable); 78 | 79 | scene.add(grabable); 80 | } 81 | 82 | 83 | const lefthand = new THREE.Mesh( 84 | new THREE.CylinderGeometry(.05, 0.05, .4, 16, 1, true), 85 | new THREE.MeshBasicMaterial({ 86 | color: 0x00ff88, 87 | wireframe: true 88 | }) 89 | ) 90 | 91 | const controllerGrip0 = renderer.xr.getControllerGrip(0) 92 | controllerGrip0.addEventListener("connected", (e: any) => { 93 | controllerGrip0.add(lefthand) 94 | grabVR.add(0, controllerGrip0, e.data.gamepad) 95 | scene.add(controllerGrip0) 96 | }) 97 | // controllerGrip0.addEventListener('grabStart', function (event) { 98 | // console.log("in grabStart event handler") 99 | // }) 100 | 101 | const righthand = new THREE.Mesh( 102 | new THREE.CylinderGeometry(.05, 0.05, .4, 16, 1, true), 103 | new THREE.MeshBasicMaterial({ 104 | color: 0x00ff88, 105 | wireframe: true 106 | }) 107 | ) 108 | const controllerGrip1 = renderer.xr.getControllerGrip(1) 109 | controllerGrip1.addEventListener("connected", (e: any) => { 110 | controllerGrip1.add(righthand) 111 | grabVR.add(1, controllerGrip1, e.data.gamepad) 112 | scene.add(controllerGrip1) 113 | }) 114 | 115 | const statsVR = new StatsVR(scene, camera) 116 | statsVR.setX(0) 117 | statsVR.setY(0) 118 | statsVR.setZ(-2) 119 | 120 | const clock: THREE.Clock = new THREE.Clock() 121 | 122 | function render() { 123 | 124 | statsVR.update() 125 | 126 | grabVR.update(clock.getDelta()); 127 | 128 | renderer.render(scene, camera) 129 | 130 | } 131 | 132 | renderer.setAnimationLoop(render) -------------------------------------------------------------------------------- /src/client/demo2.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | GrabVR - Demo 2 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/client/demo2.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import StatsVR from 'statsvr' 3 | import { VRButton } from 'three/examples/jsm/webxr/VRButton' 4 | import GrabVR from './grabvr' 5 | import * as CANNON from 'cannon-es' 6 | //import CannonDebugRenderer from './utils/cannonDebugRenderer' 7 | 8 | const scene: THREE.Scene = new THREE.Scene() 9 | 10 | const camera: THREE.PerspectiveCamera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000) 11 | camera.position.set(0, 1.6, 3); 12 | 13 | const renderer = new THREE.WebGLRenderer({ antialias: true }) 14 | renderer.setPixelRatio(window.devicePixelRatio) 15 | renderer.setSize(window.innerWidth, window.innerHeight) 16 | renderer.xr.enabled = true 17 | document.body.appendChild(renderer.domElement) 18 | 19 | document.body.appendChild(VRButton.createButton(renderer)) 20 | 21 | window.addEventListener('resize', onWindowResize, false) 22 | 23 | function onWindowResize() { 24 | camera.aspect = window.innerWidth / window.innerHeight 25 | camera.updateProjectionMatrix() 26 | renderer.setSize(window.innerWidth, window.innerHeight) 27 | } 28 | 29 | const world = new CANNON.World() 30 | world.gravity.set(0, -9.82, 0) 31 | //world.broadphase = new CANNON.NaiveBroadphase() 32 | //world.solver.iterations = 10 33 | //world.allowSleep = true 34 | 35 | const meshes: THREE.Mesh[] = [] 36 | const bodies: CANNON.Body[] = [] 37 | 38 | const grabVR = new GrabVR() 39 | grabVR.addEventListener("grabStart", (id: number) => { console.log("grabStart " + id) }) 40 | grabVR.addEventListener("grabEnd", (id: number) => { console.log("grabEnd " + id) }) 41 | grabVR.addEventListener("grabMove", (id: number) => { console.log("grabMove " + id) }) 42 | 43 | 44 | //floor 45 | const planeGeometry: THREE.PlaneGeometry = new THREE.PlaneGeometry(25, 25, 10, 10) 46 | const planeMesh: THREE.Mesh = new THREE.Mesh(planeGeometry, new THREE.MeshBasicMaterial({ 47 | color: 0x008800, 48 | wireframe: true 49 | })) 50 | planeMesh.rotateX(-Math.PI / 2) 51 | scene.add(planeMesh) 52 | const planeShape = new CANNON.Plane() 53 | const planeBody = new CANNON.Body({ mass: 0 }) 54 | planeBody.addShape(planeShape) 55 | planeBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2) 56 | world.addBody(planeBody) 57 | 58 | 59 | // cubes 60 | for (var i = 0; i < 20; i++) { 61 | const grabable = new THREE.Mesh( 62 | new THREE.BoxGeometry(1.0, 1.0, 1.0), 63 | new THREE.MeshBasicMaterial({ 64 | color: 0xff0066, 65 | wireframe: true 66 | }) 67 | ) 68 | grabable.position.x = (Math.random() * 20) - 10 69 | grabable.position.y = (Math.random() * 20) + 2 70 | grabable.position.z = (Math.random() * 20) - 10 71 | grabable.userData.isGrabbed = false 72 | 73 | grabVR.grabableObjects().push(grabable); 74 | 75 | scene.add(grabable); 76 | 77 | const cubeShape = new CANNON.Box(new CANNON.Vec3(.5, .5, .5)) 78 | const cubeBody = new CANNON.Body({ mass: 1 }); 79 | cubeBody.addShape(cubeShape) 80 | cubeBody.position.x = grabable.position.x 81 | cubeBody.position.y = grabable.position.y 82 | cubeBody.position.z = grabable.position.z 83 | world.addBody(cubeBody) 84 | 85 | meshes.push(grabable) 86 | bodies.push(cubeBody) 87 | } 88 | // spheres 89 | for (var i = 0; i < 20; i++) { 90 | const grabable = new THREE.Mesh( 91 | new THREE.SphereGeometry(.5, 6, 6), 92 | new THREE.MeshBasicMaterial({ 93 | color: 0x0099ff, 94 | wireframe: true 95 | }) 96 | ) 97 | grabable.position.x = (Math.random() * 20) - 10 98 | grabable.position.y = (Math.random() * 20) + 2 99 | grabable.position.z = (Math.random() * 20) - 10 100 | grabable.userData.isGrabbed = false 101 | 102 | grabVR.grabableObjects().push(grabable); 103 | 104 | scene.add(grabable); 105 | 106 | const sphereShape = new CANNON.Sphere(.5) 107 | const sphereBody = new CANNON.Body({ mass: 1 }); 108 | sphereBody.addShape(sphereShape) 109 | sphereBody.position.x = grabable.position.x 110 | sphereBody.position.y = grabable.position.y 111 | sphereBody.position.z = grabable.position.z 112 | world.addBody(sphereBody) 113 | 114 | meshes.push(grabable) 115 | bodies.push(sphereBody) 116 | } 117 | // cones 118 | for (var i = 0; i < 20; i++) { 119 | const grabable = new THREE.Mesh( 120 | new THREE.CylinderGeometry(0, 1, 1, 5), 121 | new THREE.MeshBasicMaterial({ 122 | color: 0xffcc00, 123 | wireframe: true 124 | }) 125 | ) 126 | grabable.position.x = (Math.random() * 20) - 10 127 | grabable.position.y = (Math.random() * 20) + 2 128 | grabable.position.z = (Math.random() * 20) - 10 129 | grabable.userData.isGrabbed = false 130 | 131 | grabVR.grabableObjects().push(grabable); 132 | 133 | scene.add(grabable); 134 | 135 | const cylinderShape = new CANNON.Cylinder(.01, 1, 1, 5) 136 | const cylinderBody = new CANNON.Body({ mass: 1 }); 137 | const cylinderQuaternion = new CANNON.Quaternion() 138 | cylinderQuaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2) 139 | cylinderBody.addShape(cylinderShape, new CANNON.Vec3, cylinderQuaternion) 140 | cylinderBody.position.x = grabable.position.x 141 | cylinderBody.position.y = grabable.position.y 142 | cylinderBody.position.z = grabable.position.z 143 | world.addBody(cylinderBody) 144 | 145 | meshes.push(grabable) 146 | bodies.push(cylinderBody) 147 | } 148 | 149 | const lefthand = new THREE.Mesh( 150 | new THREE.CylinderGeometry(.05, 0.05, .4, 16, 1, true), 151 | new THREE.MeshBasicMaterial({ 152 | color: 0x00ff88, 153 | wireframe: true 154 | }) 155 | ) 156 | 157 | const controllerGrip0 = renderer.xr.getControllerGrip(0) 158 | controllerGrip0.addEventListener('connected', (e: any) => { 159 | controllerGrip0.add(lefthand) 160 | grabVR.add(0, controllerGrip0, e.data.gamepad) 161 | scene.add(controllerGrip0) 162 | }) 163 | 164 | const righthand = new THREE.Mesh( 165 | new THREE.CylinderGeometry(.05, 0.05, .4, 16, 1, true), 166 | new THREE.MeshBasicMaterial({ 167 | color: 0x00ff88, 168 | wireframe: true 169 | }) 170 | ) 171 | const controllerGrip1 = renderer.xr.getControllerGrip(1) 172 | controllerGrip1.addEventListener('connected', (e: any) => { 173 | controllerGrip1.add(righthand) 174 | grabVR.add(1, controllerGrip1, e.data.gamepad) 175 | scene.add(controllerGrip1) 176 | }) 177 | 178 | const statsVR = new StatsVR(scene, camera) 179 | statsVR.setX(0) 180 | statsVR.setY(0) 181 | statsVR.setZ(-2) 182 | 183 | const clock: THREE.Clock = new THREE.Clock() 184 | 185 | //const cannonDebugRenderer = new CannonDebugRenderer(scene, world) 186 | 187 | function render() { 188 | 189 | statsVR.update() 190 | 191 | let delta = clock.getDelta() 192 | if (delta > .01) delta = .01 193 | world.step(delta) 194 | //cannonDebugRenderer.update() 195 | 196 | 197 | meshes.forEach((m, i) => { 198 | if (!m.userData.isGrabbed) { 199 | m.position.set(bodies[i].position.x, bodies[i].position.y, bodies[i].position.z) 200 | m.quaternion.set(bodies[i].quaternion.x, bodies[i].quaternion.y, bodies[i].quaternion.z, bodies[i].quaternion.w) 201 | } else { 202 | bodies[i].position.x = m.position.x 203 | bodies[i].position.y = m.position.y 204 | bodies[i].position.z = m.position.z 205 | bodies[i].quaternion.x = m.quaternion.x 206 | bodies[i].quaternion.y = m.quaternion.y 207 | bodies[i].quaternion.z = m.quaternion.z 208 | bodies[i].quaternion.w = m.quaternion.w 209 | bodies[i].velocity.set(0, 0, 0); 210 | bodies[i].angularVelocity.set(0, 0, 0); 211 | } 212 | }) 213 | 214 | grabVR.update(delta) 215 | 216 | renderer.render(scene, camera) 217 | 218 | } 219 | 220 | renderer.setAnimationLoop(render) -------------------------------------------------------------------------------- /src/client/grabvr.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * GrabVR library and demos 4 | * Copyright 2018-2023 Sean Bradley https://sbcode.net 5 | * https://github.com/Sean-Bradley/GrabVR/blob/master/LICENSE 6 | */ 7 | 8 | import * as THREE from "three"; 9 | 10 | export default class GrabVR { 11 | private _controller: { [id: number]: THREE.Object3D } = {}; 12 | private _raycaster: { [id: number]: THREE.Raycaster } = {}; 13 | private _quaternion: { [id: number]: THREE.Quaternion } = {}; 14 | private _grabbedObject: { [id: number]: THREE.Object3D } = {}; 15 | private _hasAGrabbedObject: { [id: number]: Boolean } = {}; 16 | private _line: { [id: number]: THREE.Object3D } = {}; 17 | private _grabberHook: { [id: number]: THREE.Mesh } = {}; 18 | private _gamepad: { [id: number]: Gamepad } = {}; 19 | private _grabableObjects: THREE.Object3D[] = []; 20 | private _direction = new THREE.Vector3(0, -1, 0); 21 | private _eventListeners: any[] = new Array(); 22 | 23 | constructor() {} 24 | 25 | public grabableObjects() { 26 | return this._grabableObjects; 27 | } 28 | 29 | public add(id: number, o: THREE.Object3D, gamepad: Gamepad) { 30 | this._controller[id] = o; 31 | this._raycaster[id] = new THREE.Raycaster(); 32 | this._quaternion[id] = new THREE.Quaternion(); 33 | this._gamepad[id] = gamepad; 34 | 35 | const points = []; 36 | points.push(new THREE.Vector3(0, 0, 0)); 37 | points.push(new THREE.Vector3(0, -100, 0)); 38 | let geometry = new THREE.BufferGeometry().setFromPoints(points); 39 | this._line[id] = new THREE.Line( 40 | geometry, 41 | new THREE.LineBasicMaterial({ color: 0x8888ff }) 42 | ); 43 | this._line[id].visible = false; 44 | o.add(this._line[id]); 45 | 46 | this._grabberHook[id] = new THREE.Mesh( 47 | new THREE.SphereGeometry(0.1, 6, 6), 48 | new THREE.MeshBasicMaterial({ 49 | color: 0x00ff00, 50 | wireframe: false, 51 | depthTest: false, 52 | depthWrite: false, 53 | transparent: true, 54 | opacity: 0.5, 55 | }) 56 | ); 57 | 58 | o.add(this._grabberHook[id]); 59 | this._grabberHook[id].visible = false; 60 | } 61 | 62 | public update(dt: number) { 63 | for (let key in Object.keys(this._controller)) { 64 | this._controller[key].getWorldPosition(this._raycaster[key].ray.origin); 65 | this._controller[key].getWorldQuaternion(this._quaternion[key]); 66 | this._raycaster[key].ray.direction 67 | .copy(this._direction) 68 | .applyEuler( 69 | new THREE.Euler().setFromQuaternion(this._quaternion[key], "XYZ") 70 | ); 71 | 72 | let intersects = this._raycaster[key].intersectObjects( 73 | this._grabableObjects 74 | ); 75 | if (intersects.length > 0) { 76 | this._line[key].visible = true; 77 | 78 | if (this._gamepad[key].buttons[1].pressed) { 79 | if (!this._hasAGrabbedObject[key]) { 80 | this._grabberHook[key].position.copy( 81 | this._controller[key].worldToLocal(intersects[0].object.position) 82 | ); 83 | this._grabbedObject[key] = intersects[0].object; 84 | this._grabbedObject[key].userData.isGrabbed = true; 85 | this._grabberHook[key].visible = true; 86 | this._hasAGrabbedObject[key] = true; 87 | this.dispatchEvent("grabStart", Number(key)); 88 | } 89 | 90 | if (this._gamepad[key].axes[3] > 0.25) { 91 | if (this._grabberHook[key].position.y <= -1) { 92 | this._grabberHook[key].translateY( 93 | this._gamepad[key].axes[3] * dt * 10 94 | ); 95 | } 96 | } else { 97 | this._grabberHook[key].translateY( 98 | this._gamepad[key].axes[3] * dt * 10 99 | ); 100 | } 101 | 102 | this.dispatchEvent("grabMove", Number(key)); 103 | } 104 | } else { 105 | this._line[key].visible = false; 106 | } 107 | 108 | if (!this._gamepad[key].buttons[1].pressed) { 109 | if (this._hasAGrabbedObject[key]) { 110 | if (this._hasAGrabbedObject[key]) { 111 | this._hasAGrabbedObject[key] = false; 112 | this._grabberHook[key].visible = false; 113 | this._grabbedObject[key].userData.isGrabbed = false; 114 | this.dispatchEvent("grabEnd", Number(key)); 115 | } 116 | } 117 | } 118 | 119 | if (this._hasAGrabbedObject[key]) { 120 | this._grabberHook[key].getWorldPosition( 121 | this._grabbedObject[key].position 122 | ); 123 | } 124 | } 125 | } 126 | 127 | public addEventListener(type: string, eventHandler: any) { 128 | const listener = { type: type, eventHandler: eventHandler }; 129 | this._eventListeners.push(listener); 130 | } 131 | 132 | public dispatchEvent(type: string, id: number) { 133 | for (let i = 0; i < this._eventListeners.length; i++) { 134 | if (type === this._eventListeners[i].type) { 135 | this._eventListeners[i].eventHandler(id); 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "ES6", 5 | "outDir": "../../dist/client", 6 | "baseUrl": ".", 7 | "moduleResolution": "node", 8 | "strict": true 9 | }, 10 | "include": [ 11 | "**/*.ts" 12 | ] 13 | } -------------------------------------------------------------------------------- /src/client/utils/cannonDebugRenderer.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Original file https://github.com/schteppe/cannon.js/blob/908aa1e954b54d05a43dd708584e882dfe30ae29/tools/threejs/CannonDebugRenderer.js CopyRight https://github.com/schteppe 3 | // Differences Copyright 2020-2023 Sean Bradley : https://sbcode.net/threejs/ 4 | // - Added import statements for THREE 5 | // - Converted to a class with a default export, 6 | // - Converted to TypeScript 7 | // - Updated to support THREE.BufferGeometry (r125) 8 | // - added support for CANNON.Cylinder 9 | // - Updated to use cannon-es 10 | 11 | import * as THREE from 'three' 12 | import * as CANNON from 'cannon-es' 13 | 14 | export default class CannonDebugRenderer { 15 | public scene: THREE.Scene 16 | public world: CANNON.World 17 | private _meshes: THREE.Mesh[] | THREE.Points[] 18 | private _material: THREE.MeshBasicMaterial 19 | private _particleMaterial = new THREE.PointsMaterial() 20 | private _sphereGeometry: THREE.SphereGeometry 21 | private _boxGeometry: THREE.BoxGeometry 22 | private _cylinderGeometry: THREE.CylinderGeometry 23 | private _planeGeometry: THREE.PlaneGeometry 24 | private _particleGeometry: THREE.BufferGeometry 25 | 26 | private tmpVec0: CANNON.Vec3 = new CANNON.Vec3() 27 | private tmpVec1: CANNON.Vec3 = new CANNON.Vec3() 28 | private tmpVec2: CANNON.Vec3 = new CANNON.Vec3() 29 | private tmpQuat0: CANNON.Quaternion = new CANNON.Quaternion() 30 | 31 | constructor(scene: THREE.Scene, world: CANNON.World, options?: object) { 32 | options = options || {} 33 | 34 | this.scene = scene 35 | this.world = world 36 | 37 | this._meshes = [] 38 | 39 | this._material = new THREE.MeshBasicMaterial({ 40 | color: 0x00ff00, 41 | wireframe: true, 42 | }) 43 | this._particleMaterial = new THREE.PointsMaterial({ 44 | color: 0xff0000, 45 | size: 10, 46 | sizeAttenuation: false, 47 | depthTest: false, 48 | }) 49 | this._sphereGeometry = new THREE.SphereGeometry(1) 50 | this._boxGeometry = new THREE.BoxGeometry(1, 1, 1) 51 | this._cylinderGeometry = new THREE.CylinderGeometry(1, 1, 2, 8) 52 | this._planeGeometry = new THREE.PlaneGeometry(10, 10, 10, 10) 53 | this._particleGeometry = new THREE.BufferGeometry() 54 | this._particleGeometry.setFromPoints([new THREE.Vector3(0, 0, 0)]) 55 | } 56 | 57 | public update() { 58 | const bodies: CANNON.Body[] = this.world.bodies 59 | const meshes: THREE.Mesh[] | THREE.Points[] = this._meshes 60 | const shapeWorldPosition: CANNON.Vec3 = this.tmpVec0 61 | const shapeWorldQuaternion: CANNON.Quaternion = this.tmpQuat0 62 | 63 | let meshIndex = 0 64 | 65 | for (let i = 0; i !== bodies.length; i++) { 66 | const body = bodies[i] 67 | 68 | for (let j = 0; j !== body.shapes.length; j++) { 69 | const shape = body.shapes[j] 70 | 71 | this._updateMesh(meshIndex, body, shape) 72 | 73 | const mesh = meshes[meshIndex] 74 | 75 | if (mesh) { 76 | // Get world position 77 | body.quaternion.vmult( 78 | body.shapeOffsets[j], 79 | shapeWorldPosition 80 | ) 81 | body.position.vadd(shapeWorldPosition, shapeWorldPosition) 82 | 83 | // Get world quaternion 84 | body.quaternion.mult( 85 | body.shapeOrientations[j], 86 | shapeWorldQuaternion 87 | ) 88 | 89 | // Copy to meshes 90 | mesh.position.x = shapeWorldPosition.x 91 | mesh.position.y = shapeWorldPosition.y 92 | mesh.position.z = shapeWorldPosition.z 93 | mesh.quaternion.x = shapeWorldQuaternion.x 94 | mesh.quaternion.y = shapeWorldQuaternion.y 95 | mesh.quaternion.z = shapeWorldQuaternion.z 96 | mesh.quaternion.w = shapeWorldQuaternion.w 97 | } 98 | 99 | meshIndex++ 100 | } 101 | } 102 | 103 | for (let i = meshIndex; i < meshes.length; i++) { 104 | const mesh: THREE.Mesh | THREE.Points = meshes[i] 105 | if (mesh) { 106 | this.scene.remove(mesh) 107 | } 108 | } 109 | 110 | meshes.length = meshIndex 111 | } 112 | 113 | private _updateMesh(index: number, body: CANNON.Body, shape: CANNON.Shape) { 114 | let mesh = this._meshes[index] 115 | if (!this._typeMatch(mesh, shape)) { 116 | if (mesh) { 117 | //console.log(shape.type) 118 | this.scene.remove(mesh) 119 | } 120 | mesh = this._meshes[index] = this._createMesh(shape) 121 | } 122 | this._scaleMesh(mesh, shape) 123 | } 124 | 125 | private _typeMatch( 126 | mesh: THREE.Mesh | THREE.Points, 127 | shape: CANNON.Shape 128 | ): boolean { 129 | if (!mesh) { 130 | return false 131 | } 132 | const geo: THREE.BufferGeometry = mesh.geometry 133 | return ( 134 | (geo instanceof THREE.SphereGeometry && 135 | shape instanceof CANNON.Sphere) || 136 | (geo instanceof THREE.BoxGeometry && shape instanceof CANNON.Box) || 137 | (geo instanceof THREE.CylinderGeometry && 138 | shape instanceof CANNON.Cylinder) || 139 | (geo instanceof THREE.PlaneGeometry && 140 | shape instanceof CANNON.Plane) || 141 | shape instanceof CANNON.ConvexPolyhedron || 142 | (geo.id === shape.id && shape instanceof CANNON.Trimesh) || 143 | (geo.id === shape.id && shape instanceof CANNON.Heightfield) 144 | ) 145 | } 146 | 147 | private _createMesh(shape: CANNON.Shape): THREE.Mesh | THREE.Points { 148 | let mesh: THREE.Mesh | THREE.Points 149 | let geometry: THREE.BufferGeometry 150 | let v0: CANNON.Vec3 151 | let v1: CANNON.Vec3 152 | let v2: CANNON.Vec3 153 | const material: THREE.MeshBasicMaterial = this._material 154 | let points: THREE.Vector3[] = [] 155 | 156 | switch (shape.type) { 157 | case CANNON.Shape.types.SPHERE: 158 | mesh = new THREE.Mesh(this._sphereGeometry, material) 159 | break 160 | 161 | case CANNON.Shape.types.BOX: 162 | mesh = new THREE.Mesh(this._boxGeometry, material) 163 | break 164 | 165 | case CANNON.Shape.types.CYLINDER: 166 | geometry = new THREE.CylinderGeometry( 167 | (shape as CANNON.Cylinder).radiusTop, 168 | (shape as CANNON.Cylinder).radiusBottom, 169 | (shape as CANNON.Cylinder).height, 170 | (shape as CANNON.Cylinder).numSegments 171 | ) 172 | mesh = new THREE.Mesh(geometry, material) 173 | break 174 | 175 | case CANNON.Shape.types.PLANE: 176 | mesh = new THREE.Mesh(this._planeGeometry, material) 177 | break 178 | 179 | case CANNON.Shape.types.PARTICLE: 180 | mesh = new THREE.Points( 181 | this._particleGeometry, 182 | this._particleMaterial 183 | ) 184 | break 185 | 186 | case CANNON.Shape.types.CONVEXPOLYHEDRON: 187 | // Create mesh 188 | geometry = new THREE.BufferGeometry() 189 | shape.id = geometry.id 190 | points = [] 191 | for ( 192 | let i = 0; 193 | i < (shape as CANNON.ConvexPolyhedron).vertices.length; 194 | i += 1 195 | ) { 196 | const v = (shape as CANNON.ConvexPolyhedron).vertices[i] 197 | points.push(new THREE.Vector3(v.x, v.y, v.z)) 198 | } 199 | geometry.setFromPoints(points) 200 | 201 | const indices = [] 202 | for ( 203 | let i = 0; 204 | i < (shape as CANNON.ConvexPolyhedron).faces.length; 205 | i++ 206 | ) { 207 | const face = (shape as CANNON.ConvexPolyhedron).faces[i] 208 | const a = face[0] 209 | for (let j = 1; j < face.length - 1; j++) { 210 | const b = face[j] 211 | const c = face[j + 1] 212 | indices.push(a, b, c) 213 | } 214 | } 215 | 216 | geometry.setIndex(indices) 217 | 218 | mesh = new THREE.Mesh(geometry, material) 219 | 220 | break 221 | 222 | case CANNON.Shape.types.TRIMESH: 223 | geometry = new THREE.BufferGeometry() 224 | shape.id = geometry.id 225 | points = [] 226 | for ( 227 | let i = 0; 228 | i < (shape as CANNON.Trimesh).vertices.length; 229 | i += 3 230 | ) { 231 | points.push( 232 | new THREE.Vector3( 233 | (shape as CANNON.Trimesh).vertices[i], 234 | (shape as CANNON.Trimesh).vertices[i + 1], 235 | (shape as CANNON.Trimesh).vertices[i + 2] 236 | ) 237 | ) 238 | } 239 | geometry.setFromPoints(points) 240 | mesh = new THREE.Mesh(geometry, material) 241 | 242 | break 243 | 244 | case CANNON.Shape.types.HEIGHTFIELD: 245 | geometry = new THREE.BufferGeometry() 246 | 247 | v0 = this.tmpVec0 248 | v1 = this.tmpVec1 249 | v2 = this.tmpVec2 250 | for ( 251 | let xi = 0; 252 | xi < (shape as CANNON.Heightfield).data.length - 1; 253 | xi++ 254 | ) { 255 | for ( 256 | let yi = 0; 257 | yi < (shape as CANNON.Heightfield).data[xi].length - 1; 258 | yi++ 259 | ) { 260 | for (let k = 0; k < 2; k++) { 261 | ;( 262 | shape as CANNON.Heightfield 263 | ).getConvexTrianglePillar(xi, yi, k === 0) 264 | v0.copy( 265 | (shape as CANNON.Heightfield).pillarConvex 266 | .vertices[0] 267 | ) 268 | v1.copy( 269 | (shape as CANNON.Heightfield).pillarConvex 270 | .vertices[1] 271 | ) 272 | v2.copy( 273 | (shape as CANNON.Heightfield).pillarConvex 274 | .vertices[2] 275 | ) 276 | v0.vadd( 277 | (shape as CANNON.Heightfield).pillarOffset, 278 | v0 279 | ) 280 | v1.vadd( 281 | (shape as CANNON.Heightfield).pillarOffset, 282 | v1 283 | ) 284 | v2.vadd( 285 | (shape as CANNON.Heightfield).pillarOffset, 286 | v2 287 | ) 288 | points.push( 289 | new THREE.Vector3(v0.x, v0.y, v0.z), 290 | new THREE.Vector3(v1.x, v1.y, v1.z), 291 | new THREE.Vector3(v2.x, v2.y, v2.z) 292 | ) 293 | //const i = geometry.vertices.length - 3 294 | //geometry.faces.push(new THREE.Face3(i, i + 1, i + 2)) 295 | } 296 | } 297 | } 298 | geometry.setFromPoints(points) 299 | //geometry.computeBoundingSphere() 300 | //geometry.computeFaceNormals() 301 | mesh = new THREE.Mesh(geometry, material) 302 | shape.id = geometry.id 303 | break 304 | default: 305 | mesh = new THREE.Mesh() 306 | break 307 | } 308 | 309 | if (mesh && mesh.geometry) { 310 | this.scene.add(mesh) 311 | } 312 | 313 | return mesh 314 | } 315 | 316 | private _scaleMesh(mesh: THREE.Mesh | THREE.Points, shape: CANNON.Shape) { 317 | let radius: number 318 | let halfExtents: CANNON.Vec3 319 | let scale: CANNON.Vec3 320 | 321 | switch (shape.type) { 322 | case CANNON.Shape.types.SPHERE: 323 | radius = (shape as CANNON.Sphere).radius 324 | mesh.scale.set(radius, radius, radius) 325 | break 326 | 327 | case CANNON.Shape.types.BOX: 328 | halfExtents = (shape as CANNON.Box).halfExtents 329 | mesh.scale.copy( 330 | new THREE.Vector3( 331 | halfExtents.x, 332 | halfExtents.y, 333 | halfExtents.z 334 | ) 335 | ) 336 | mesh.scale.multiplyScalar(2) 337 | break 338 | 339 | case CANNON.Shape.types.CONVEXPOLYHEDRON: 340 | mesh.scale.set(1, 1, 1) 341 | break 342 | 343 | case CANNON.Shape.types.TRIMESH: 344 | scale = (shape as CANNON.Trimesh).scale 345 | mesh.scale.copy(new THREE.Vector3(scale.x, scale.y, scale.z)) 346 | break 347 | 348 | case CANNON.Shape.types.HEIGHTFIELD: 349 | mesh.scale.set(1, 1, 1) 350 | break 351 | } 352 | } 353 | } -------------------------------------------------------------------------------- /src/client/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: { 6 | demo1: path.resolve(__dirname, './demo1.ts'), 7 | demo2: path.resolve(__dirname, './demo2.ts') 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.tsx?$/, 13 | use: 'ts-loader', 14 | exclude: /node_modules/, 15 | }, 16 | ] 17 | }, 18 | resolve: { 19 | alias: { 20 | three: path.resolve('./node_modules/three') 21 | }, 22 | extensions: ['.tsx', '.ts', '.js'], 23 | }, 24 | output: { 25 | filename: '[name].bundle.js', 26 | path: path.resolve(__dirname, '../../dist/client'), 27 | }, 28 | plugins: [ 29 | new HtmlWebpackPlugin({ 30 | template: path.resolve(__dirname, "./demo1.html"), 31 | filename: path.resolve(__dirname, '../../dist/client/demo1.html'), 32 | chunks: ['demo1'] 33 | }), 34 | new HtmlWebpackPlugin({ 35 | template: path.resolve(__dirname, "./demo2.html"), 36 | filename: path.resolve(__dirname, '../../dist/client/demo2.html'), 37 | chunks: ['demo2'] 38 | }) 39 | ] 40 | }; -------------------------------------------------------------------------------- /src/client/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge') 2 | const common = require('./webpack.common.js') 3 | const path = require('path') 4 | 5 | module.exports = merge(common, { 6 | mode: 'development', 7 | devtool: 'eval-source-map', 8 | devServer: { 9 | server: 'https', 10 | static: { 11 | directory: path.join(__dirname, '../../dist/client'), 12 | }, 13 | hot: true, 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /src/client/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'production', 6 | performance: { 7 | hints: false 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /src/server/server.ts: -------------------------------------------------------------------------------- 1 | import express from "express" 2 | import path from "path" 3 | import http from "http" 4 | 5 | const port: number = 3000 6 | 7 | class App { 8 | private server: http.Server 9 | private port: number 10 | 11 | constructor(port: number) { 12 | this.port = port 13 | const app = express() 14 | app.use(express.static(path.join(__dirname, '../client'))) 15 | // In the webpack version of the boilerplate, it is not necessary 16 | // to add static references to the libs in node_modules if 17 | // you are using module specifiers in your client.ts imports. 18 | // 19 | // Visit https://sbcode.net/threejs/module-specifiers/ for info about module specifiers 20 | // 21 | // This server.ts is only useful if you are running this on a production server or you 22 | // want to see how the production version of bundle.js works 23 | // 24 | // to use this server.ts 25 | // # npm run build (this creates the production version of bundle.js and places it in ./dist/client/) 26 | // # tsc -p ./src/server (this compiles ./src/server/server.ts into ./dist/server/server.js) 27 | // # npm start (this starts nodejs with express and serves the ./dist/client folder) 28 | // 29 | // visit http://127.0.0.1:3000 30 | 31 | this.server = new http.Server(app); 32 | } 33 | 34 | public Start() { 35 | this.server.listen(this.port, () => { 36 | console.log( `Server listening on port ${this.port}.` ) 37 | }) 38 | } 39 | } 40 | 41 | new App(port).Start() -------------------------------------------------------------------------------- /src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "module": "commonjs", 5 | "outDir": "../../dist/server", 6 | "sourceMap": true, 7 | "esModuleInterop": true 8 | }, 9 | "include": [ 10 | "**/*.ts" 11 | ] 12 | } -------------------------------------------------------------------------------- /threejs-course-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Bradley/GrabVR/d267df987d9bceefffc5d0816cc528fcd1daf4d0/threejs-course-image.png --------------------------------------------------------------------------------