├── .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 | [](https://sbcode.net/threejs/grabvr-1/)
73 |
74 | ## Example 2
75 |
76 | GrabVR demo using Cannonjs.
77 |
78 | [](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 | [](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 |
56 |
59 |
60 |
61 |
Demo 2
62 |
GrabVR Demo using Cannonjs.
63 |
68 |
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
--------------------------------------------------------------------------------