├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── _config.yml
├── package-lock.json
├── package.json
├── sandcastleprojects.png
├── src
├── engine
│ ├── camera.js
│ ├── engine.js
│ ├── networking
│ │ ├── firebasesignalingserver.js
│ │ ├── peerconnection.js
│ │ ├── remotesync.js
│ │ └── webrtcclient.js
│ ├── physics
│ │ ├── physics.js
│ │ └── physics.worker.js
│ ├── renderer.js
│ ├── state.js
│ ├── util
│ │ ├── cameracontrols
│ │ │ └── engineeditorcamera.js
│ │ ├── debughelpers
│ │ │ └── cannondebugrenderer.js
│ │ ├── hostbot.js
│ │ ├── webxr
│ │ │ ├── raycaster.ts
│ │ │ └── sessionhandler.js
│ │ └── xrpk
│ │ │ └── xrpk-build.js
│ └── xrinput.js
├── examples
│ ├── artovr
│ │ ├── assets
│ │ │ ├── models
│ │ │ │ └── polyCrow
│ │ │ │ │ ├── Bird_01(1).bin
│ │ │ │ │ └── polyCrow_updated.glb
│ │ │ └── textures
│ │ │ │ └── waternormals.jpg
│ │ ├── boid.js
│ │ ├── scene.js
│ │ ├── sky.js
│ │ └── water.js
│ ├── daynightcycle
│ │ ├── assets
│ │ │ ├── shaders
│ │ │ │ ├── fs_clouds.glsl
│ │ │ │ └── vs_clouds.glsl
│ │ │ └── textures
│ │ │ │ ├── 1.jpg
│ │ │ │ ├── 2.jpg
│ │ │ │ └── cloud10.png
│ │ ├── cloud.js
│ │ ├── scene.js
│ │ └── sky.js
│ ├── defaultscene.js
│ ├── physicsexample
│ │ ├── brickcustomshader.js
│ │ ├── scene.js
│ │ └── shaders
│ │ │ ├── fs_bloomFireflies.glsl
│ │ │ ├── fs_matrixLetters.glsl
│ │ │ ├── fs_neonGrid.glsl
│ │ │ ├── fs_pastelCheckers.glsl
│ │ │ ├── fs_puddles.glsl
│ │ │ └── vs_defaultVertex.glsl
│ ├── pongxr
│ │ ├── assets
│ │ │ ├── audio
│ │ │ │ ├── elecping.ogg
│ │ │ │ └── hitgoal.ogg
│ │ │ └── shaders
│ │ │ │ ├── fs_goal.glsl
│ │ │ │ ├── fs_puddles.glsl
│ │ │ │ └── vs_defaultVertex.glsl
│ │ ├── ball.js
│ │ ├── frictionlessMaterial.js
│ │ ├── level.js
│ │ ├── paddle.js
│ │ ├── placementcube.js
│ │ └── scene.js
│ └── voicestreaming
│ │ ├── scene.js
│ │ ├── shaders
│ │ ├── fs_partycles.glsl
│ │ └── vs_partycles.glsl
│ │ └── textures
│ │ └── spark1.png
├── index.html
├── index.ts
└── style.css
├── tsconfig.json
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-typescript", "@babel/preset-env"],
3 | "plugins": [["@babel/plugin-proposal-class-properties"]]
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | .vscode
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Michael Hazani
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 |
Sandcastle
2 | A friendly framework for creating spatial-first, multi-user WebXR apps
3 |
4 | 
5 |
6 | ## Features
7 |
8 | - Built on vanilla [Three.js](http://Three.js.org/)
9 |
10 | - Made for WebXR (ergonomic XR [session & state management](https://github.com/plutovr/sandcastle/wiki#webxr-general-1) & easy [XR input event handling](https://github.com/plutovr/sandcastle/wiki#webxr-input-1))
11 |
12 | - Built-in [Physics & Collision Detection](https://github.com/plutovr/sandcastle/wiki#physics-1) courtesy of [CannonJS](http://www.cannonjs.org/)
13 |
14 | - Easy, WebRTC-based [networking & media streaming](https://github.com/plutovr/sandcastle/wiki#networking-1) courtesy of [ThreeNetwork](https://github.com/takahirox/ThreeNetwork)
15 |
16 | - Designed for [XR Packages](https://github.com/webaverse/xrpackage) and shared WebXR experiences/multiapps
17 |
18 | - Tiny project build sizes (~250kb gzipped before assets, way less than the image above!)
19 |
20 | ---
21 |
22 | ## Usage
23 |
24 | Run `npm init sandcastle NAMEOFPROJECT` _OR_ `npx create-sandcastle NAMEOFPROJECT`, where `NAMEOFPROJECT` is your desired project name.
25 |
26 | This will automatically:
27 |
28 | 1. clone this repo into a new folder of that name
29 | 2. install Sandcastle's dependencies
30 | 3. remove the `.git` repo
31 | 4. launch the dev server and
32 | 5. open the default scene (located at `./src/examples/defaultScene.js`) in your browser.
33 |
34 | ---
35 |
36 | ## Development & Building
37 |
38 | - `npm start` will launch the dev server and open the sample scene.
39 |
40 | - `npm run build` will process and build your project into a `dist` folder.
41 |
42 | - `npm run build-xrpk` will `npm build`, then create an [XR Package](https://github.com/webaverse/xrpackage) in `dist`. (Note that this script runs an interactive CLI for details about the various aspects of your XR Package.)
43 |
44 | - `npm run dev-xrpk` will do the same but output an _unminified, source-mapped_ XR Package to help you debug your XR Package in your runtime of choice (we recommend [Chimera](https://chimera.pluto-api.com/)). Please note the resulting .wbn file size will be very large - don't use this in production!
45 |
46 | ---
47 |
48 | ## Learning Resources
49 |
50 | - Check out the [Wiki](https://github.com/plutovr/sandcastle/wiki) for a closer look at Sandcastle's Networking API, Physics API, event handling and state management, asset pipelines and more.
51 |
52 | - See the `examples` folder for usage examples of networking, media streaming, physics, AR-in-VR experiences and more.
53 |
54 | #### This project is in pre-alpha and currently undergoes daily work. Is something broken or unclear? Please file an issue!
55 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-tactile
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sandcastle",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "git://github.com/PlutoVR/sandcastle.git"
9 | },
10 | "scripts": {
11 | "start": "webpack-dev-server --config webpack.dev.js",
12 | "build": "webpack --progress --profile --colors --config webpack.prod.js",
13 | "dev-xrpk": "webpack --progress --profile --colors --config webpack.dev.js && node ./src/engine/util/xrpk/xrpk-build.js",
14 | "build-xrpk": "npm run build && node ./src/engine/util/xrpk/xrpk-build.js"
15 | },
16 | "keywords": [],
17 | "author": "",
18 | "license": "ISC",
19 | "dependencies": {
20 | "cannon": "^0.6.2",
21 | "firebase": "^8.0.0",
22 | "inquirer": "^7.2.0",
23 | "loader-utils": "^2.0.0",
24 | "speak-tts": "^2.0.8",
25 | "three": "^0.122.0"
26 | },
27 | "devDependencies": {
28 | "@babel/core": "^7.9.6",
29 | "@babel/plugin-proposal-class-properties": "^7.13.0",
30 | "@babel/preset-env": "^7.9.6",
31 | "@babel/preset-typescript": "^7.13.0",
32 | "@types/three": "^0.128.0",
33 | "babel-loader": "^8.1.0",
34 | "clean-webpack-plugin": "^3.0.0",
35 | "compression-webpack-plugin": "^4.0.0",
36 | "css-loader": "^3.5.3",
37 | "file-loader": "^6.0.0",
38 | "gltf-webpack-loader": "^1.0.6",
39 | "html-loader": "^1.1.0",
40 | "html-webpack-plugin": "^4.3.0",
41 | "imagemin-jpegtran": "^6.0.0",
42 | "imagemin-mozjpeg": "^8.0.0",
43 | "imagemin-optipng": "^7.1.0",
44 | "imagemin-svgo": "^7.1.0",
45 | "imagemin-webpack-plugin": "^2.4.2",
46 | "offline-plugin": "^5.0.7",
47 | "script-ext-html-webpack-plugin": "^2.1.4",
48 | "shader-loader": "^1.3.1",
49 | "style-loader": "^1.2.1",
50 | "ts-loader": "^8.2.0",
51 | "typescript": "^4.2.4",
52 | "webpack": "^4.43.0",
53 | "webpack-cli": "^3.3.11",
54 | "webpack-dev-server": "^3.11.0",
55 | "webpack-glsl-loader": "^1.0.1",
56 | "webpack-merge": "^4.2.2",
57 | "worker-loader": "^2.0.0",
58 | "xrpk": "0.0.70"
59 | },
60 | "prettier": {
61 | "semi": true,
62 | "singleQuote": false,
63 | "tabWidth": 2,
64 | "trailingComma": "es5",
65 | "printWidth": 80,
66 | "arrowParens": "avoid"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/sandcastleprojects.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PlutoVR/sandcastle/060b6ddb0dba5225479a6860febfa11c96f3e340/sandcastleprojects.png
--------------------------------------------------------------------------------
/src/engine/camera.js:
--------------------------------------------------------------------------------
1 | import { PerspectiveCamera } from "three";
2 | import State from "./state";
3 |
4 | class TrackingCamera extends PerspectiveCamera {
5 | constructor() {
6 | super(75, window.innerWidth / window.innerHeight, 0.1, 10000);
7 | }
8 |
9 | Update() {
10 | if (!State.isXRSession) return;
11 | this.matrixWorld.decompose(this.position, this.quaternion, this.scale);
12 | }
13 | }
14 |
15 | const Camera = new TrackingCamera();
16 |
17 | export default Camera;
18 |
--------------------------------------------------------------------------------
/src/engine/engine.js:
--------------------------------------------------------------------------------
1 | import Camera from "./camera";
2 | import State from "./state";
3 | import { AudioListener } from "three";
4 | import EngineEditorCamera from "./util/cameracontrols/engineeditorcamera";
5 | import SessionHandler from "./util/webxr/sessionhandler";
6 | import Renderer from "./renderer";
7 | import XRInput from "./xrinput";
8 |
9 | export function loadScene(scene) {
10 | scene.add(new EngineEditorCamera(Camera, Renderer.domElement));
11 | Camera.audioListener = new AudioListener();
12 | Camera.add(Camera.audioListener);
13 | scene.add(Camera);
14 |
15 | // main app render loop
16 | Renderer.setAnimationLoop(() => {
17 | // RENDERING
18 | Renderer.render(scene, Camera);
19 | // INPUT
20 | if (State.isXRSession) XRInput.Update();
21 |
22 | // TRAVERSE UPDATE METHODS IN SCENE OBJECTS
23 | scene.traverse(obj => {
24 | typeof obj.Update === "function" ? obj.Update() : false;
25 | });
26 | });
27 |
28 | // DOM append
29 | document.querySelector(".app").appendChild(Renderer.domElement);
30 | // WebXR button
31 | document.querySelector(".app").appendChild(new SessionHandler());
32 |
33 | window.addEventListener("resize", () => {
34 | Camera.aspect = window.innerWidth / window.innerHeight;
35 | Camera.updateProjectionMatrix();
36 | Renderer.setSize(window.innerWidth, window.innerHeight);
37 | });
38 | }
39 |
--------------------------------------------------------------------------------
/src/engine/networking/firebasesignalingserver.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Takahiro https://github.com/takahirox
3 | *
4 | * TODO:
5 | * support all authorize types.
6 | */
7 |
8 | const firebase = require("firebase/app");
9 | require("firebase/auth");
10 | require("firebase/database");
11 |
12 | import { Math } from "three";
13 | import RS from "./remotesync";
14 |
15 | /**
16 | * FirebaseSignalingServer constructor.
17 | * FirebaseSignalingServer uses Firebase as a signaling server.
18 | * @param {object} params - parameters for instantiate and Firebase configuration (optional)
19 | */
20 | class FirebaseSignalingServer extends RS.SignalingServer {
21 | constructor(params) {
22 | super(params);
23 |
24 | if (firebase == undefined || firebase == undefined) {
25 | throw new Error("FirebaseSignalingServer: Import firebase app and db");
26 | }
27 |
28 | if (params === undefined) params = {};
29 |
30 | RS.SignalingServer.call(this);
31 |
32 | // Refer to Frebase document for them
33 | this.apiKey = params.apiKey !== undefined ? params.apiKey : "";
34 | this.authDomain = params.authDomain !== undefined ? params.authDomain : "";
35 | this.databaseURL =
36 | params.databaseURL !== undefined ? params.databaseURL : "";
37 |
38 | this.authType =
39 | params.authType !== undefined ? params.authType : "anonymous";
40 |
41 | this.init();
42 | this.auth();
43 | }
44 | // FirebaseSignalingServer.prototype = Object.create(SignalingServer.prototype);
45 | // FirebaseSignalingServer.prototype.constructor = FirebaseSignalingServer;
46 |
47 | /**
48 | * Initializes Firebase.
49 | */
50 | init() {
51 | firebase.initializeApp({
52 | apiKey: this.apiKey,
53 | authDomain: this.authDomain,
54 | databaseURL: this.databaseURL,
55 | });
56 | }
57 |
58 | /**
59 | * Authorizes Firebase, depending on authorize type.
60 | */
61 | auth() {
62 | switch (this.authType) {
63 | case "none":
64 | this.authNone();
65 | break;
66 |
67 | case "anonymous":
68 | this.authAnonymous();
69 | break;
70 |
71 | default:
72 | console.log(
73 | "FirebaseSignalingServer.auth: Unknown authType " + this.authType
74 | );
75 | break;
76 | }
77 | }
78 |
79 | /**
80 | * Doesn't authorize.
81 | */
82 | authNone() {
83 | var self = this;
84 |
85 | // makes an unique 16-char id by myself.
86 | var id = Math.generateUUID().replace(/-/g, "").toLowerCase().slice(0, 16);
87 |
88 | // asynchronously invokes open listeners for the compatibility with other auth types.
89 | requestAnimationFrame(function () {
90 | self.id = id;
91 | self.invokeOpenListeners(id);
92 | });
93 | }
94 |
95 | /**
96 | * Authorizes as anonymous.
97 | */
98 | authAnonymous() {
99 | var self = this;
100 |
101 | firebase
102 | .auth()
103 | .signInAnonymously()
104 | .catch(function (error) {
105 | console.log("FirebaseSignalingServer.authAnonymous: " + error);
106 |
107 | self.invokeErrorListeners(error);
108 | });
109 |
110 | firebase.auth().onAuthStateChanged(function (user) {
111 | if (user === null) {
112 | // disconnected from server
113 |
114 | self.invokeCloseListeners(self.id);
115 | } else {
116 | // authorized
117 |
118 | self.id = user.uid;
119 | self.invokeOpenListeners(self.id);
120 | }
121 | });
122 | }
123 |
124 | /**
125 | * Gets server timestamp.
126 | * @param {function} callback
127 | */
128 | getTimestamp(callback) {
129 | var ref = firebase.database().ref("tmp" + "/" + this.id);
130 |
131 | ref.set(firebase.database.ServerValue.TIMESTAMP);
132 |
133 | ref.once("value", function (data) {
134 | var timestamp = data.val();
135 |
136 | ref.remove();
137 |
138 | callback(timestamp);
139 | });
140 |
141 | ref.onDisconnect().remove();
142 | }
143 |
144 | // public concrete method
145 |
146 | connect(roomId) {
147 | // console.log("connected signaling");
148 | // console.log("roomid " + roomId);
149 | // console.log("id " + this.id);
150 | var self = this;
151 |
152 | this.roomId = roomId;
153 |
154 | // TODO: check if this timestamp logic can race.
155 | this.getTimestamp(function (timestamp) {
156 | var ref = firebase.database().ref(self.roomId + "/" + self.id);
157 |
158 | ref.set({ timestamp: timestamp, signal: "" });
159 |
160 | ref.onDisconnect().remove();
161 |
162 | var doneTable = {}; // remote peer id -> true or undefined, indicates if already done.
163 |
164 | firebase
165 | .database()
166 | .ref(self.roomId)
167 | .on("child_added", function (data) {
168 | var id = data.key;
169 |
170 | if (id === self.id || doneTable[id] === true) return;
171 |
172 | doneTable[id] = true;
173 |
174 | var remoteTimestamp = data.val().timestamp;
175 |
176 | // received signal
177 | firebase
178 | .database()
179 | .ref(self.roomId + "/" + id + "/signal")
180 | .on("value", function (data) {
181 | if (data.val() === null || data.val() === "") return;
182 |
183 | self.invokeReceiveListeners(data.val());
184 | });
185 |
186 | self.invokeRemoteJoinListeners(id, timestamp, remoteTimestamp);
187 | });
188 |
189 | firebase
190 | .database()
191 | .ref(roomId)
192 | .on("child_removed", function (data) {
193 | delete doneTable[data.key];
194 | });
195 | });
196 | }
197 |
198 | // TODO: we should enable .send() to send signal to a peer, not only broadcast?
199 | send(data) {
200 | firebase
201 | .database()
202 | .ref(this.roomId + "/" + this.id + "/signal")
203 | .set(data);
204 | }
205 | }
206 |
207 | export default FirebaseSignalingServer;
208 |
--------------------------------------------------------------------------------
/src/engine/networking/peerconnection.js:
--------------------------------------------------------------------------------
1 | import RS from "./remotesync";
2 | import State from "../state";
3 | import { Object3D } from "three";
4 | import FirebaseSignalingServer from "./firebasesignalingserver";
5 | import WebRTCClient from "./webrtcclient";
6 |
7 | class PeerConnection {
8 | constructor(scene, stream) {
9 | this.scene = scene;
10 | // assign random id to URL
11 | if (location.href.indexOf("?") === -1) {
12 | location.href += "?" + ((Math.random() * 10000) | 0);
13 | }
14 |
15 | this.remoteSync = new RS.RemoteSync(
16 | new WebRTCClient(
17 | new FirebaseSignalingServer({
18 | authType: "none",
19 | apiKey: "AIzaSyBu6M0W3iBAWPLIkW5L3ixr7io2IQZxQOA",
20 | authDomain: "sandcastle-e07df.firebaseapp.com",
21 | databaseURL: "https://sandcastle-e07df.firebaseio.com",
22 | }),
23 | { stream: stream }
24 | )
25 | );
26 | this.remoteSync.addEventListener("open", this.onOpen.bind(this));
27 | this.remoteSync.addEventListener("close", this.onClose.bind(this));
28 | this.remoteSync.addEventListener("error", this.onError.bind(this));
29 | this.remoteSync.addEventListener("connect", this.onConnect.bind(this));
30 | this.remoteSync.addEventListener(
31 | "disconnect",
32 | this.onDisconnect.bind(this)
33 | );
34 | this.remoteSync.addEventListener("receive", this.onReceive.bind(this));
35 | this.remoteSync.addEventListener("add", this.onAdd.bind(this));
36 | this.remoteSync.addEventListener("remove", this.onRemove.bind(this));
37 | this.remoteSync.addEventListener("master", this.onPrimary.bind(this));
38 | this.remoteSync.addEventListener("slave", this.onSecondary.bind(this));
39 |
40 | //add networking update method
41 | const networkingUpdate = new Object3D();
42 | networkingUpdate.Update = () => {
43 | this.remoteSync.sync();
44 | };
45 | this.scene.add(networkingUpdate);
46 | }
47 |
48 | onOpen(id) {
49 | this.clientId = id;
50 | const link = location.href;
51 | const a = document.createElement("a");
52 | a.target = "_blank";
53 | a.setAttribute("href", link);
54 | a.setAttribute("target", "_blank");
55 | a.innerHTML = link;
56 | document.querySelector(".info").appendChild(a);
57 | this.connectFromURL();
58 | State.eventHandler.dispatchEvent("peerconnected");
59 | }
60 |
61 | onReceive(data) {
62 | // console.log("OnReceive")
63 | // console.log(data);
64 | }
65 |
66 | onPrimary(data) {
67 | State.isPrimary = true;
68 | }
69 |
70 | onSecondary(data) {
71 | State.isPrimary = false;
72 | }
73 |
74 | onAdd(destId, objectId, info) {
75 | if (State.debugMode) {
76 | console.log("onAdd: adding " + objectId);
77 | console.log(info);
78 | }
79 | }
80 |
81 | onRemove(destId, objectId, object) {
82 | if (State.debugMode) {
83 | console.log("onRemove: removing " + objectId);
84 | console.log(object);
85 | }
86 | if (object.parent !== null) object.parent.remove(object);
87 | }
88 |
89 | onClose(destId) {
90 | if (State.debugMode) console.log("Disconnected to " + destId);
91 | }
92 |
93 | onError(error) {
94 | console.error(error);
95 | }
96 |
97 | onConnect(destId) {
98 | if (State.debugMode) console.log("onConnect: Connected with " + destId);
99 | }
100 |
101 | onDisconnect(destId, object) {
102 | if (State.debugMode) console.log("Disconnected with " + destId);
103 | }
104 |
105 | connect(id) {
106 | if (id === this.clientId) {
107 | if (State.debugMode) console.log(id + " is your id");
108 | return;
109 | }
110 | if (State.debugMode) console.log("Connecting with " + id);
111 | this.remoteSync.connect(id);
112 | }
113 |
114 | connectFromURL() {
115 | const url = location.href;
116 | const index = url.indexOf("?");
117 | if (index >= 0) {
118 | const id = url.slice(index + 1);
119 | this.connect(id);
120 | }
121 | }
122 | }
123 |
124 | export default PeerConnection;
125 |
--------------------------------------------------------------------------------
/src/engine/networking/webrtcclient.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Takahiro https://github.com/takahirox
3 | * Gently modified by Michael Hazani
4 | */
5 |
6 | const RemoteSync = require("./remotesync");
7 |
8 | /**
9 | * WebRTCClient constructor.
10 | * General WebRTC Client, establishes a connection via Signaling server.
11 | * @param {THREE.SignalingServer} server
12 | * @param {object} params - parameters for instantiate (optional)
13 | */
14 |
15 | export default class WebRTCClient extends RemoteSync.NetworkClient {
16 | constructor(server, params) {
17 | super(params);
18 | if (params === undefined) params = {};
19 |
20 | RemoteSync.NetworkClient.call(this, params);
21 |
22 | this.server = server;
23 |
24 | // ice servers for RTCPeerConnection.
25 |
26 | this.init();
27 | }
28 |
29 | init() {
30 | var self = this;
31 |
32 | // connected with server
33 | this.server.addEventListener(
34 | "open",
35 |
36 | function (id) {
37 | this.invokeOpenListeners(id);
38 | }.bind(this)
39 | );
40 |
41 | // disconnected from server
42 | this.server.addEventListener(
43 | "close",
44 |
45 | function (id) {
46 | self.invokeCloseListeners(id);
47 | }
48 | );
49 |
50 | // error occurred with server
51 | this.server.addEventListener(
52 | "error",
53 |
54 | function (error) {
55 | self.invokeErrorListeners(error);
56 | }
57 | );
58 |
59 | // aware of a remote peer join the room
60 | this.server.addEventListener("remote_join", function (
61 | id,
62 | localTimestamp,
63 | remoteTimestamp
64 | ) {
65 | if (id === self.id || self.hasConnection(id)) return;
66 |
67 | // TODO: need a workaround for localTimestamp === remoteTimestamp
68 | var connectFromMe = localTimestamp > remoteTimestamp;
69 |
70 | var peer = new WebRTCPeer(self.id, id, self.server, self.stream);
71 |
72 | // received signal from a remote peer via server
73 | self.server.addEventListener(
74 | "receive",
75 |
76 | function (signal) {
77 | peer.handleSignal(signal);
78 | }
79 | );
80 |
81 | // connected with a remote peer
82 | peer.addEventListener("open", function (id) {
83 | if (self.addConnection(id, peer)) {
84 | self.invokeConnectListeners(id, !connectFromMe);
85 | }
86 |
87 | // TODO: remove server 'receive' listener here.
88 | // if .addConnection() fails here?
89 | });
90 |
91 | // disconnected from a remote peer
92 | peer.addEventListener("close", function (id) {
93 | if (self.removeConnection(id)) {
94 | // TODO: remove server 'receive' listener here.
95 |
96 | self.invokeDisconnectListeners(id);
97 | }
98 | });
99 |
100 | // error occurred with a remote peer
101 | peer.addEventListener("error", function (error) {
102 | self.invokeErrorListeners(error);
103 | });
104 |
105 | // received data from a remote peer
106 | peer.addEventListener("receive", function (data) {
107 | self.invokeReceiveListeners(data);
108 | });
109 |
110 | // received remote media streaming
111 | peer.addEventListener("receive_stream", function (stream) {
112 | self.invokeRemoteStreamListeners(stream);
113 | });
114 |
115 | if (connectFromMe) peer.offer();
116 | });
117 |
118 | // for the compatibility with other NetworkClient classes.
119 | // if already connected with signaling server, asynchronously invokes open listeners.
120 | if (this.server.id !== "") {
121 | requestAnimationFrame(function () {
122 | self.invokeOpenListeners(self.server.id);
123 | });
124 | }
125 | }
126 |
127 | // public concrete method
128 |
129 | connect(roomId) {
130 | var self = this;
131 |
132 | this.roomId = roomId;
133 | this.server.connect(roomId);
134 | }
135 |
136 | send(id, data) {
137 | var connection = this.connectionTable[id];
138 |
139 | if (connection === undefined) return;
140 |
141 | connection.send(data);
142 | }
143 |
144 | broadcast(data) {
145 | for (var i = 0, il = this.connections.length; i < il; i++) {
146 | this.send(this.connections[i].peer, data);
147 | }
148 | }
149 |
150 | /**
151 | * WebRTCPeer constructor.
152 | * WebRTCPeer handles WebRTC connection and data transfer with RTCPeerConnection.
153 | * Refer to RTCPeerConnection document for the message handling detail.
154 | * @param {string} id - local peer id
155 | * @param {string} peer - remote peer id
156 | * @param {SignalingServer} server
157 | * @param {MediaStream} stream - sends media stream to remote peer if it's provided (optional)
158 | */
159 | }
160 | class WebRTCPeer {
161 | constructor(id, peer, server, stream) {
162 | this.id = id;
163 | this.peer = peer;
164 | this.server = server;
165 | this.pc = this.createPeerConnection(stream);
166 | this.channel = null;
167 |
168 | this.open = false;
169 |
170 | // event listeners
171 |
172 | this.onOpens = [];
173 | this.onCloses = [];
174 | this.onErrors = [];
175 | this.onReceives = [];
176 | this.onReceiveStreams = [];
177 | }
178 |
179 | /**
180 | * Adds EventListener. Callback function will be invoked when
181 | * 'open': a connection is established with a remote peer
182 | * 'close': a connection is disconnected from a remote peer
183 | * 'error': error occurs
184 | * 'receive': receives data from a remote peer
185 | * 'remote_stream': receives a remote media stream
186 | *
187 | * Arguments for callback functions are
188 | * 'open': {string} local peer id
189 | * 'close': {string} local peer id
190 | * 'error': {string} error message
191 | * 'receive': {anything} signal sent from a remote peer
192 | * 'remote_stream': {MediaStream} remote media stream
193 | *
194 | * @param {string} type - event type
195 | * @param {function} func - callback function
196 | */
197 | addEventListener(type, func) {
198 | switch (type) {
199 | case "open":
200 | this.onOpens.push(func);
201 | break;
202 |
203 | case "close":
204 | this.onCloses.push(func);
205 | break;
206 |
207 | case "error":
208 | this.onErrors.push(func);
209 | break;
210 |
211 | case "receive":
212 | this.onReceives.push(func);
213 | break;
214 |
215 | case "receive_stream":
216 | this.onReceiveStreams.push(func);
217 | break;
218 |
219 | default:
220 | console.log("WebRTCPeer.addEventListener: Unknown type " + type);
221 | break;
222 | }
223 | }
224 |
225 | /**
226 | * Creates peer connection.
227 | * @param {MediaStream} stream - sends media stream to remote if it's provided (optional)
228 | * @returns {RTCPeerConnection}
229 | */
230 | createPeerConnection(stream) {
231 | this.ICE_SERVERS = [
232 | { urls: "stun:stun.l.google.com:19302" },
233 | { urls: "stun:stun1.l.google.com:19302" },
234 | { urls: "stun:stun2.l.google.com:19302" },
235 | { urls: "stun:stun3.l.google.com:19302" },
236 | { urls: "stun:stun4.l.google.com:19302" },
237 | ];
238 |
239 | var self = this;
240 |
241 | var RTCPeerConnection =
242 | window.RTCPeerConnection ||
243 | window.webkitRTCPeerConnection ||
244 | window.mozRTCPeerConnection ||
245 | window.msRTCPeerConnection;
246 |
247 | if (RTCPeerConnection === undefined) {
248 | throw new Error(
249 | "WebRTCPeer.createPeerConnection: This browser does not seem to support WebRTC."
250 | );
251 | }
252 | const configuration = { iceServers: this.ICE_SERVERS };
253 |
254 | var pc = new RTCPeerConnection(configuration);
255 | if (stream !== null && stream !== undefined) pc.addStream(stream);
256 |
257 | pc.onicecandidate = function (event) {
258 | if (event.candidate) {
259 | var params = {
260 | id: self.id,
261 | peer: self.peer,
262 | type: "candidate",
263 | sdpMLineIndex: event.candidate.sdpMLineIndex,
264 | candidate: event.candidate.candidate,
265 | };
266 |
267 | self.server.send(params);
268 | }
269 | };
270 |
271 | pc.onaddstream = function (event) {
272 | self.invokeReceiveStreamListeners(event.stream);
273 | };
274 |
275 | // Note: seems like channel.onclose hander is unreliable on some platforms,
276 | // so also try to detect disconnection here.
277 | pc.oniceconnectionstatechange = function () {
278 | if (self.open && pc.iceConnectionState == "disconnected") {
279 | self.open = false;
280 |
281 | self.invokeCloseListeners(self.peer);
282 | }
283 | };
284 |
285 | return pc;
286 | }
287 |
288 | /**
289 | * Handles offer request.
290 | * @param {object} message - message sent from a remote peer
291 | */
292 | handleOffer(message) {
293 | var self = this;
294 |
295 | this.pc.ondatachannel = function (event) {
296 | self.channel = event.channel;
297 | self.setupChannelListener();
298 | };
299 |
300 | this.setRemoteDescription(message);
301 |
302 | this.pc.createAnswer(
303 | function (sdp) {
304 | self.handleSessionDescription(sdp);
305 | },
306 |
307 | function (error) {
308 | console.log("WebRTCPeer.handleOffer: " + error);
309 | self.invokeErrorListeners(error);
310 | }
311 | );
312 | }
313 |
314 | /**
315 | * Handles answer response.
316 | * @param {object} message - message sent from a remote peer
317 | */
318 | handleAnswer(message) {
319 | this.setRemoteDescription(message);
320 | }
321 |
322 | /**
323 | * Handles candidate sent from a remote peer.
324 | * @param {object} message - message sent from a remote peer
325 | */
326 | handleCandidate(message) {
327 | var self = this;
328 |
329 | var RTCIceCandidate =
330 | window.RTCIceCandidate ||
331 | window.webkitRTCIceCandidate ||
332 | window.mozRTCIceCandidate;
333 |
334 | this.pc.addIceCandidate(
335 | new RTCIceCandidate(message),
336 |
337 | function () {},
338 |
339 | function (error) {
340 | console.log("WebRTCPeer.handleCandidate: " + error);
341 | self.invokeErrorListeners(error);
342 | }
343 | );
344 | }
345 |
346 | /**
347 | * Handles SessionDescription.
348 | * @param {RTCSessionDescription} sdp
349 | */
350 | handleSessionDescription(sdp) {
351 | var self = this;
352 |
353 | this.pc.setLocalDescription(
354 | sdp,
355 |
356 | function () {},
357 |
358 | function (error) {
359 | console.log("WebRTCPeer.handleSessionDescription: " + error);
360 | self.invokeErrorListeners(error);
361 | }
362 | );
363 |
364 | this.server.send({
365 | id: this.id,
366 | peer: this.peer,
367 | type: sdp.type,
368 | sdp: sdp.sdp,
369 | });
370 | }
371 |
372 | /**
373 | * Sets remote description.
374 | * @param {object} message - message sent from a remote peer
375 | */
376 | setRemoteDescription(message) {
377 | var self = this;
378 |
379 | var RTCSessionDescription =
380 | window.RTCSessionDescription ||
381 | window.webkitRTCSessionDescription ||
382 | window.mozRTCSessionDescription ||
383 | window.msRTCSessionDescription;
384 |
385 | this.pc.setRemoteDescription(
386 | new RTCSessionDescription(message),
387 |
388 | function () {},
389 |
390 | function (error) {
391 | console.log("WebRTCPeer.setRemoteDescription: " + error);
392 | self.invokeErrorListeners(error);
393 | }
394 | );
395 | }
396 |
397 | /**
398 | * Sets up channel listeners.
399 | */
400 | setupChannelListener() {
401 | var self = this;
402 |
403 | // received data from a remote peer
404 | this.channel.onmessage = function (event) {
405 | self.invokeReceiveListeners(JSON.parse(event.data));
406 | };
407 |
408 | // connected with a remote peer
409 | this.channel.onopen = function (event) {
410 | self.open = true;
411 |
412 | self.invokeOpenListeners(self.peer);
413 | };
414 |
415 | // disconnected from a remote peer
416 | this.channel.onclose = function (event) {
417 | if (!self.open) return;
418 |
419 | self.open = false;
420 |
421 | self.invokeCloseListeners(self.peer);
422 | };
423 |
424 | // error occurred with a remote peer
425 | this.channel.onerror = function (error) {
426 | self.invokeErrorListeners(error);
427 | };
428 | }
429 |
430 | // event listeners, refer to .addEventListeners() comment for the arguments.
431 |
432 | invokeOpenListeners(id) {
433 | for (var i = 0, il = this.onOpens.length; i < il; i++) {
434 | this.onOpens[i](id);
435 | }
436 | }
437 |
438 | invokeCloseListeners(id) {
439 | for (var i = 0, il = this.onCloses.length; i < il; i++) {
440 | this.onCloses[i](id);
441 | }
442 | }
443 |
444 | invokeErrorListeners(error) {
445 | for (var i = 0, il = this.onErrors.length; i < il; i++) {
446 | this.onErrors[i](error);
447 | }
448 | }
449 |
450 | invokeReceiveListeners(message) {
451 | for (var i = 0, il = this.onReceives.length; i < il; i++) {
452 | this.onReceives[i](message);
453 | }
454 | }
455 |
456 | invokeReceiveStreamListeners(stream) {
457 | for (var i = 0, il = this.onReceiveStreams.length; i < il; i++) {
458 | this.onReceiveStreams[i](stream);
459 | }
460 | }
461 |
462 | // public
463 |
464 | /**
465 | * Sends connection request (offer) to a remote peer.
466 | */
467 | offer() {
468 | var self = this;
469 |
470 | this.channel = this.pc.createDataChannel("mychannel", { reliable: false });
471 |
472 | this.setupChannelListener();
473 |
474 | this.pc.createOffer(
475 | function (sdp) {
476 | self.handleSessionDescription(sdp);
477 | },
478 |
479 | function (error) {
480 | console.error(error);
481 | self.onError(error);
482 | }
483 | );
484 | }
485 |
486 | /**
487 | * Sends data to a remote peer.
488 | * @param {anything} data
489 | */
490 | send(data) {
491 | // TODO: throw error?
492 | if (this.channel === null || this.channel.readyState !== "open") return;
493 |
494 | this.channel.send(JSON.stringify(data));
495 | }
496 |
497 | /**
498 | * Handles signal sent from a remote peer via server.
499 | * @param {object} signal - must have .peer as destination peer id and .id as source peer id
500 | */
501 | handleSignal(signal) {
502 | // ignores signal if it isn't for me
503 | if (this.id !== signal.peer || this.peer !== signal.id) return;
504 |
505 | switch (signal.type) {
506 | case "offer":
507 | this.handleOffer(signal);
508 | break;
509 |
510 | case "answer":
511 | this.handleAnswer(signal);
512 | break;
513 |
514 | case "candidate":
515 | this.handleCandidate(signal);
516 | break;
517 |
518 | default:
519 | console.log("WebRTCPeer: Unknown signal type " + signal.type);
520 | break;
521 | }
522 | }
523 | }
524 |
525 | export { WebRTCPeer, WebRTCClient };
526 |
--------------------------------------------------------------------------------
/src/engine/physics/physics.js:
--------------------------------------------------------------------------------
1 | import {
2 | World,
3 | NaiveBroadphase,
4 | Body,
5 | Plane,
6 | Box,
7 | Sphere,
8 | Cylinder,
9 | Vec3,
10 | } from "cannon";
11 | import CannonDebugRenderer from "../util/debughelpers/cannondebugrenderer";
12 | import { Vector3 } from "three";
13 | import State from "../state";
14 | import XRInput from "../../engine/xrinput";
15 |
16 | const TIMESTEP = 1 / 120;
17 | const YGRAVITY = -9.81;
18 |
19 | // Physics singleton
20 | const Physics = {
21 | rigidbodies: new Array(),
22 | controllerRigidbodies: new Array(),
23 | RigidBodyShape: {
24 | Box: 1,
25 | Sphere: 2,
26 | Plane: 3,
27 | Cylinder: 4,
28 | },
29 | Body: Body,
30 | };
31 |
32 | // Init Physics
33 | Physics.cannonWorld = new World();
34 |
35 | Physics.cannonWorld.broadphase = new NaiveBroadphase();
36 | Physics.cannonWorld.gravity.set(0, YGRAVITY, 0);
37 | Physics.cannonWorld.solver.iterations = 50; //50
38 | Physics.cannonWorld.solver.tolerance = 0;
39 |
40 | if (State.debugMode) console.log("CannonJS world created");
41 |
42 | Physics.addControllerRigidBody = controller => {
43 | const _cRB = new Body({
44 | mass: 0,
45 | type: Body.KINEMATIC,
46 | });
47 | _cRB.name =
48 | "Controller " + Physics.controllerRigidbodies.length + " RigidBody";
49 | _cRB.collisionResponse = 1;
50 | // _cRB.addEventListener("collide", function (e) { console.log("controller collided!"); });
51 | _cRB.addShape(new Sphere(0.075));
52 | Physics.cannonWorld.add(_cRB);
53 | Physics.controllerRigidbodies.push(_cRB);
54 | Physics.rigidbodies.push(_cRB);
55 | if (State.debugMode) console.log(_cRB.name + " created");
56 | };
57 |
58 | Physics.enableDebugger = scene => {
59 | Physics.debugRenderer = new CannonDebugRenderer(scene, Physics.cannonWorld);
60 | };
61 |
62 | Physics.updateControllers = () => {
63 | if (
64 | XRInput.controllerGrips == null ||
65 | XRInput.controllerGrips.length == 0 ||
66 | Physics.controllerRigidbodies.length == 0
67 | )
68 | return;
69 | XRInput.controllerGrips.forEach((ctrl, i) => {
70 | Physics.controllerRigidbodies[i].position.copy(
71 | XRInput.controllerGrips[i].position
72 | );
73 | Physics.controllerRigidbodies[i].quaternion.copy(
74 | XRInput.controllerGrips[i].quaternion
75 | );
76 | });
77 | };
78 |
79 | Physics.Update = () => {
80 | // sendDataToWorker();
81 | if (Physics.rigidbodies.length < 1) return;
82 |
83 | // run sim
84 | Physics.cannonWorld.step(TIMESTEP);
85 |
86 | Physics.updateControllers();
87 |
88 | // sync w/scene objects
89 | Physics.cannonWorld.bodies.forEach((body, i) => {
90 | if (Physics.cannonWorld.bodies[i].type == Body.KINEMATIC) return;
91 | Physics.rigidbodies[i].quaternion.copy(body.quaternion);
92 | Physics.rigidbodies[i].position.copy(body.position);
93 | });
94 |
95 | if (Physics.debugRenderer != undefined)
96 | Physics.debugRenderer.update(State.debugMode);
97 | };
98 |
99 | Physics.resetScene = () => {
100 | const bodies = Physics.cannonWorld.bodies;
101 | let i = bodies.length;
102 | while (i--) {
103 | this.removeBody(bodies[i]);
104 | }
105 | };
106 |
107 | Physics.addRigidBody = (mesh, rbShape, type = Body.DYNAMIC, mass = 1) => {
108 | if (mesh.geometry == undefined) {
109 | if (State.debugMode)
110 | console.warn(
111 | "no mesh geometry found for " +
112 | mesh.type +
113 | ", aborting rigibdoy creation"
114 | );
115 | return;
116 | }
117 |
118 | mesh.geometry.computeBoundingBox();
119 | const bbSize = new Vector3();
120 | mesh.geometry.boundingBox.getSize(bbSize);
121 | bbSize.divideScalar(2);
122 |
123 | let shape;
124 |
125 | switch (rbShape) {
126 | case Physics.RigidBodyShape.Box:
127 | shape = new Box(new Vec3(bbSize.x, bbSize.y, bbSize.z));
128 | break;
129 |
130 | case Physics.RigidBodyShape.Sphere:
131 | shape = new Sphere(Math.max(bbSize.x, bbSize.y, bbSize.z));
132 | break;
133 |
134 | case Physics.RigidBodyShape.Plane:
135 | shape = new Plane();
136 | break;
137 |
138 | case Physics.RigidBodyShape.Cylinder:
139 | const minSize = Math.min(bbSize.x, bbSize.y, bbSize.z);
140 | const maxSize = Math.max(bbSize.x, bbSize.y, bbSize.z) * 2;
141 | shape = new Cylinder(minSize, minSize, maxSize, 16);
142 | break;
143 |
144 | default:
145 | console.error(
146 | "Physics.addBody: No matching rigidbody found! See Physics.RigidBody object for options"
147 | );
148 | break;
149 | }
150 |
151 | const body = new Body({
152 | mass: mass,
153 | type: type,
154 | allowSleep: true,
155 | sleepSpeedLimit: 1.0,
156 | });
157 |
158 | body.addShape(shape);
159 | body.position.copy(mesh.position);
160 | body.quaternion.copy(mesh.quaternion);
161 | Physics.cannonWorld.addBody(body);
162 |
163 | body.addEventListener("collide", function (e) {
164 | if (State.debugMode) console.log("body collided");
165 | });
166 | Physics.rigidbodies.push(mesh);
167 | return body;
168 | };
169 |
170 | // reset rigidbody completely
171 |
172 | Physics.resetRigidbody = body => {
173 | // Position
174 | body.position.setZero();
175 | body.previousPosition.setZero();
176 | body.interpolatedPosition.setZero();
177 | body.initPosition.setZero();
178 |
179 | // orientation
180 | body.quaternion.set(0, 0, 0, 1);
181 | body.initQuaternion.set(0, 0, 0, 1);
182 | // body.previousQuaternion.set(0, 0, 0, 1);
183 | // body.interpolatedQuaternion.set(0, 0, 0, 1);
184 |
185 | // Velocity
186 | body.velocity.setZero();
187 | body.initVelocity.setZero();
188 | body.angularVelocity.setZero();
189 | body.initAngularVelocity.setZero();
190 |
191 | // Force
192 | body.force.setZero();
193 | body.torque.setZero();
194 |
195 | // Sleep state reset
196 | body.sleepState = 0;
197 | body.timeLastSleepy = 0;
198 | body._wakeUpAfterNarrowphase = false;
199 | };
200 |
201 | export default Physics;
202 |
--------------------------------------------------------------------------------
/src/engine/physics/physics.worker.js:
--------------------------------------------------------------------------------
1 | // TODO: IMPLEMENT WEBWORKER
2 | // basic physics.js future code:
3 |
4 | // const PhysicsSolver = new PhysicsSolver();
5 | // PhysicsSolver.postMessage = PhysicsSolver.webkitPostMessage || PhysicsSolver.postMessage;
6 |
7 | // const sendDataToWorker = () =>
8 | // {
9 | // PhysicsSolver.postMessage({
10 | // // N: N,
11 | // });
12 | // }
13 |
14 | // PhysicsSolver.addEventListener('message', worker =>
15 | // {
16 | // scene.children.forEach(child =>
17 | // {
18 | // if (child.Physics)
19 | // {
20 | // child.position.copy(worker.data.positions);
21 | // child.quaternion.copy(worker.data.quaternions);
22 | // }
23 | // });
24 | // });
25 |
26 | //////////////////////////////////////////
27 |
28 | // webworker code:
29 | // import * as CANNON from "cannon";
30 |
31 | // // let cannonWorld;
32 | // const TIMESTEP = 1 / 60;
33 | // const YGRAVITY = 0;
34 |
35 | // let shape, body;
36 |
37 | // shape = new CANNON.Box(new CANNON.Vec3(.5, .5, .5));
38 | // body = new CANNON.Body({ mass: 1 });
39 | // body.position.set(0, 1, -3);
40 | // body.addShape(shape);
41 | // body.angularVelocity.set(0, 10, 0);
42 | // body.angularDamping = 0.05;
43 |
44 |
45 |
46 |
47 | // function addBody(mesh)
48 | // {
49 | // const shape = new CANNON.Box(new CANNON.Vec3(.5, .5, .5));
50 | // const body = new CANNON.Body({ mass: 1 });
51 | // body.position.set(mesh.position);
52 | // body.addShape(shape);
53 | // cannonWorld.addBody(body);
54 | // }
55 |
56 | // onmessage = function (e)
57 | // {
58 | // if (cannonWorld == null) return;
59 |
60 |
61 | // cannonWorld.bodies.forEach(e =>
62 | // {
63 | // console.log(e);
64 | // });
65 | // var positions = body.position;
66 | // var rotations = body.quaternion;
67 | // this.postMessage({ positions: positions, quaternions: rotations });
68 | // }
69 |
--------------------------------------------------------------------------------
/src/engine/renderer.js:
--------------------------------------------------------------------------------
1 | import { WebGLRenderer } from "three";
2 |
3 | const Renderer = new WebGLRenderer({ antialias: true, alpha: true });
4 | Renderer.setPixelRatio(window.devicePixelRatio);
5 | Renderer.setSize(window.innerWidth, window.innerHeight);
6 | Renderer.setClearColor(0x000000, 0.0);
7 | Renderer.sortObjects = false;
8 | Renderer.physicallyCorrectLights = true;
9 | Renderer.xr.enabled = true;
10 |
11 | export default Renderer;
12 |
--------------------------------------------------------------------------------
/src/engine/state.js:
--------------------------------------------------------------------------------
1 | //helper class for custom XR events
2 | class Event {
3 | constructor(name) {
4 | this.name = name;
5 | this.callbacks = [];
6 | }
7 | registerCallback(callback) {
8 | this.callbacks.push(callback);
9 | }
10 | }
11 |
12 | class EventHandler {
13 | constructor() {
14 | this.events = {};
15 | }
16 |
17 | registerEvent(eventName) {
18 | var event = new Event(eventName);
19 | this.events[eventName] = event;
20 | }
21 |
22 | dispatchEvent(eventName, eventArgs) {
23 | this.events[eventName].callbacks.forEach(function (callback) {
24 | callback(eventArgs);
25 | });
26 | }
27 |
28 | addEventListener(eventName, callback) {
29 | this.events[eventName].registerCallback(callback);
30 | }
31 | }
32 |
33 | // main state singleton
34 | class StateClass {
35 | constructor() {
36 | this.globals = {};
37 | this.isPrimary = true; // until claimed otherwise by a PeerConnection
38 | this.isXRSession = false;
39 | this.isPaused = false;
40 | this.currentSession = null;
41 | this.debugMode = true;
42 | this.eventHandler = new EventHandler();
43 | this.eventHandler.registerEvent("xrsessionstarted");
44 | this.eventHandler.registerEvent("xrsessionended");
45 | this.eventHandler.registerEvent("inputsourceschange");
46 | this.eventHandler.registerEvent("selectend");
47 | this.eventHandler.registerEvent("selectstart");
48 | this.eventHandler.registerEvent("select");
49 | this.eventHandler.registerEvent("squeezeend");
50 | this.eventHandler.registerEvent("squeezestart");
51 | this.eventHandler.registerEvent("squeeze");
52 | this.eventHandler.registerEvent("peerconnected");
53 | this.eventHandler.registerEvent("peerdisconnected");
54 | this.bindKeys();
55 | }
56 |
57 | bindKeys() {
58 | document.addEventListener("keydown", e => {
59 | if (!e.shiftKey) return;
60 |
61 | switch (e.keyCode) {
62 | case 192: // tilde
63 | this.debugMode = !this.debugMode;
64 | console.log("Debug: " + this.debugMode);
65 | break;
66 | case 80: //"p"
67 | this.isPaused = !this.isPaused;
68 | console.log("Paused: " + this.isPaused);
69 | break;
70 | default:
71 | break;
72 | }
73 | });
74 | }
75 | }
76 |
77 | const State = new StateClass();
78 | export default State;
79 |
--------------------------------------------------------------------------------
/src/engine/util/cameracontrols/engineeditorcamera.js:
--------------------------------------------------------------------------------
1 | import { Vector3, Euler, Object3D } from "three";
2 |
3 | class EngineEditorCamera extends Object3D {
4 | constructor(camera, domElement, params) {
5 | super(params);
6 | this.camera = camera;
7 | this.domElement = domElement;
8 | this.PI_2 = Math.PI / 2;
9 | this.euler = new Euler(0, 0, 0, "YXZ");
10 | this.vec = new Vector3();
11 | this.cameraForward = new Vector3();
12 | this._rcPressed = false;
13 | this.pressedKeyMap = {
14 | 87: false, // w
15 | 65: false, // a
16 | 83: false, // s
17 | 68: false, // d
18 | 81: false, // q
19 | 69: false, // e
20 | 16: false, // shift
21 | };
22 |
23 | window.addEventListener("keydown", this.onKeyDown.bind(this), false);
24 | window.addEventListener("keyup", this.onKeyUp.bind(this), false);
25 | window.addEventListener("mousedown", this.onMouseDown.bind(this), false);
26 | window.addEventListener("mouseup", this.onMouseUp.bind(this), false);
27 | window.addEventListener("mousemove", this.onMouseMove.bind(this), false);
28 | window.addEventListener("wheel", this.onMouseWheel.bind(this), false);
29 |
30 | //disable rClick
31 | document.oncontextmenu = e => {
32 | if (e.preventDefault != undefined) e.preventDefault();
33 | if (e.stopPropagation != undefined) e.stopPropagation();
34 | };
35 | this.retrieveSessionData();
36 | }
37 |
38 | setSessionData() {
39 | this.editorCamState = {
40 | camSpeed: this.CAM_SPEED,
41 | cameraPosition: this.camera.position,
42 | cameraQuaternion: this.camera.quaternion,
43 | };
44 | window.sessionStorage.setItem(
45 | "camState",
46 | JSON.stringify(this.editorCamState)
47 | );
48 | // console.log("saving editor camera session data");
49 | }
50 |
51 | retrieveSessionData() {
52 | const camState = JSON.parse(window.sessionStorage.getItem("camState"));
53 | if (camState == null) {
54 | this.CAM_SPEED = 0.015;
55 | this.setSessionData();
56 | return;
57 | }
58 |
59 | this.CAM_SPEED = "camSpeed" in camState ? camState["camSpeed"] : 0.015;
60 | this.camera.position.copy(
61 | "cameraPosition" in camState ? camState["cameraPosition"] : new Vector3()
62 | );
63 | this.camera.applyQuaternion(
64 | "cameraQuaternion" in camState
65 | ? camState["cameraQuaternion"]
66 | : new Vector3()
67 | );
68 | // console.log("loading editor camera session data");
69 | }
70 |
71 | Update() {
72 | this.shiftSpeedMulti = this.pressedKeyMap[16] ? 2 : 1;
73 | if (this.pressedKeyMap[87])
74 | this.moveForward(this.CAM_SPEED * this.shiftSpeedMulti);
75 | if (this.pressedKeyMap[83])
76 | this.moveForward(-this.CAM_SPEED * this.shiftSpeedMulti);
77 | if (this.pressedKeyMap[69])
78 | this.moveUp(this.CAM_SPEED * this.shiftSpeedMulti);
79 | if (this.pressedKeyMap[81])
80 | this.moveUp(-this.CAM_SPEED * this.shiftSpeedMulti);
81 | if (this.pressedKeyMap[68])
82 | this.moveRight(this.CAM_SPEED * this.shiftSpeedMulti);
83 | if (this.pressedKeyMap[65])
84 | this.moveRight(-this.CAM_SPEED * this.shiftSpeedMulti);
85 | }
86 |
87 | onKeyDown(event) {
88 | this.pressedKeyMap[event.keyCode] = event.keyCode in this.pressedKeyMap;
89 | }
90 |
91 | onKeyUp(event) {
92 | this.pressedKeyMap[event.keyCode] = !(event.keyCode in this.pressedKeyMap);
93 | }
94 |
95 | onMouseDown(event) {
96 | if (event.button == 2) {
97 | this._rcPressed = true;
98 | this.domElement.requestPointerLock =
99 | this.domElement.requestPointerLock || domElement.mozRequestPointerLock;
100 | this.domElement.requestPointerLock();
101 | }
102 | }
103 |
104 | onMouseUp(event) {
105 | if (event.button == 2) {
106 | this._rcPressed = false;
107 | document.exitPointerLock();
108 | this.setSessionData();
109 | }
110 | }
111 |
112 | moveForward(distance) {
113 | this.camera.getWorldDirection(this.cameraForward);
114 | this.camera.position.addScaledVector(this.cameraForward, distance);
115 | }
116 |
117 | moveRight(distance) {
118 | this.vec.setFromMatrixColumn(this.camera.matrix, 0);
119 | this.camera.position.addScaledVector(this.vec, distance);
120 | }
121 |
122 | moveUp(distance) {
123 | this.camera.position.addScaledVector(this.camera.up, distance);
124 | }
125 |
126 | onMouseMove(event) {
127 | if (!this._rcPressed) return;
128 | const movementX =
129 | event.movementX || event.mozMovementX || event.webkitMovementX || 0;
130 | const movementY =
131 | event.movementY || event.mozMovementY || event.webkitMovementY || 0;
132 | this.euler.setFromQuaternion(this.camera.quaternion);
133 | this.euler.y -= movementX * 0.002;
134 | this.euler.x -= movementY * 0.002;
135 | this.euler.x = Math.max(-this.PI_2, Math.min(this.PI_2, this.euler.x));
136 | this.camera.quaternion.setFromEuler(this.euler);
137 | }
138 |
139 | onMouseWheel(event) {
140 | if (this._rcPressed)
141 | this.CAM_SPEED = Math.max(
142 | 0.01,
143 | (this.CAM_SPEED -= event.deltaY * 0.0001)
144 | );
145 | else this.moveForward((this.CAM_SPEED * -event.deltaY) / 10);
146 | }
147 | }
148 |
149 | export default EngineEditorCamera;
150 |
--------------------------------------------------------------------------------
/src/engine/util/debughelpers/cannondebugrenderer.js:
--------------------------------------------------------------------------------
1 | import {
2 | MeshBasicMaterial,
3 | SphereGeometry,
4 | BoxGeometry,
5 | PlaneGeometry,
6 | CylinderGeometry,
7 | Mesh,
8 | Geometry,
9 | Vector3,
10 | Face3,
11 | } from "three";
12 | import {
13 | Vec3,
14 | Shape,
15 | Sphere,
16 | Box,
17 | Plane,
18 | ConvexPolyhedron,
19 | Trimesh,
20 | Heightfield,
21 | World,
22 | } from "cannon";
23 |
24 | /* global CANNON,THREE,Detector */
25 |
26 | /**
27 | * Adds Three.js primitives into the scene where all the Cannon bodies and shapes are.
28 | * @class CannonDebugRenderer
29 | * @param {THREE.Scene} scene
30 | * @param {CANNON.World} world
31 | * @param {object} [options]
32 | */
33 |
34 | class CannonDebugRenderer {
35 | constructor(scene, world, options) {
36 | options = options || {};
37 |
38 | this.scene = scene;
39 | this.world = world;
40 |
41 | this._meshes = [];
42 |
43 | this._material = new MeshBasicMaterial({
44 | color: 0x00ff00,
45 | wireframe: true,
46 | });
47 | this._sphereGeometry = new SphereGeometry(1);
48 | this._boxGeometry = new BoxGeometry(1, 1, 1);
49 | this._planeGeometry = new PlaneGeometry(10, 10, 10, 10);
50 | this._cylinderGeometry = new CylinderGeometry(1, 1, 10, 10);
51 |
52 | this.tmpVec0 = new Vec3();
53 | this.tmpVec1 = new Vec3();
54 | this.tmpVec2 = new Vec3();
55 | this.tmpQuat0 = new Vec3();
56 | }
57 |
58 | update(debugPhysics) {
59 | if (!debugPhysics) {
60 | if (this._meshes.length > 0) {
61 | this._meshes.forEach(e => {
62 | this.scene.remove(e);
63 | });
64 | this._meshes.length = 0;
65 | }
66 | } else {
67 | var bodies = this.world.bodies;
68 | var meshes = this._meshes;
69 | var shapeWorldPosition = this.tmpVec0;
70 | var shapeWorldQuaternion = this.tmpQuat0;
71 |
72 | var meshIndex = 0;
73 |
74 | for (var i = 0; i !== bodies.length; i++) {
75 | var body = bodies[i];
76 |
77 | for (var j = 0; j !== body.shapes.length; j++) {
78 | var shape = body.shapes[j];
79 |
80 | this._updateMesh(meshIndex, body, shape);
81 |
82 | var mesh = meshes[meshIndex];
83 |
84 | if (mesh) {
85 | // Get world position
86 | body.quaternion.vmult(body.shapeOffsets[j], shapeWorldPosition);
87 | body.position.vadd(shapeWorldPosition, shapeWorldPosition);
88 |
89 | // Get world quaternion
90 | body.quaternion.mult(
91 | body.shapeOrientations[j],
92 | shapeWorldQuaternion
93 | );
94 |
95 | // Copy to meshes
96 | mesh.position.copy(shapeWorldPosition);
97 | mesh.quaternion.copy(shapeWorldQuaternion);
98 | }
99 |
100 | meshIndex++;
101 | }
102 | }
103 |
104 | for (var i = meshIndex; i < meshes.length; i++) {
105 | var mesh = meshes[i];
106 | if (mesh) {
107 | this.scene.remove(mesh);
108 | }
109 | }
110 |
111 | meshes.length = meshIndex;
112 | }
113 | }
114 |
115 | _updateMesh(index, body, shape) {
116 | var mesh = this._meshes[index];
117 | if (!this._typeMatch(mesh, shape)) {
118 | if (mesh) {
119 | this.scene.remove(mesh);
120 | }
121 | mesh = this._meshes[index] = this._createMesh(shape);
122 | }
123 | this._scaleMesh(mesh, shape);
124 | }
125 |
126 | _typeMatch(mesh, shape) {
127 | if (!mesh) {
128 | return false;
129 | }
130 | var geo = mesh.geometry;
131 | return (
132 | (geo instanceof SphereGeometry && shape instanceof Sphere) ||
133 | (geo instanceof BoxGeometry && shape instanceof Box) ||
134 | (geo instanceof PlaneGeometry && shape instanceof Plane) ||
135 | (geo.id === shape.geometryId && shape instanceof ConvexPolyhedron) ||
136 | (geo.id === shape.geometryId && shape instanceof Trimesh) ||
137 | (geo.id === shape.geometryId && shape instanceof Heightfield)
138 | );
139 | }
140 |
141 | _createMesh(shape) {
142 | var mesh;
143 | var material = this._material;
144 |
145 | switch (shape.type) {
146 | case Shape.types.SPHERE:
147 | mesh = new Mesh(this._sphereGeometry, material);
148 | break;
149 |
150 | case Shape.types.BOX:
151 | mesh = new Mesh(this._boxGeometry, material);
152 | break;
153 |
154 | case Shape.types.PLANE:
155 | mesh = new Mesh(this._planeGeometry, material);
156 | break;
157 |
158 | case Shape.types.CONVEXPOLYHEDRON:
159 | // Create mesh
160 | var geo = new Geometry();
161 |
162 | // Add vertices
163 | for (var i = 0; i < shape.vertices.length; i++) {
164 | var v = shape.vertices[i];
165 | geo.vertices.push(new Vector3(v.x, v.y, v.z));
166 | }
167 |
168 | for (var i = 0; i < shape.faces.length; i++) {
169 | var face = shape.faces[i];
170 |
171 | // add triangles
172 | var a = face[0];
173 | for (var j = 1; j < face.length - 1; j++) {
174 | var b = face[j];
175 | var c = face[j + 1];
176 | geo.faces.push(new Face3(a, b, c));
177 | }
178 | }
179 | geo.computeBoundingSphere();
180 | geo.computeFaceNormals();
181 |
182 | mesh = new Mesh(geo, material);
183 | shape.geometryId = geo.id;
184 | break;
185 |
186 | case Shape.types.TRIMESH:
187 | var geometry = new Geometry();
188 | var v0 = this.tmpVec0;
189 | var v1 = this.tmpVec1;
190 | var v2 = this.tmpVec2;
191 | for (var i = 0; i < shape.indices.length / 3; i++) {
192 | shape.getTriangleVertices(i, v0, v1, v2);
193 | geometry.vertices.push(
194 | new Vector3(v0.x, v0.y, v0.z),
195 | new Vector3(v1.x, v1.y, v1.z),
196 | new Vector3(v2.x, v2.y, v2.z)
197 | );
198 | var j = geometry.vertices.length - 3;
199 | geometry.faces.push(new Face3(j, j + 1, j + 2));
200 | }
201 | geometry.computeBoundingSphere();
202 | geometry.computeFaceNormals();
203 | mesh = new Mesh(geometry, material);
204 | shape.geometryId = geometry.id;
205 | break;
206 |
207 | case Shape.types.HEIGHTFIELD:
208 | var geometry = new Geometry();
209 |
210 | var v0 = this.tmpVec0;
211 | var v1 = this.tmpVec1;
212 | var v2 = this.tmpVec2;
213 | for (var xi = 0; xi < shape.data.length - 1; xi++) {
214 | for (var yi = 0; yi < shape.data[xi].length - 1; yi++) {
215 | for (var k = 0; k < 2; k++) {
216 | shape.getConvexTrianglePillar(xi, yi, k === 0);
217 | v0.copy(shape.pillarConvex.vertices[0]);
218 | v1.copy(shape.pillarConvex.vertices[1]);
219 | v2.copy(shape.pillarConvex.vertices[2]);
220 | v0.vadd(shape.pillarOffset, v0);
221 | v1.vadd(shape.pillarOffset, v1);
222 | v2.vadd(shape.pillarOffset, v2);
223 | geometry.vertices.push(
224 | new Vector3(v0.x, v0.y, v0.z),
225 | new Vector3(v1.x, v1.y, v1.z),
226 | new Vector3(v2.x, v2.y, v2.z)
227 | );
228 | var i = geometry.vertices.length - 3;
229 | geometry.faces.push(new Face3(i, i + 1, i + 2));
230 | }
231 | }
232 | }
233 | geometry.computeBoundingSphere();
234 | geometry.computeFaceNormals();
235 | mesh = new Mesh(geometry, material);
236 | shape.geometryId = geometry.id;
237 | break;
238 | }
239 |
240 | if (mesh) {
241 | this.scene.add(mesh);
242 | }
243 |
244 | return mesh;
245 | }
246 |
247 | _scaleMesh(mesh, shape) {
248 | switch (shape.type) {
249 | case Shape.types.SPHERE:
250 | var radius = shape.radius;
251 | mesh.scale.set(radius, radius, radius);
252 | break;
253 |
254 | case Shape.types.BOX:
255 | mesh.scale.copy(shape.halfExtents);
256 | mesh.scale.multiplyScalar(2);
257 | break;
258 |
259 | case Shape.types.CONVEXPOLYHEDRON:
260 | mesh.scale.set(1, 1, 1);
261 | break;
262 |
263 | case Shape.types.TRIMESH:
264 | mesh.scale.copy(shape.scale);
265 | break;
266 |
267 | case Shape.types.HEIGHTFIELD:
268 | mesh.scale.set(1, 1, 1);
269 | break;
270 | }
271 | }
272 | }
273 |
274 | export default CannonDebugRenderer;
275 |
--------------------------------------------------------------------------------
/src/engine/util/hostbot.js:
--------------------------------------------------------------------------------
1 | import Speech from "speak-tts";
2 | import State from "../state";
3 |
4 | const messageType = { log: 1, warn: 2, error: 3 };
5 |
6 | class HostBot {
7 | constructor(networking) {
8 | this.speech = new Speech();
9 | this.networking = networking;
10 | this.scene = networking.scene;
11 | this.speech
12 | .init({
13 | volume: 1,
14 | lang: "en-US",
15 | rate: 1,
16 | pitch: 1,
17 | voice: "Microsoft Zira Desktop - English (United States)",
18 | splitSentences: true,
19 | })
20 | .then(data => {})
21 | .catch(e => {
22 | console.error("An error occured while initializing : ", e);
23 | });
24 | this.networking.remoteSync.addEventListener("open", this.onOpen.bind(this));
25 | this.networking.remoteSync.addEventListener(
26 | "close",
27 | this.onClose.bind(this)
28 | );
29 | this.networking.remoteSync.addEventListener(
30 | "error",
31 | this.onError.bind(this)
32 | );
33 | this.networking.remoteSync.addEventListener(
34 | "connect",
35 | this.onConnect.bind(this)
36 | );
37 | this.networking.remoteSync.addEventListener(
38 | "disconnect",
39 | this.onDisconnect.bind(this)
40 | );
41 | if (State.debugMode) console.log("HostBot ready");
42 | // this.networking.remoteSync.addEventListener('receive', this.onReceive.bind(this));
43 | // this.networking.remoteSync.addEventListener('add', this.onAdd.bind(this));
44 | // this.networking.remoteSync.addEventListener('remove', this.onRemove.bind(this));
45 | }
46 |
47 | onOpen() {
48 | // this.log("Connected to Signaling server", messageType.log);
49 | }
50 | onClose() {
51 | this.log("Disconnected from Signaling server", messageType.log);
52 | }
53 | onConnect() {
54 | this.log("A new player has joined!", messageType.log);
55 | }
56 |
57 | onDisconnect() {
58 | this.log("Player disconnected!", messageType.log);
59 | }
60 | onError(e) {
61 | const errorMsg = "Error!" + e.error.message;
62 | this.log(errorMsg, messageType.error);
63 | }
64 |
65 | log(message, type) {
66 | this.speech.speak({ text: message });
67 | if (!State.debugMode) return;
68 |
69 | switch (type) {
70 | default:
71 | case messageType.log:
72 | console.log(message);
73 | break;
74 |
75 | case messageType.warn:
76 | console.warn(message);
77 | break;
78 |
79 | case messageType.error:
80 | console.error(message);
81 | break;
82 | }
83 | }
84 | }
85 |
86 | export default HostBot;
87 |
--------------------------------------------------------------------------------
/src/engine/util/webxr/raycaster.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Raycaster,
3 | Vector3,
4 | BufferGeometry,
5 | LineBasicMaterial,
6 | Line,
7 | Matrix4,
8 | } from "three";
9 |
10 | /** Three.js Line, extended with an Update method */
11 | class SCLine extends Line {
12 | constructor(
13 | bufferGeo: THREE.BufferGeometry,
14 | material: THREE.LineBasicMaterial
15 | ) {
16 | super(bufferGeo, material);
17 | }
18 | Update(): void {}
19 | }
20 |
21 | /**
22 | * Sandcastle's own Raycaster; WebXR-friendly syntactic sugar over Three.js's Raycaster
23 | * @extends Raycaster
24 | */
25 | export class SCRaycaster extends Raycaster {
26 | _originObject: THREE.Mesh | THREE.Group;
27 | _target: THREE.Mesh | Array | THREE.Box3;
28 | _direction: THREE.Vector3;
29 | _isRecursive: boolean;
30 | _near: number;
31 | _far: number;
32 | _visualizedRaycast: THREE.Line | undefined;
33 | _isTargetArray: boolean;
34 | _isTargetBox3: boolean;
35 | _tempMatrix: THREE.Matrix4;
36 | /**
37 | * Construct a Sandcastle Raycaster.
38 | * @param originObject - object to raycast from
39 | * @param target - object(s) to raycast to. Can be a Mesh, a Mesh array or a Box3.
40 | * @param [direction] - The normalized direction vector that gives direction to the ray. Default value is new Vector3(0,0,-1).
41 | * @param [isRecursive] - If true, it also checks all descendants. Otherwise it only checks intersection with the object. Default is true.
42 | * @param [near] - All results returned are further away than near. Near can't be negative. Default value is 0.1.
43 | * @param [far] - All results returned are closer than far. Far can't be lower than near. Default value is 10.
44 | */
45 | constructor(
46 | originObject: THREE.Mesh | THREE.Group,
47 | target: THREE.Mesh | Array | THREE.Box3,
48 | direction: THREE.Vector3 = new Vector3(0, 0, -1),
49 | isRecursive: boolean = true,
50 | near: number = 0.1,
51 | far: number = 10
52 | ) {
53 | super();
54 | if (!originObject.parent || originObject.parent.type != "Scene") {
55 | throw new Error("Error: the raycasting object is not in the scene!");
56 | }
57 |
58 | this._tempMatrix = new Matrix4();
59 | this._originObject = originObject;
60 | this._target = target;
61 | this._direction = direction;
62 | this._isRecursive = isRecursive;
63 | this._near = near;
64 | this._far = far;
65 | this._visualizedRaycast = undefined;
66 | this._isTargetArray = Array.isArray(this._target); // cache check because it will impact every frame
67 | this._isTargetBox3 = this._target.hasOwnProperty("isBox3");
68 | }
69 |
70 | /** Get intersections of origin object with target object or array.
71 | * Usually run within the update loop or as the result of an event.
72 | * Will return a bool if intersects against a Box3, and an array if intersecting against scene objects.
73 | * */
74 | getIntersections(): any {
75 | this._tempMatrix.identity().extractRotation(this._originObject.matrixWorld);
76 | this.ray.origin.setFromMatrixPosition(this._originObject.matrixWorld);
77 | this.ray.direction.set(0, 0, -1).applyMatrix4(this._tempMatrix);
78 |
79 | return this._isTargetBox3
80 | ? this.ray.intersectsBox(this._target as THREE.Box3)
81 | : this._isTargetArray
82 | ? this.intersectObjects(this._target as [THREE.Mesh], this._isRecursive)
83 | : this.intersectObject(this._target as THREE.Mesh, this._isRecursive);
84 | }
85 |
86 | /**
87 | * A helper method for visualizing raycaster rays
88 | * @param color - visualizing ray color
89 | * @param onlyWhenHit - whether ray should be visualized only when a raycast hits the target or always
90 | */
91 | visualize(color = 0xffffff, onlyWhenHit = false): void {
92 | const lineGeo = new BufferGeometry().setFromPoints([
93 | new Vector3(0, 0, 0),
94 | new Vector3(0, 0, -1),
95 | ]);
96 | const lineMat = new LineBasicMaterial({ color });
97 | const _visualizedRaycast = new SCLine(lineGeo, lineMat);
98 | _visualizedRaycast.name = "line";
99 | if (onlyWhenHit) {
100 | _visualizedRaycast.Update = () => {
101 | _visualizedRaycast.visible =
102 | this.getIntersections().length > 0 || this.getIntersections() == true;
103 | };
104 | }
105 | this._originObject.add(_visualizedRaycast);
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/engine/util/webxr/sessionhandler.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author mrdoob / http://mrdoob.com
3 | * @author Mugen87 / https://github.com/Mugen87
4 | * modified for Sandcastle by @author MichaelHazani / https://github.com/MichaelHazani
5 | */
6 |
7 | import State from "../../state";
8 | import Renderer from "../../renderer";
9 |
10 | class SessionHandler {
11 | constructor() {
12 | const that = this;
13 | this.renderer = Renderer;
14 | this.showEnterVR = this.showEnterVR.bind(this);
15 |
16 | if ("xr" in navigator) {
17 | // autostart session for XRPackages and general futureproofing
18 | navigator.xr.addEventListener("sessiongranted", e => {
19 | navigator.xr
20 | .requestSession("immersive-vr", {
21 | optionalFeatures: ["local-floor", "bounded-floor"],
22 | })
23 | .then(session => {
24 | that.onSessionStarted(session);
25 | });
26 | });
27 |
28 | this.button = document.createElement("this.button");
29 | this.button.style.display = "none";
30 | this.stylizeElement(this.button);
31 | navigator.xr
32 | .isSessionSupported("immersive-vr")
33 | .then(function (supported) {
34 | supported ? that.showEnterVR() : that.showWebXRNotFound();
35 | });
36 | return this.button;
37 | } else {
38 | const message = document.createElement("a");
39 | if (window.isSecureContext === false) {
40 | message.href = document.location.href.replace(/^http:/, "https:");
41 | message.innerHTML = "WEBXR NEEDS HTTPS"; // TODO Improve message
42 | } else {
43 | message.href = "https://immersiveweb.dev/";
44 | message.innerHTML = "WEBXR NOT AVAILABLE";
45 | }
46 | message.style.left = "calc(50% - 90px)";
47 | message.style.width = "180px";
48 | message.style.textDecoration = "none";
49 | this.stylizeElement(message);
50 | return message;
51 | }
52 | }
53 |
54 | onSessionStarted(session) {
55 | session.addEventListener("end", this.onSessionEnded.bind(this));
56 | this.renderer.xr.setSession(session);
57 | session.addEventListener("inputsourceschange", this.onInputSourcesChange);
58 |
59 | if (State.debugMode) console.warn("xr session started");
60 | State.eventHandler.dispatchEvent("xrsessionstarted", session);
61 | this.button.textContent = "EXIT VR";
62 | State.isXRSession = true;
63 | State.currentSession = session;
64 | }
65 |
66 | onSessionEnded(/*event*/) {
67 | if (State.debugMode) console.warn("xr session ended");
68 | State.currentSession.removeEventListener("end", this.onSessionEnded);
69 | State.eventHandler.dispatchEvent("xrsessionended");
70 | this.button.textContent = "ENTER VR";
71 | State.isXRSession = false;
72 | State.currentSession = null;
73 | }
74 |
75 | onInputSourcesChange(event) {
76 | if (State.debugMode) console.log("input sources change");
77 | State.eventHandler.dispatchEvent("inputsourceschange", event);
78 | }
79 |
80 | showEnterVR(/*device*/) {
81 | this.button.style.display = "";
82 | this.button.style.cursor = "pointer";
83 | this.button.style.left = "calc(50% - 50px)";
84 | this.button.style.width = "100px";
85 | this.button.style.background = "black";
86 | this.button.textContent = "ENTER VR";
87 |
88 | this.button.onmouseenter = () => {
89 | this.button.style.opacity = "1.0";
90 | };
91 |
92 | this.button.onmouseleave = () => {
93 | this.button.style.opacity = "0.5";
94 | };
95 |
96 | this.button.onclick = () => {
97 | if (State.currentSession === null) {
98 | // WebXR's requestReferenceSpace only works if the corresponding feature
99 | // was requested at session creation time. For simplicity, just ask for
100 | // the interesting ones as optional features, but be aware that the
101 | // requestReferenceSpace call will fail if it turns out to be unavailable.
102 | // ('local' is always available for immersive sessions and doesn't need to
103 | // be requested separately.)
104 | const sessionInit = {
105 | optionalFeatures: ["local-floor", "bounded-floor"],
106 | };
107 |
108 | navigator.xr
109 | .requestSession("immersive-vr", sessionInit)
110 | .then(this.onSessionStarted.bind(this));
111 | } else {
112 | State.currentSession.end();
113 | }
114 | };
115 | }
116 |
117 | disableButton() {
118 | this.button.style.display = "";
119 | this.button.style.cursor = "auto";
120 | this.button.style.left = "calc(50% - 75px)";
121 | this.button.style.width = "150px";
122 | this.button.onmouseenter = null;
123 | this.button.onmouseleave = null;
124 | this.button.onclick = null;
125 | }
126 |
127 | showWebXRNotFound() {
128 | this.disableButton();
129 | this.button.textContent = "VR NOT SUPPORTED";
130 | }
131 |
132 | stylizeElement(element) {
133 | element.style.position = "absolute";
134 | element.style.bottom = "20px";
135 | element.style.padding = "12px 6px";
136 | element.style.border = "1px solid #fff";
137 | element.style.borderRadius = "4px";
138 | element.style.background = "rgba(0,0,0,0.1)";
139 | element.style.color = "#fff";
140 | element.style.font = "normal 13px sans-serif";
141 | element.style.textAlign = "center";
142 | element.style.opacity = "0.5";
143 | element.style.outline = "none";
144 | element.style.zIndex = "999";
145 | }
146 | }
147 |
148 | export default SessionHandler;
149 |
--------------------------------------------------------------------------------
/src/engine/util/xrpk/xrpk-build.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 | const { execSync } = require("child_process");
4 | const inquirer = require("inquirer");
5 |
6 | let revisedJSON;
7 |
8 | (async () => {
9 | console.log("----------------\ninitalizing XRPK\n----------------");
10 | execSync("cd dist && xrpk init", {
11 | stdio: "inherit",
12 | });
13 |
14 | fs.readFile("./dist/manifest.json", "utf8", (err, str) => {
15 | if (err) {
16 | console.error("manifest read failed:", err);
17 | return;
18 | }
19 | const jsonManifest = JSON.parse(str);
20 | console.log("----------------");
21 |
22 | inquirer
23 | .prompt([
24 | {
25 | type: "input",
26 | name: "name",
27 | message: "XR Package name: ",
28 | default: function () {
29 | return path.basename(process.cwd());
30 | },
31 | validate: function (value) {
32 | const pass = value.match(/^[a-zA-Z]+$/i);
33 | if (pass) {
34 | return true;
35 | }
36 |
37 | return "XR Package name must consist of letters only";
38 | },
39 | filter: function (val) {
40 | return val.toLowerCase();
41 | },
42 | },
43 | {
44 | type: "input",
45 | name: "description",
46 | message: "XR Package Description: ",
47 | default: function () {
48 | return "XR Package";
49 | },
50 | },
51 | {
52 | type: "list",
53 | name: "xr_type",
54 | message: "Package Type - hit ENTER if unsure",
55 | choices: [
56 | "webxr-site@0.0.1",
57 | new inquirer.Separator(),
58 | "gltf@0.0.1",
59 | new inquirer.Separator(),
60 | "vrm@0.0.1",
61 | new inquirer.Separator(),
62 | "vox@0.0.1",
63 | ],
64 | default: function () {
65 | return "webxr-site@0.0.1";
66 | },
67 | },
68 | {
69 | type: "input",
70 | name: "start_url",
71 | message: "URL entry point - hit ENTER if unsure",
72 | default: function () {
73 | return "index.html";
74 | },
75 | },
76 | ])
77 | .then(answers => {
78 | jsonManifest.name = answers.name;
79 | jsonManifest.description = answers.description;
80 | jsonManifest.xr_type = answers.xr_type;
81 | jsonManifest.start_url = answers.start_url;
82 | revisedJSON = JSON.stringify(jsonManifest);
83 | })
84 | .then(async () => {
85 | console.log("-------------------\nupdating manifest");
86 | try {
87 | await fs.promises.writeFile("./dist/manifest.json", revisedJSON);
88 | console.log("Manifest revised successfully");
89 | } catch (error) {
90 | console.error("Error writing file", error);
91 | fs.unlinkSync("./dist/manifest.json");
92 | console.log("Manifest deleted");
93 | }
94 | console.log("-------------------\nbuilding XR Package");
95 | execSync('cd dist && xrpk build . "' + jsonManifest.name + '.wbn"', {
96 | stdio: "inherit",
97 | });
98 | console.log(
99 | "-------------------\nXR Package " +
100 | jsonManifest.name +
101 | ".wbn created in ./dist!"
102 | );
103 | });
104 | });
105 | })();
106 |
--------------------------------------------------------------------------------
/src/engine/xrinput.js:
--------------------------------------------------------------------------------
1 | import { Group } from "three";
2 | import State from "./state";
3 | import Renderer from "./renderer";
4 |
5 | class XRInputClass {
6 | constructor() {
7 | /** @deprecated Use `leftControllerGrip` or `rightControllerGrip` instead */
8 | this.controllerGrips = [];
9 | this.inputSources = null;
10 |
11 | this._leftController = new Group();
12 | this._leftControllerGrip = new Group();
13 | this._rightController = new Group();
14 | this._rightControllerGrip = new Group();
15 | }
16 |
17 | get hasInputSources() {
18 | return !!XRInput.inputSources;
19 | }
20 |
21 | /** A Group containing controller data for the left controller. Empty until XR session has started and there are input sources.
22 | *
23 | * Use {@link hasInputSources} [named of the method](file-name #hasInputSources) to determine when controller data is provided.
24 | */
25 | get leftController() {
26 | return this._leftController;
27 | }
28 |
29 | get leftControllerGrip() {
30 | return this._leftControllerGrip;
31 | }
32 |
33 | /** A Group containing controller data for the right controller. Empty until the XR session has started and the input sources change. */
34 | get rightController() {
35 | return this._rightController;
36 | }
37 |
38 | get rightControllerGrip() {
39 | return this._rightControllerGrip;
40 | }
41 |
42 | // trigger start
43 | onSelectStart(e) {
44 | if (State.debugMode) {
45 | console.log("select started!");
46 | console.log(e);
47 | }
48 | State.eventHandler.dispatchEvent("selectstart", e);
49 | }
50 |
51 | // trigger end
52 | onSelectEnd(e) {
53 | if (State.debugMode) {
54 | console.log("select ended!");
55 | console.log(e);
56 | }
57 | State.eventHandler.dispatchEvent("selectend", e);
58 | }
59 |
60 | // trigger "event" (fully completed after release)
61 | onSelect(e) {
62 | if (State.debugMode) {
63 | console.log("select event!");
64 | console.log(e);
65 | }
66 | State.eventHandler.dispatchEvent("select", e);
67 | }
68 |
69 | // side button start
70 | onSqueezeStart(e) {
71 | if (State.debugMode) {
72 | console.log("squeeze pressed!");
73 | console.log(e);
74 | }
75 | State.eventHandler.dispatchEvent("squeezestart", e);
76 | }
77 |
78 | // side button end
79 | onSqueezeEnd(e) {
80 | if (State.debugMode) {
81 | console.log("squeeze released!");
82 | console.log(e);
83 | }
84 | State.eventHandler.dispatchEvent("squeezeend", e);
85 | }
86 |
87 | // side button "event" (fully completed after release)
88 | onSqueeze(e) {
89 | if (State.debugMode) {
90 | console.log("squeeze event!");
91 | console.log(e);
92 | }
93 | State.eventHandler.dispatchEvent("squeeze", e);
94 | }
95 |
96 | // controller connection
97 | onConnected(e) {
98 | if (State.debugMode) {
99 | console.log("Controller Connected");
100 | console.log(e.data);
101 | }
102 | }
103 |
104 | // controller disconnection
105 | onDisconnected(e) {
106 | if (State.debugMode) {
107 | console.log("Controller Disconnected");
108 | console.log(e.data);
109 | }
110 | }
111 |
112 | Update() {
113 | if (State.debugMode) this.debugOutput();
114 | }
115 |
116 | debugOutput() {
117 | if (this.inputSources == null) return;
118 |
119 | this.inputDebugString = "";
120 | this.inputSources.forEach(e => {
121 | if (e.gamepad == null) return;
122 | e.gamepad.buttons.forEach((button, i) => {
123 | if (button.pressed == true) {
124 | this.inputDebugString +=
125 | e.handedness + " controller button " + i + "\n";
126 | this.inputDebugString += "value: " + button.value + "\n";
127 | }
128 | });
129 |
130 | e.gamepad.axes.forEach((axis, axisIndex) => {
131 | if (axis != 0) {
132 | this.inputDebugString += e.handedness + " joystick:\n";
133 |
134 | // regardless of axis count, odds will be X, evens will be Y
135 | if (axisIndex % 2 == 0) {
136 | this.inputDebugString += "x: " + axis + "\n";
137 | } else {
138 | // Y (typically 1 or 3)
139 | this.inputDebugString += "y: " + axis + "\n";
140 | }
141 | }
142 | });
143 | });
144 | return this.inputDebugString == "" ? 0 : console.log(this.inputDebugString);
145 | }
146 | }
147 |
148 | //xrInput singleton
149 | const XRInput = new XRInputClass();
150 |
151 | // subscribe to input events on XR session start
152 | State.eventHandler.addEventListener("xrsessionstarted", e => {
153 | InputSourcesChanged(e.inputSources);
154 | e.addEventListener("selectend", XRInput.onSelectEnd.bind(XRInput));
155 | e.addEventListener("selectstart", XRInput.onSelectStart.bind(XRInput));
156 | e.addEventListener("select", XRInput.onSelect.bind(XRInput));
157 | e.addEventListener("squeezestart", XRInput.onSqueezeStart.bind(XRInput));
158 | e.addEventListener("squeezeend", XRInput.onSqueezeEnd.bind(XRInput));
159 | e.addEventListener("squeeze", XRInput.onSqueeze.bind(XRInput));
160 | e.addEventListener("connected", XRInput.onConnected.bind(XRInput));
161 | e.addEventListener("disconnected", XRInput.onDisconnected.bind(XRInput));
162 | });
163 |
164 | State.eventHandler.addEventListener("inputsourceschange", e =>
165 | InputSourcesChanged(e.session.inputSources)
166 | );
167 |
168 | State.eventHandler.addEventListener("xrsessionended", () => {
169 | XRInput.controllerGrips = [];
170 | XRInput.inputSources = null;
171 | XRInput._leftController = new Group();
172 | XRInput._leftControllerGrip = new Group();
173 | XRInput._rightController = new Group();
174 | XRInput._rightControllerGrip = new Group();
175 | });
176 |
177 | const InputSourcesChanged = inputSources => {
178 | XRInput.controllerGrips = [];
179 | XRInput.inputSources = inputSources;
180 |
181 | XRInput.inputSources.forEach((inputSource, controllerIndex) => {
182 | let controller = Renderer.xr.getController(controllerIndex);
183 | let controllerGrip = Renderer.xr.getControllerGrip(controllerIndex);
184 |
185 | if (inputSource.handedness === "left") {
186 | XRInput._leftController = controller;
187 | XRInput._leftControllerGrip = controllerGrip;
188 | } else if (inputSource.handedness === "right") {
189 | XRInput._rightController = controller;
190 | XRInput._rightControllerGrip = controllerGrip;
191 | }
192 | });
193 |
194 | // metachromium-specific hack to fix nonconformance bug
195 | const isUserAgentMetachromium = navigator.userAgent.indexOf("Mchr") !== -1;
196 | const inputNum = isUserAgentMetachromium ? 2 : XRInput.inputSources.length;
197 |
198 | for (let i = 0; i < inputNum; i++) {
199 | if (
200 | isUserAgentMetachromium ||
201 | XRInput.inputSources[i].gripSpace != undefined
202 | ) {
203 | if (State.debugMode) console.log("adding controller grip " + i);
204 | XRInput.controllerGrips[i] = Renderer.xr.getControllerGrip(i);
205 | }
206 | }
207 | };
208 |
209 | export default XRInput;
210 |
--------------------------------------------------------------------------------
/src/examples/artovr/assets/models/polyCrow/Bird_01(1).bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PlutoVR/sandcastle/060b6ddb0dba5225479a6860febfa11c96f3e340/src/examples/artovr/assets/models/polyCrow/Bird_01(1).bin
--------------------------------------------------------------------------------
/src/examples/artovr/assets/models/polyCrow/polyCrow_updated.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PlutoVR/sandcastle/060b6ddb0dba5225479a6860febfa11c96f3e340/src/examples/artovr/assets/models/polyCrow/polyCrow_updated.glb
--------------------------------------------------------------------------------
/src/examples/artovr/assets/textures/waternormals.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PlutoVR/sandcastle/060b6ddb0dba5225479a6860febfa11c96f3e340/src/examples/artovr/assets/textures/waternormals.jpg
--------------------------------------------------------------------------------
/src/examples/artovr/boid.js:
--------------------------------------------------------------------------------
1 | // boids implementation adapted from https://codepen.io/coaster/pen/QpqVjP
2 | import {
3 | Vector3,
4 | Mesh,
5 | MeshNormalMaterial,
6 | Group,
7 | SphereBufferGeometry,
8 | BoxBufferGeometry,
9 | MeshStandardMaterial,
10 | } from "three";
11 |
12 | const birdMat = new MeshStandardMaterial({
13 | color: 0xffffff,
14 | roughness: 0,
15 | metalness: 1.0,
16 | });
17 | let t = 0;
18 | export class Boid {
19 | constructor(scene, object = undefined) {
20 | // Initial movement vectors
21 | this.position = new Vector3(
22 | this.rrand(-80, -150),
23 | this.rrand(20, 40),
24 | this.rrand(-80, -150)
25 | );
26 | this.velocity = new Vector3(
27 | this.rrand(-1, 1),
28 | this.rrand(-1, 1),
29 | this.rrand(-1, 1)
30 | );
31 | this.acceleration = new Vector3(0, 0, 0);
32 | this.mass = 1;
33 | // Type determines boid geometry, home location, and starting position
34 | // this.obj = (type) ? new this.Box() : new this.Sphere();
35 | this.obj = object == undefined ? new this.Sphere() : new this.Bird(object);
36 | this.homeVec = new Vector3(0, 50, 0);
37 | this.t = 0;
38 | this.home = this.homeVec;
39 |
40 | scene.add(this.obj.mesh);
41 | }
42 |
43 | Sphere() {
44 | const spG = new Group();
45 | const sp = new Mesh(
46 | new SphereBufferGeometry(5, 20, 20),
47 | new MeshNormalMaterial({ wireframe: true })
48 | );
49 | spG.add(sp);
50 | this.mesh = spG;
51 | }
52 |
53 | Box() {
54 | const bxG = new Group();
55 | const bx = new Mesh(
56 | new BoxBufferGeometry(5, 5, 5),
57 | new MeshNormalMaterial({ wireframe: true })
58 | );
59 | bxG.add(bx);
60 | this.mesh = bxG;
61 | }
62 |
63 | Bird(object) {
64 | const spG = new Group();
65 | // const sp = object.clone();
66 | const sp = new Mesh(object.geometry, birdMat);
67 | spG.add(sp);
68 | this.mesh = spG;
69 | }
70 |
71 | rrand(min, max) {
72 | return Math.random() * (max - min) + min;
73 | }
74 |
75 | // Run an iteration of the flock
76 | step(flock) {
77 | this.accumulate(flock);
78 |
79 | //change target home
80 | t += 0.00001;
81 | this.homeVec.x = 500 * Math.cos(t) + 10;
82 | this.homeVec.z = 500 * Math.sin(t) + 10; // These to strings make it work
83 |
84 | this.update();
85 | this.obj.mesh.position.set(
86 | this.position.x,
87 | this.position.y,
88 | this.position.z
89 | );
90 | }
91 |
92 | // Apply Forces
93 | accumulate(flock) {
94 | let separation, alignment, cohesion, centering;
95 | separation = this.separate(flock).multiplyScalar(0.02 * this.mass);
96 | alignment = this.align(flock).multiplyScalar(0.05);
97 | cohesion = this.cohesion(flock).multiplyScalar(0.01);
98 | centering = this.steer(this.home).multiplyScalar(0.0001);
99 | centering.multiplyScalar(this.position.distanceTo(this.home) * this.mass); // stronger centering if farther away
100 | this.acceleration.add(separation);
101 | this.acceleration.add(alignment);
102 | this.acceleration.add(cohesion);
103 | this.acceleration.add(centering);
104 | this.acceleration.divideScalar(this.mass);
105 | }
106 |
107 | // Update Movement Vectors
108 |
109 | update() {
110 | this.velocity.add(this.acceleration);
111 | this.position.add(this.velocity);
112 | this.acceleration.set(0, 0, 0); // reset each iteration
113 |
114 | // X-Boids point in their direction of travel, O-Boids point in their direction of acceleration
115 | // const pointAt = (this.type) ? this.position.clone() : this.velocity.clone();
116 | this.obj.mesh.lookAt(this.position.clone());
117 | }
118 |
119 | // Separation Function (personal space)
120 | separate(flock) {
121 | const minRange = 200;
122 | let currBoid;
123 | const total = new Vector3(0, 0, 0);
124 | let count = 0;
125 | // Find total weight of separation
126 | for (let i = 0; i < flock.length; i++) {
127 | currBoid = flock[i];
128 | const dist = this.position.distanceTo(currBoid.position) * 3;
129 | // Apply weight if too close
130 | if (dist < minRange && dist > 0) {
131 | const force = this.position.clone();
132 | force.sub(currBoid.position);
133 | force.normalize();
134 | force.divideScalar(dist);
135 | total.add(force);
136 | count++;
137 | }
138 | }
139 | // Average out total weight
140 | if (count > 0) {
141 | total.divideScalar(count);
142 | total.normalize();
143 | }
144 | return total;
145 | }
146 |
147 | // Alignment Function (follow neighbours)
148 | align(flock) {
149 | const neighborRange = 60;
150 | let currBoid;
151 | const total = new Vector3(0, 0, 0);
152 | let count = 0;
153 | // Find total weight for alignment
154 | for (let i = 0; i < flock.length; i++) {
155 | currBoid = flock[i];
156 | const dist = this.position.distanceTo(currBoid.position);
157 | // Apply force if near enough
158 | if (dist < neighborRange && dist > 0) {
159 | total.add(currBoid.velocity);
160 | count++;
161 | }
162 | }
163 | // Average out total weight
164 | if (count > 0) {
165 | total.divideScalar(count);
166 | total.limit(1);
167 | }
168 | return total;
169 | }
170 |
171 | // Cohesion Function (follow whole flock)
172 | cohesion(flock) {
173 | const neighborRange = 60;
174 | let currBoid;
175 | const total = new Vector3(0, 0, 0);
176 | let count = 0;
177 | // Find total weight for cohesion
178 | for (let i = 0; i < flock.length; i++) {
179 | currBoid = flock[i];
180 | const dist = this.position.distanceTo(currBoid.position);
181 | // Apply weight if near enough
182 | if (dist < neighborRange && dist > 0) {
183 | total.add(currBoid.position);
184 | count++;
185 | }
186 | }
187 | // Average out total weight
188 | if (count > 0) {
189 | total.divideScalar(count);
190 | // Find direction to steer with
191 | return this.steer(total);
192 | } else {
193 | return total;
194 | }
195 | }
196 |
197 | steer(target) {
198 | const steer = new Vector3(0, 0, 0);
199 | const des = new Vector3().subVectors(target, this.position);
200 | const dist = des.length();
201 | if (dist > 0) {
202 | des.normalize();
203 | steer.subVectors(des, this.velocity);
204 | }
205 | return steer;
206 | }
207 | }
208 | // Limit max forces
209 | Vector3.prototype.limit = function (max) {
210 | if (this.length() > max) {
211 | this.normalize();
212 | this.multiplyScalar(max);
213 | }
214 | };
215 |
--------------------------------------------------------------------------------
/src/examples/artovr/scene.js:
--------------------------------------------------------------------------------
1 | // AR-IN-VR sample
2 | // important: test in metachromium or build as an XR Package, to enable transparent WebXR rendering
3 | // https://webaverse.com/
4 |
5 | // Instructions:
6 | // squeeze trigger to toggle between a day/night skybox and your external reality layer
7 | // (i.e SteamVR)
8 |
9 | import {
10 | Scene,
11 | Object3D,
12 | PlaneBufferGeometry,
13 | DirectionalLight,
14 | TextureLoader,
15 | RepeatWrapping,
16 | CubeCamera,
17 | WebGLCubeRenderTarget,
18 | RGBAFormat,
19 | LinearMipmapLinearFilter,
20 | } from "three";
21 | import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
22 | import Renderer from "../../engine/renderer";
23 | import State from "../../engine/state";
24 |
25 | import { Boid } from "./boid";
26 | import { Water } from "./water";
27 | import { Sky } from "./sky.js";
28 | const WaterNormalsTexture = require("./assets/textures/waternormals.jpg");
29 | const GLTFbird = require("./assets/models/polyCrow/polyCrow_updated.glb");
30 |
31 | const scene = new Scene();
32 |
33 | scene.init = () => {
34 | // Boids
35 | const loader = new GLTFLoader();
36 | // "Anonymous Bird" from https://poly.google.com/view/8Ph79kHbt9s
37 | let bird;
38 | loader.load(GLTFbird, function (gltf) {
39 | bird = gltf.scene.children[0];
40 | setupFlock(400, bird);
41 | });
42 |
43 | const boids = [];
44 | const setupFlock = (count, obj) => {
45 | let i = 0;
46 | while (i < count) {
47 | boids[i] = new Boid(scene, obj);
48 | i++;
49 | }
50 | };
51 |
52 | // Ocean
53 | const light = new DirectionalLight(0xffffff, 0.8);
54 | scene.add(light);
55 | const waterGeometry = new PlaneBufferGeometry(10000, 10000);
56 | const water = new Water(waterGeometry, {
57 | textureWidth: 512,
58 | textureHeight: 512,
59 | waterNormals: new TextureLoader().load(
60 | WaterNormalsTexture,
61 | function (texture) {
62 | texture.wrapS = texture.wrapT = RepeatWrapping;
63 | }
64 | ),
65 | alpha: 1.0,
66 | sunDirection: light.position.clone().normalize(),
67 | sunColor: 0xffffff,
68 | waterColor: 0x001e0f,
69 | distortionScale: 3.7,
70 | fog: scene.fog !== undefined,
71 | });
72 | water.rotation.x = -Math.PI / 2;
73 | water.position.y = -3;
74 | scene.add(water);
75 |
76 | // Atmosphere / day-night cycle. Custom XRCubeCamera component that handles XR rendering, soon merged to Three.js:
77 | const sky = new Sky();
78 | const uniforms = sky.material.uniforms;
79 | uniforms["turbidity"].value = 10;
80 | uniforms["rayleigh"].value = 2;
81 | uniforms["luminance"].value = 1;
82 | uniforms["mieCoefficient"].value = 0.005;
83 | uniforms["mieDirectionalG"].value = 0.8;
84 |
85 | var cubeRenderTarget = new WebGLCubeRenderTarget(512, {
86 | format: RGBAFormat,
87 | generateMipmaps: true,
88 | minFilter: LinearMipmapLinearFilter,
89 | });
90 |
91 | const cubeCamera = new CubeCamera(0.1, 1000, cubeRenderTarget);
92 | cubeCamera.renderTarget.texture.generateMipmaps = true;
93 | cubeCamera.renderTarget.texture.minFilter = LinearMipmapLinearFilter;
94 | scene.background = cubeCamera.renderTarget;
95 |
96 | const parameters = {
97 | distance: 400,
98 | inclination: 0.49,
99 | azimuth: 0.205,
100 | };
101 | let sunTheta = Math.PI * (parameters.inclination - 0.5);
102 | let sunPhi = 2 * Math.PI * (parameters.azimuth - 0.5);
103 |
104 | // tap render loop's rAF to update certain vars every frame;
105 | const data = new Object3D();
106 | data.Update = () => {
107 | // boids update
108 | for (let i = 0; i < boids.length; i++) {
109 | boids[i].step(boids);
110 | }
111 |
112 | // day/night cycle
113 |
114 | // long nights are boring, speed through them:
115 | parameters.inclination =
116 | parameters.inclination <= -0.55 ? 0.55 : parameters.inclination - 0.00025;
117 | sunTheta = Math.PI * (parameters.inclination - 0.5);
118 | sunPhi = 2 * Math.PI * (parameters.azimuth - 0.5);
119 | light.position.x = parameters.distance * Math.cos(sunPhi);
120 | light.position.y =
121 | parameters.distance * Math.sin(sunPhi) * Math.sin(sunTheta);
122 | light.position.z =
123 | parameters.distance * Math.sin(sunPhi) * Math.cos(sunTheta);
124 | sky.material.uniforms["sunPosition"].value = light.position.copy(
125 | light.position
126 | );
127 |
128 | // water
129 | water.material.uniforms["time"].value += 1.0 / 60.0;
130 | water.material.uniforms["sunDirection"].value
131 | .copy(light.position)
132 | .normalize();
133 | cubeCamera.update(Renderer, sky);
134 | };
135 | scene.add(data);
136 |
137 | //toggle VR: day/night cycle vs. AR: transparent background
138 | State.eventHandler.addEventListener("selectstart", e => {
139 | scene.background == null
140 | ? (scene.background = cubeCamera.renderTarget)
141 | : (scene.background = null);
142 | });
143 | };
144 | scene.init();
145 |
146 | export { scene };
147 |
--------------------------------------------------------------------------------
/src/examples/artovr/sky.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author zz85 / https://github.com/zz85
3 | *
4 | * Based on "A Practical Analytic Model for Daylight"
5 | * aka The Preetham Model, the de facto standard analytic skydome model
6 | * http://www.cs.utah.edu/~shirley/papers/sunsky/sunsky.pdf
7 | *
8 | * First implemented by Simon Wallner
9 | * http://www.simonwallner.at/projects/atmospheric-scattering
10 | *
11 | * Improved by Martin Upitis
12 | * http://blenderartists.org/forum/showthread.php?245954-preethams-sky-impementation-HDR
13 | *
14 | * Three.js integration by zz85 http://twitter.com/blurspline
15 | */
16 |
17 | import {
18 | BackSide,
19 | BoxBufferGeometry,
20 | Mesh,
21 | ShaderMaterial,
22 | UniformsUtils,
23 | Vector3,
24 | } from "three";
25 |
26 | var Sky = function () {
27 | var shader = Sky.SkyShader;
28 |
29 | var material = new ShaderMaterial({
30 | fragmentShader: shader.fragmentShader,
31 | vertexShader: shader.vertexShader,
32 | uniforms: UniformsUtils.clone(shader.uniforms),
33 | side: BackSide,
34 | depthWrite: false,
35 | });
36 |
37 | Mesh.call(this, new BoxBufferGeometry(1, 1, 1), material);
38 | };
39 |
40 | Sky.prototype = Object.create(Mesh.prototype);
41 |
42 | Sky.SkyShader = {
43 | uniforms: {
44 | luminance: { value: 1 },
45 | turbidity: { value: 2 },
46 | rayleigh: { value: 1 },
47 | mieCoefficient: { value: 0.005 },
48 | mieDirectionalG: { value: 0.8 },
49 | sunPosition: { value: new Vector3() },
50 | up: { value: new Vector3(0, 1, 0) },
51 | },
52 |
53 | vertexShader: [
54 | "uniform vec3 sunPosition;",
55 | "uniform float rayleigh;",
56 | "uniform float turbidity;",
57 | "uniform float mieCoefficient;",
58 | "uniform vec3 up;",
59 |
60 | "varying vec3 vWorldPosition;",
61 | "varying vec3 vSunDirection;",
62 | "varying float vSunfade;",
63 | "varying vec3 vBetaR;",
64 | "varying vec3 vBetaM;",
65 | "varying float vSunE;",
66 |
67 | // constants for atmospheric scattering
68 | "const float e = 2.71828182845904523536028747135266249775724709369995957;",
69 | "const float pi = 3.141592653589793238462643383279502884197169;",
70 |
71 | // wavelength of used primaries, according to preetham
72 | "const vec3 lambda = vec3( 680E-9, 550E-9, 450E-9 );",
73 | // this pre-calcuation replaces older TotalRayleigh(vec3 lambda) function:
74 | // (8.0 * pow(pi, 3.0) * pow(pow(n, 2.0) - 1.0, 2.0) * (6.0 + 3.0 * pn)) / (3.0 * N * pow(lambda, vec3(4.0)) * (6.0 - 7.0 * pn))
75 | "const vec3 totalRayleigh = vec3( 5.804542996261093E-6, 1.3562911419845635E-5, 3.0265902468824876E-5 );",
76 |
77 | // mie stuff
78 | // K coefficient for the primaries
79 | "const float v = 4.0;",
80 | "const vec3 K = vec3( 0.686, 0.678, 0.666 );",
81 | // MieConst = pi * pow( ( 2.0 * pi ) / lambda, vec3( v - 2.0 ) ) * K
82 | "const vec3 MieConst = vec3( 1.8399918514433978E14, 2.7798023919660528E14, 4.0790479543861094E14 );",
83 |
84 | // earth shadow hack
85 | // cutoffAngle = pi / 1.95;
86 | "const float cutoffAngle = 1.6110731556870734;",
87 | "const float steepness = 1.5;",
88 | "const float EE = 1000.0;",
89 |
90 | "float sunIntensity( float zenithAngleCos ) {",
91 | " zenithAngleCos = clamp( zenithAngleCos, -1.0, 1.0 );",
92 | " return EE * max( 0.0, 1.0 - pow( e, -( ( cutoffAngle - acos( zenithAngleCos ) ) / steepness ) ) );",
93 | "}",
94 |
95 | "vec3 totalMie( float T ) {",
96 | " float c = ( 0.2 * T ) * 10E-18;",
97 | " return 0.434 * c * MieConst;",
98 | "}",
99 |
100 | "void main() {",
101 |
102 | " vec4 worldPosition = modelMatrix * vec4( position, 1.0 );",
103 | " vWorldPosition = worldPosition.xyz;",
104 |
105 | " gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
106 | " gl_Position.z = gl_Position.w;", // set z to camera.far
107 |
108 | " vSunDirection = normalize( sunPosition );",
109 |
110 | " vSunE = sunIntensity( dot( vSunDirection, up ) );",
111 |
112 | " vSunfade = 1.0 - clamp( 1.0 - exp( ( sunPosition.y / 450000.0 ) ), 0.0, 1.0 );",
113 |
114 | " float rayleighCoefficient = rayleigh - ( 1.0 * ( 1.0 - vSunfade ) );",
115 |
116 | // extinction (absorbtion + out scattering)
117 | // rayleigh coefficients
118 | " vBetaR = totalRayleigh * rayleighCoefficient;",
119 |
120 | // mie coefficients
121 | " vBetaM = totalMie( turbidity ) * mieCoefficient;",
122 |
123 | "}",
124 | ].join("\n"),
125 |
126 | fragmentShader: [
127 | "varying vec3 vWorldPosition;",
128 | "varying vec3 vSunDirection;",
129 | "varying float vSunfade;",
130 | "varying vec3 vBetaR;",
131 | "varying vec3 vBetaM;",
132 | "varying float vSunE;",
133 |
134 | "uniform float luminance;",
135 | "uniform float mieDirectionalG;",
136 | "uniform vec3 up;",
137 |
138 | "const vec3 cameraPos = vec3( 0.0, 0.0, 0.0 );",
139 |
140 | // constants for atmospheric scattering
141 | "const float pi = 3.141592653589793238462643383279502884197169;",
142 |
143 | "const float n = 1.0003;", // refractive index of air
144 | "const float N = 2.545E25;", // number of molecules per unit volume for air at 288.15K and 1013mb (sea level -45 celsius)
145 |
146 | // optical length at zenith for molecules
147 | "const float rayleighZenithLength = 8.4E3;",
148 | "const float mieZenithLength = 1.25E3;",
149 | // 66 arc seconds -> degrees, and the cosine of that
150 | "const float sunAngularDiameterCos = 0.999956676946448443553574619906976478926848692873900859324;",
151 |
152 | // 3.0 / ( 16.0 * pi )
153 | "const float THREE_OVER_SIXTEENPI = 0.05968310365946075;",
154 | // 1.0 / ( 4.0 * pi )
155 | "const float ONE_OVER_FOURPI = 0.07957747154594767;",
156 |
157 | "float rayleighPhase( float cosTheta ) {",
158 | " return THREE_OVER_SIXTEENPI * ( 1.0 + pow( cosTheta, 2.0 ) );",
159 | "}",
160 |
161 | "float hgPhase( float cosTheta, float g ) {",
162 | " float g2 = pow( g, 2.0 );",
163 | " float inverse = 1.0 / pow( 1.0 - 2.0 * g * cosTheta + g2, 1.5 );",
164 | " return ONE_OVER_FOURPI * ( ( 1.0 - g2 ) * inverse );",
165 | "}",
166 |
167 | // Filmic ToneMapping http://filmicgames.com/archives/75
168 | "const float A = 0.15;",
169 | "const float B = 0.50;",
170 | "const float C = 0.10;",
171 | "const float D = 0.20;",
172 | "const float E = 0.02;",
173 | "const float F = 0.30;",
174 |
175 | "const float whiteScale = 1.0748724675633854;", // 1.0 / Uncharted2Tonemap(1000.0)
176 |
177 | "vec3 Uncharted2Tonemap( vec3 x ) {",
178 | " return ( ( x * ( A * x + C * B ) + D * E ) / ( x * ( A * x + B ) + D * F ) ) - E / F;",
179 | "}",
180 |
181 | "void main() {",
182 |
183 | " vec3 direction = normalize( vWorldPosition - cameraPos );",
184 |
185 | // optical length
186 | // cutoff angle at 90 to avoid singularity in next formula.
187 | " float zenithAngle = acos( max( 0.0, dot( up, direction ) ) );",
188 | " float inverse = 1.0 / ( cos( zenithAngle ) + 0.15 * pow( 93.885 - ( ( zenithAngle * 180.0 ) / pi ), -1.253 ) );",
189 | " float sR = rayleighZenithLength * inverse;",
190 | " float sM = mieZenithLength * inverse;",
191 |
192 | // combined extinction factor
193 | " vec3 Fex = exp( -( vBetaR * sR + vBetaM * sM ) );",
194 |
195 | // in scattering
196 | " float cosTheta = dot( direction, vSunDirection );",
197 |
198 | " float rPhase = rayleighPhase( cosTheta * 0.5 + 0.5 );",
199 | " vec3 betaRTheta = vBetaR * rPhase;",
200 |
201 | " float mPhase = hgPhase( cosTheta, mieDirectionalG );",
202 | " vec3 betaMTheta = vBetaM * mPhase;",
203 |
204 | " vec3 Lin = pow( vSunE * ( ( betaRTheta + betaMTheta ) / ( vBetaR + vBetaM ) ) * ( 1.0 - Fex ), vec3( 1.5 ) );",
205 | " Lin *= mix( vec3( 1.0 ), pow( vSunE * ( ( betaRTheta + betaMTheta ) / ( vBetaR + vBetaM ) ) * Fex, vec3( 1.0 / 2.0 ) ), clamp( pow( 1.0 - dot( up, vSunDirection ), 5.0 ), 0.0, 1.0 ) );",
206 |
207 | // nightsky
208 | " float theta = acos( direction.y ); // elevation --> y-axis, [-pi/2, pi/2]",
209 | " float phi = atan( direction.z, direction.x ); // azimuth --> x-axis [-pi/2, pi/2]",
210 | " vec2 uv = vec2( phi, theta ) / vec2( 2.0 * pi, pi ) + vec2( 0.5, 0.0 );",
211 | " vec3 L0 = vec3( 0.1 ) * Fex;",
212 |
213 | // composition + solar disc
214 | " float sundisk = smoothstep( sunAngularDiameterCos, sunAngularDiameterCos + 0.00002, cosTheta );",
215 | " L0 += ( vSunE * 19000.0 * Fex ) * sundisk;",
216 |
217 | " vec3 texColor = ( Lin + L0 ) * 0.04 + vec3( 0.0, 0.0003, 0.00075 );",
218 |
219 | " vec3 curr = Uncharted2Tonemap( ( log2( 2.0 / pow( luminance, 4.0 ) ) ) * texColor );",
220 | " vec3 color = curr * whiteScale;",
221 |
222 | " vec3 retColor = pow( color, vec3( 1.0 / ( 1.2 + ( 1.2 * vSunfade ) ) ) );",
223 |
224 | " gl_FragColor = vec4( retColor, 1.0 );",
225 |
226 | "}",
227 | ].join("\n"),
228 | };
229 |
230 | export { Sky };
231 |
--------------------------------------------------------------------------------
/src/examples/artovr/water.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author jbouny / https://github.com/jbouny
3 | *
4 | * Work based on :
5 | * @author Slayvin / http://slayvin.net : Flat mirror for three.js
6 | * @author Stemkoski / http://www.adelphi.edu/~stemkoski : An implementation of water shader based on the flat mirror
7 | * @author Jonas Wagner / http://29a.ch/ && http://29a.ch/slides/2012/webglwater/ : Water shader explanations in WebGL
8 | */
9 |
10 | import {
11 | Color,
12 | FrontSide,
13 | LinearFilter,
14 | MathUtils,
15 | Matrix4,
16 | Mesh,
17 | PerspectiveCamera,
18 | Plane,
19 | RGBFormat,
20 | ShaderMaterial,
21 | UniformsLib,
22 | UniformsUtils,
23 | Vector3,
24 | Vector4,
25 | WebGLRenderTarget,
26 | } from "three";
27 |
28 | var Water = function (geometry, options) {
29 | Mesh.call(this, geometry);
30 |
31 | var scope = this;
32 |
33 | options = options || {};
34 |
35 | var textureWidth =
36 | options.textureWidth !== undefined ? options.textureWidth : 512;
37 | var textureHeight =
38 | options.textureHeight !== undefined ? options.textureHeight : 512;
39 |
40 | var clipBias = options.clipBias !== undefined ? options.clipBias : 0.0;
41 | var alpha = options.alpha !== undefined ? options.alpha : 1.0;
42 | var time = options.time !== undefined ? options.time : 0.0;
43 | var normalSampler =
44 | options.waterNormals !== undefined ? options.waterNormals : null;
45 | var sunDirection =
46 | options.sunDirection !== undefined
47 | ? options.sunDirection
48 | : new Vector3(0.70707, 0.70707, 0.0);
49 | var sunColor = new Color(
50 | options.sunColor !== undefined ? options.sunColor : 0xffffff
51 | );
52 | var waterColor = new Color(
53 | options.waterColor !== undefined ? options.waterColor : 0x7f7f7f
54 | );
55 | var eye = options.eye !== undefined ? options.eye : new Vector3(0, 0, 0);
56 | var distortionScale =
57 | options.distortionScale !== undefined ? options.distortionScale : 20.0;
58 | var side = options.side !== undefined ? options.side : FrontSide;
59 | var fog = options.fog !== undefined ? options.fog : false;
60 |
61 | //
62 |
63 | var mirrorPlane = new Plane();
64 | var normal = new Vector3();
65 | var mirrorWorldPosition = new Vector3();
66 | var cameraWorldPosition = new Vector3();
67 | var rotationMatrix = new Matrix4();
68 | var lookAtPosition = new Vector3(0, 0, -1);
69 | var clipPlane = new Vector4();
70 |
71 | var view = new Vector3();
72 | var target = new Vector3();
73 | var q = new Vector4();
74 |
75 | var textureMatrix = new Matrix4();
76 |
77 | var mirrorCamera = new PerspectiveCamera();
78 |
79 | var parameters = {
80 | minFilter: LinearFilter,
81 | magFilter: LinearFilter,
82 | format: RGBFormat,
83 | stencilBuffer: false,
84 | };
85 |
86 | var renderTarget = new WebGLRenderTarget(
87 | textureWidth,
88 | textureHeight,
89 | parameters
90 | );
91 |
92 | if (
93 | !MathUtils.isPowerOfTwo(textureWidth) ||
94 | !MathUtils.isPowerOfTwo(textureHeight)
95 | ) {
96 | renderTarget.texture.generateMipmaps = false;
97 | }
98 |
99 | var mirrorShader = {
100 | uniforms: UniformsUtils.merge([
101 | UniformsLib["fog"],
102 | UniformsLib["lights"],
103 | {
104 | normalSampler: { value: null },
105 | mirrorSampler: { value: null },
106 | alpha: { value: 1.0 },
107 | time: { value: 0.0 },
108 | size: { value: 1.0 },
109 | distortionScale: { value: 20.0 },
110 | textureMatrix: { value: new Matrix4() },
111 | sunColor: { value: new Color(0x7f7f7f) },
112 | sunDirection: { value: new Vector3(0.70707, 0.70707, 0) },
113 | eye: { value: new Vector3() },
114 | waterColor: { value: new Color(0x555555) },
115 | },
116 | ]),
117 |
118 | vertexShader: [
119 | "uniform mat4 textureMatrix;",
120 | "uniform float time;",
121 |
122 | "varying vec4 mirrorCoord;",
123 | "varying vec4 worldPosition;",
124 |
125 | "#include ",
126 | "#include ",
127 | "#include ",
128 | "#include ",
129 |
130 | "void main() {",
131 | " mirrorCoord = modelMatrix * vec4( position, 1.0 );",
132 | " worldPosition = mirrorCoord.xyzw;",
133 | " mirrorCoord = textureMatrix * mirrorCoord;",
134 | " vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );",
135 | " gl_Position = projectionMatrix * mvPosition;",
136 |
137 | "#include ",
138 | "#include ",
139 | "#include ",
140 | "}",
141 | ].join("\n"),
142 |
143 | fragmentShader: [
144 | "uniform sampler2D mirrorSampler;",
145 | "uniform float alpha;",
146 | "uniform float time;",
147 | "uniform float size;",
148 | "uniform float distortionScale;",
149 | "uniform sampler2D normalSampler;",
150 | "uniform vec3 sunColor;",
151 | "uniform vec3 sunDirection;",
152 | "uniform vec3 eye;",
153 | "uniform vec3 waterColor;",
154 |
155 | "varying vec4 mirrorCoord;",
156 | "varying vec4 worldPosition;",
157 |
158 | "vec4 getNoise( vec2 uv ) {",
159 | " vec2 uv0 = ( uv / 103.0 ) + vec2(time / 17.0, time / 29.0);",
160 | " vec2 uv1 = uv / 107.0-vec2( time / -19.0, time / 31.0 );",
161 | " vec2 uv2 = uv / vec2( 8907.0, 9803.0 ) + vec2( time / 101.0, time / 97.0 );",
162 | " vec2 uv3 = uv / vec2( 1091.0, 1027.0 ) - vec2( time / 109.0, time / -113.0 );",
163 | " vec4 noise = texture2D( normalSampler, uv0 ) +",
164 | " texture2D( normalSampler, uv1 ) +",
165 | " texture2D( normalSampler, uv2 ) +",
166 | " texture2D( normalSampler, uv3 );",
167 | " return noise * 0.5 - 1.0;",
168 | "}",
169 |
170 | "void sunLight( const vec3 surfaceNormal, const vec3 eyeDirection, float shiny, float spec, float diffuse, inout vec3 diffuseColor, inout vec3 specularColor ) {",
171 | " vec3 reflection = normalize( reflect( -sunDirection, surfaceNormal ) );",
172 | " float direction = max( 0.0, dot( eyeDirection, reflection ) );",
173 | " specularColor += pow( direction, shiny ) * sunColor * spec;",
174 | " diffuseColor += max( dot( sunDirection, surfaceNormal ), 0.0 ) * sunColor * diffuse;",
175 | "}",
176 |
177 | "#include ",
178 | "#include ",
179 | "#include ",
180 | "#include ",
181 | "#include ",
182 | "#include ",
183 | "#include ",
184 | "#include ",
185 |
186 | "void main() {",
187 |
188 | "#include ",
189 | " vec4 noise = getNoise( worldPosition.xz * size );",
190 | " vec3 surfaceNormal = normalize( noise.xzy * vec3( 1.5, 1.0, 1.5 ) );",
191 |
192 | " vec3 diffuseLight = vec3(0.0);",
193 | " vec3 specularLight = vec3(0.0);",
194 |
195 | " vec3 worldToEye = eye-worldPosition.xyz;",
196 | " vec3 eyeDirection = normalize( worldToEye );",
197 | " sunLight( surfaceNormal, eyeDirection, 100.0, 2.0, 0.5, diffuseLight, specularLight );",
198 |
199 | " float distance = length(worldToEye);",
200 |
201 | " vec2 distortion = surfaceNormal.xz * ( 0.001 + 1.0 / distance ) * distortionScale;",
202 | " vec3 reflectionSample = vec3( texture2D( mirrorSampler, mirrorCoord.xy / mirrorCoord.w + distortion ) );",
203 |
204 | " float theta = max( dot( eyeDirection, surfaceNormal ), 0.0 );",
205 | " float rf0 = 0.3;",
206 | " float reflectance = rf0 + ( 1.0 - rf0 ) * pow( ( 1.0 - theta ), 5.0 );",
207 | " vec3 scatter = max( 0.0, dot( surfaceNormal, eyeDirection ) ) * waterColor;",
208 | " vec3 albedo = mix( ( sunColor * diffuseLight * 0.3 + scatter ) * getShadowMask(), ( vec3( 0.1 ) + reflectionSample * 0.9 + reflectionSample * specularLight ), reflectance);",
209 | " vec3 outgoingLight = albedo;",
210 | " gl_FragColor = vec4( outgoingLight, alpha );",
211 |
212 | "#include ",
213 | "#include ",
214 | "}",
215 | ].join("\n"),
216 | };
217 |
218 | var material = new ShaderMaterial({
219 | fragmentShader: mirrorShader.fragmentShader,
220 | vertexShader: mirrorShader.vertexShader,
221 | uniforms: UniformsUtils.clone(mirrorShader.uniforms),
222 | lights: true,
223 | side: side,
224 | fog: fog,
225 | });
226 |
227 | material.uniforms["mirrorSampler"].value = renderTarget.texture;
228 | material.uniforms["textureMatrix"].value = textureMatrix;
229 | material.uniforms["alpha"].value = alpha;
230 | material.uniforms["time"].value = time;
231 | material.uniforms["normalSampler"].value = normalSampler;
232 | material.uniforms["sunColor"].value = sunColor;
233 | material.uniforms["waterColor"].value = waterColor;
234 | material.uniforms["sunDirection"].value = sunDirection;
235 | material.uniforms["distortionScale"].value = distortionScale;
236 |
237 | material.uniforms["eye"].value = eye;
238 |
239 | scope.material = material;
240 |
241 | scope.onBeforeRender = function (renderer, scene, camera) {
242 | mirrorWorldPosition.setFromMatrixPosition(scope.matrixWorld);
243 | cameraWorldPosition.setFromMatrixPosition(camera.matrixWorld);
244 |
245 | rotationMatrix.extractRotation(scope.matrixWorld);
246 |
247 | normal.set(0, 0, 1);
248 | normal.applyMatrix4(rotationMatrix);
249 |
250 | view.subVectors(mirrorWorldPosition, cameraWorldPosition);
251 |
252 | // Avoid rendering when mirror is facing away
253 |
254 | if (view.dot(normal) > 0) return;
255 |
256 | view.reflect(normal).negate();
257 | view.add(mirrorWorldPosition);
258 |
259 | rotationMatrix.extractRotation(camera.matrixWorld);
260 |
261 | lookAtPosition.set(0, 0, -1);
262 | lookAtPosition.applyMatrix4(rotationMatrix);
263 | lookAtPosition.add(cameraWorldPosition);
264 |
265 | target.subVectors(mirrorWorldPosition, lookAtPosition);
266 | target.reflect(normal).negate();
267 | target.add(mirrorWorldPosition);
268 |
269 | mirrorCamera.position.copy(view);
270 | mirrorCamera.up.set(0, 1, 0);
271 | mirrorCamera.up.applyMatrix4(rotationMatrix);
272 | mirrorCamera.up.reflect(normal);
273 | mirrorCamera.lookAt(target);
274 |
275 | mirrorCamera.far = camera.far; // Used in WebGLBackground
276 |
277 | mirrorCamera.updateMatrixWorld();
278 | mirrorCamera.projectionMatrix.copy(camera.projectionMatrix);
279 |
280 | // Update the texture matrix
281 | textureMatrix.set(
282 | 0.5,
283 | 0.0,
284 | 0.0,
285 | 0.5,
286 | 0.0,
287 | 0.5,
288 | 0.0,
289 | 0.5,
290 | 0.0,
291 | 0.0,
292 | 0.5,
293 | 0.5,
294 | 0.0,
295 | 0.0,
296 | 0.0,
297 | 1.0
298 | );
299 | textureMatrix.multiply(mirrorCamera.projectionMatrix);
300 | textureMatrix.multiply(mirrorCamera.matrixWorldInverse);
301 |
302 | // Now update projection matrix with new clip plane, implementing code from: http://www.terathon.com/code/oblique.html
303 | // Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdf
304 | mirrorPlane.setFromNormalAndCoplanarPoint(normal, mirrorWorldPosition);
305 | mirrorPlane.applyMatrix4(mirrorCamera.matrixWorldInverse);
306 |
307 | clipPlane.set(
308 | mirrorPlane.normal.x,
309 | mirrorPlane.normal.y,
310 | mirrorPlane.normal.z,
311 | mirrorPlane.constant
312 | );
313 |
314 | var projectionMatrix = mirrorCamera.projectionMatrix;
315 |
316 | q.x =
317 | (Math.sign(clipPlane.x) + projectionMatrix.elements[8]) /
318 | projectionMatrix.elements[0];
319 | q.y =
320 | (Math.sign(clipPlane.y) + projectionMatrix.elements[9]) /
321 | projectionMatrix.elements[5];
322 | q.z = -1.0;
323 | q.w = (1.0 + projectionMatrix.elements[10]) / projectionMatrix.elements[14];
324 |
325 | // Calculate the scaled plane vector
326 | clipPlane.multiplyScalar(2.0 / clipPlane.dot(q));
327 |
328 | // Replacing the third row of the projection matrix
329 | projectionMatrix.elements[2] = clipPlane.x;
330 | projectionMatrix.elements[6] = clipPlane.y;
331 | projectionMatrix.elements[10] = clipPlane.z + 1.0 - clipBias;
332 | projectionMatrix.elements[14] = clipPlane.w;
333 |
334 | eye.setFromMatrixPosition(camera.matrixWorld);
335 |
336 | //
337 |
338 | var currentRenderTarget = renderer.getRenderTarget();
339 |
340 | var currentXrEnabled = renderer.xr.enabled;
341 | var currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;
342 |
343 | scope.visible = false;
344 |
345 | renderer.xr.enabled = false; // Avoid camera modification and recursion
346 | renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows
347 |
348 | renderer.setRenderTarget(renderTarget);
349 |
350 | renderer.state.buffers.depth.setMask(true); // make sure the depth buffer is writable so it can be properly cleared, see #18897
351 |
352 | if (renderer.autoClear === false) renderer.clear();
353 | renderer.render(scene, mirrorCamera);
354 |
355 | scope.visible = true;
356 |
357 | renderer.xr.enabled = currentXrEnabled;
358 | renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;
359 |
360 | renderer.setRenderTarget(currentRenderTarget);
361 |
362 | // Restore viewport
363 |
364 | var viewport = camera.viewport;
365 |
366 | if (viewport !== undefined) {
367 | renderer.state.viewport(viewport);
368 | }
369 | };
370 | };
371 |
372 | Water.prototype = Object.create(Mesh.prototype);
373 | Water.prototype.constructor = Water;
374 |
375 | export { Water };
376 |
--------------------------------------------------------------------------------
/src/examples/daynightcycle/assets/shaders/fs_clouds.glsl:
--------------------------------------------------------------------------------
1 | uniform sampler2D uTxtShape;
2 | uniform sampler2D uTxtCloudNoise;
3 | uniform float uTime;
4 |
5 | uniform float uFac1;
6 | uniform float uFac2;
7 | uniform float uTimeFactor1;
8 | uniform float uTimeFactor2;
9 | uniform float uDisplStrenght1;
10 | uniform float uDisplStrenght2;
11 |
12 | // fbm3d, snoise3, levels
13 |
14 | vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}
15 | float mod289(float x){return x - floor(x * (1.0 / 289.0)) * 289.0;}
16 | vec4 mod289(vec4 x){return x - floor(x * (1.0 / 289.0)) * 289.0;}
17 | vec3 mod289(vec3 x){return x - floor(x * (1.0 / 289.0)) * 289.0;}
18 | vec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }
19 | vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }
20 | vec4 gammaCorrect(vec4 color, float gamma){
21 | return pow(color, vec4(1.0 / gamma));
22 | }
23 |
24 | vec4 levelRange(vec4 color, float minInput, float maxInput){
25 | return min(max(color - vec4(minInput), vec4(0.0)) / (vec4(maxInput) - vec4(minInput)), vec4(1.0));
26 | }
27 |
28 | vec4 levels(vec4 color, float minInput, float gamma, float maxInput){
29 | return gammaCorrect(levelRange(color, minInput, maxInput), gamma);
30 | }
31 | float snoise3(vec3 v)
32 | {
33 | const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
34 | const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
35 |
36 | // First corner
37 | vec3 i = floor(v + dot(v, C.yyy) );
38 | vec3 x0 = v - i + dot(i, C.xxx) ;
39 |
40 | // Other corners
41 | vec3 g = step(x0.yzx, x0.xyz);
42 | vec3 l = 1.0 - g;
43 | vec3 i1 = min( g.xyz, l.zxy );
44 | vec3 i2 = max( g.xyz, l.zxy );
45 |
46 | // x0 = x0 - 0.0 + 0.0 * C.xxx;
47 | // x1 = x0 - i1 + 1.0 * C.xxx;
48 | // x2 = x0 - i2 + 2.0 * C.xxx;
49 | // x3 = x0 - 1.0 + 3.0 * C.xxx;
50 | vec3 x1 = x0 - i1 + C.xxx;
51 | vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
52 | vec3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y
53 |
54 | // Permutations
55 | i = mod289(i);
56 | vec4 p = permute( permute( permute(
57 | i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
58 | + i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
59 | + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
60 |
61 | // Gradients: 7x7 points over a square, mapped onto an octahedron.
62 | // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
63 | float n_ = 0.142857142857; // 1.0/7.0
64 | vec3 ns = n_ * D.wyz - D.xzx;
65 |
66 | vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7)
67 |
68 | vec4 x_ = floor(j * ns.z);
69 | vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N)
70 |
71 | vec4 x = x_ *ns.x + ns.yyyy;
72 | vec4 y = y_ *ns.x + ns.yyyy;
73 | vec4 h = 1.0 - abs(x) - abs(y);
74 |
75 | vec4 b0 = vec4( x.xy, y.xy );
76 | vec4 b1 = vec4( x.zw, y.zw );
77 |
78 | //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
79 | //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
80 | vec4 s0 = floor(b0)*2.0 + 1.0;
81 | vec4 s1 = floor(b1)*2.0 + 1.0;
82 | vec4 sh = -step(h, vec4(0.0));
83 |
84 | vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
85 | vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
86 |
87 | vec3 p0 = vec3(a0.xy,h.x);
88 | vec3 p1 = vec3(a0.zw,h.y);
89 | vec3 p2 = vec3(a1.xy,h.z);
90 | vec3 p3 = vec3(a1.zw,h.w);
91 |
92 | //Normalise gradients
93 | vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
94 | p0 *= norm.x;
95 | p1 *= norm.y;
96 | p2 *= norm.z;
97 | p3 *= norm.w;
98 |
99 | // Mix final noise value
100 | vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
101 | m = m * m;
102 | return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
103 | dot(p2,x2), dot(p3,x3) ) );
104 | }
105 | float snoise(vec2 v){
106 | const vec4 C = vec4(0.211324865405187, 0.366025403784439,
107 | -0.577350269189626, 0.024390243902439);
108 | vec2 i = floor(v + dot(v, C.yy) );
109 | vec2 x0 = v - i + dot(i, C.xx);
110 | vec2 i1;
111 | i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
112 | vec4 x12 = x0.xyxy + C.xxzz;
113 | x12.xy -= i1;
114 | i = mod(i, 289.0);
115 | vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
116 | + i.x + vec3(0.0, i1.x, 1.0 ));
117 | vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy),
118 | dot(x12.zw,x12.zw)), 0.0);
119 | m = m*m ;
120 | m = m*m ;
121 | vec3 x = 2.0 * fract(p * C.www) - 1.0;
122 | vec3 h = abs(x) - 0.5;
123 | vec3 ox = floor(x + 0.5);
124 | vec3 a0 = x - ox;
125 | m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
126 | vec3 g;
127 | g.x = a0.x * x0.x + h.x * x0.y;
128 | g.yz = a0.yz * x12.xz + h.yz * x12.yw;
129 | return 130.0 * dot(m, g);
130 | }
131 |
132 | float snoise(vec3 v)
133 | {
134 | const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
135 | const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
136 |
137 | // First corner
138 | vec3 i = floor(v + dot(v, C.yyy) );
139 | vec3 x0 = v - i + dot(i, C.xxx) ;
140 |
141 | // Other corners
142 | vec3 g = step(x0.yzx, x0.xyz);
143 | vec3 l = 1.0 - g;
144 | vec3 i1 = min( g.xyz, l.zxy );
145 | vec3 i2 = max( g.xyz, l.zxy );
146 |
147 | // x0 = x0 - 0.0 + 0.0 * C.xxx;
148 | // x1 = x0 - i1 + 1.0 * C.xxx;
149 | // x2 = x0 - i2 + 2.0 * C.xxx;
150 | // x3 = x0 - 1.0 + 3.0 * C.xxx;
151 | vec3 x1 = x0 - i1 + C.xxx;
152 | vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
153 | vec3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y
154 |
155 | // Permutations
156 | i = mod289(i);
157 | vec4 p = permute( permute( permute(
158 | i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
159 | + i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
160 | + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
161 |
162 | // Gradients: 7x7 points over a square, mapped onto an octahedron.
163 | // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
164 | float n_ = 0.142857142857; // 1.0/7.0
165 | vec3 ns = n_ * D.wyz - D.xzx;
166 |
167 | vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7)
168 |
169 | vec4 x_ = floor(j * ns.z);
170 | vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N)
171 |
172 | vec4 x = x_ *ns.x + ns.yyyy;
173 | vec4 y = y_ *ns.x + ns.yyyy;
174 | vec4 h = 1.0 - abs(x) - abs(y);
175 |
176 | vec4 b0 = vec4( x.xy, y.xy );
177 | vec4 b1 = vec4( x.zw, y.zw );
178 |
179 | //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
180 | //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
181 | vec4 s0 = floor(b0)*2.0 + 1.0;
182 | vec4 s1 = floor(b1)*2.0 + 1.0;
183 | vec4 sh = -step(h, vec4(0.0));
184 |
185 | vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
186 | vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
187 |
188 | vec3 p0 = vec3(a0.xy,h.x);
189 | vec3 p1 = vec3(a0.zw,h.y);
190 | vec3 p2 = vec3(a1.xy,h.z);
191 | vec3 p3 = vec3(a1.zw,h.w);
192 |
193 | //Normalise gradients
194 | vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
195 | p0 *= norm.x;
196 | p1 *= norm.y;
197 | p2 *= norm.z;
198 | p3 *= norm.w;
199 |
200 | // Mix final noise value
201 | vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
202 | m = m * m;
203 | return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
204 | dot(p2,x2), dot(p3,x3) ) );
205 | }
206 |
207 | float fbm3d(vec3 x, const in int it) {
208 | float v = 0.0;
209 | float a = 0.5;
210 | vec3 shift = vec3(100);
211 |
212 |
213 | for (int i = 0; i < 32; ++i) {
214 | if(i
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | varying vec2 v_uv;
12 |
13 | void main() {
14 | v_uv = uv;
15 |
16 | vec4 mvPosition = modelViewMatrix * vec4( 0.0, 0.0, 0.0, 1.0 );
17 | vec2 scale;
18 | scale.x = length( vec3( modelMatrix[ 0 ].x, modelMatrix[ 0 ].y, modelMatrix[ 0 ].z ) );
19 | scale.y = length( vec3( modelMatrix[ 1 ].x, modelMatrix[ 1 ].y, modelMatrix[ 1 ].z ) );
20 |
21 | vec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale;
22 | vec2 rotatedPosition;
23 | rotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y;
24 | rotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y;
25 | mvPosition.xy += rotatedPosition;
26 |
27 | gl_Position = projectionMatrix * mvPosition;
28 |
29 | #include
30 | #include
31 | #include
32 | }
--------------------------------------------------------------------------------
/src/examples/daynightcycle/assets/textures/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PlutoVR/sandcastle/060b6ddb0dba5225479a6860febfa11c96f3e340/src/examples/daynightcycle/assets/textures/1.jpg
--------------------------------------------------------------------------------
/src/examples/daynightcycle/assets/textures/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PlutoVR/sandcastle/060b6ddb0dba5225479a6860febfa11c96f3e340/src/examples/daynightcycle/assets/textures/2.jpg
--------------------------------------------------------------------------------
/src/examples/daynightcycle/assets/textures/cloud10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PlutoVR/sandcastle/060b6ddb0dba5225479a6860febfa11c96f3e340/src/examples/daynightcycle/assets/textures/cloud10.png
--------------------------------------------------------------------------------
/src/examples/daynightcycle/cloud.js:
--------------------------------------------------------------------------------
1 | // lovely clouds courtesy of @dghez from https://github.com/dghez/Three.js_Procedural-clouds/
2 | // tutorial https://tympanus.net/codrops/2020/01/28/how-to-create-procedural-clouds-using-three-js-sprites/
3 |
4 | import {
5 | PlaneBufferGeometry,
6 | Mesh,
7 | ShaderMaterial,
8 | TextureLoader,
9 | Object3D,
10 | } from "three";
11 | const tl = new TextureLoader();
12 | const vs_clouds = require("./assets/shaders/vs_clouds.glsl");
13 | const fs_clouds = require("./assets/shaders/fs_clouds.glsl");
14 | const cloud1 = require("./assets/textures/1.jpg");
15 | const cloud2 = require("./assets/textures/2.jpg");
16 |
17 | export class Cloud extends Object3D {
18 | constructor(params) {
19 | super(params);
20 | const t2 = tl.load(cloud2);
21 | const t1 = tl.load(cloud1);
22 | const cloudUniforms = {
23 | uTime: { value: 0 },
24 | uTxtShape: { value: t1 },
25 | uTxtCloudNoise: { value: t2 },
26 | uFac1: { value: 17.8 },
27 | uFac2: { value: 2.7 },
28 | uTimeFactor1: { value: 0.002 },
29 | uTimeFactor2: { value: 0.0015 },
30 | uDisplStrenght1: { value: 0.04 },
31 | uDisplStrenght2: { value: 0.08 },
32 | };
33 |
34 | const cloudMat = new ShaderMaterial({
35 | uniforms: cloudUniforms,
36 | vertexShader: vs_clouds,
37 | fragmentShader: fs_clouds,
38 | transparent: true,
39 | uTxtShape: t1,
40 | uTxtCloudNoise: t2,
41 | });
42 |
43 | const cloudGeo = new PlaneBufferGeometry(500.0, 500.0, 5, 5);
44 | const cloud = new Mesh(cloudGeo, cloudMat);
45 | cloud.Update = function () {
46 | cloud.material.uniforms.uTime.value += 1;
47 | };
48 | return cloud;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/examples/daynightcycle/scene.js:
--------------------------------------------------------------------------------
1 | // Reusable Day/night sycle, atmosphere, advanced noise-based cloud shader
2 |
3 | import {
4 | Scene,
5 | Object3D,
6 | DirectionalLight,
7 | WebGLCubeRenderTarget,
8 | CubeCamera,
9 | RGBAFormat,
10 | LinearMipmapLinearFilter,
11 | } from "three";
12 | import Renderer from "../../engine/renderer";
13 | import XRInput from "../../engine/xrinput";
14 |
15 | import { Cloud } from "./cloud";
16 | import { Sky } from "./sky.js";
17 |
18 | export const scene = new Scene();
19 |
20 | scene.init = () => {
21 | const cloud = new Cloud();
22 | cloud.position.set(40, 40, -255);
23 | scene.add(cloud);
24 |
25 | //sky, light
26 | const light = new DirectionalLight(0xffffff, 0.8);
27 | scene.add(light);
28 | const sky = new Sky();
29 | const uniforms = sky.material.uniforms;
30 | uniforms["turbidity"].value = 10;
31 | uniforms["rayleigh"].value = 2;
32 | uniforms["luminance"].value = 1;
33 | uniforms["mieCoefficient"].value = 0.005;
34 | uniforms["mieDirectionalG"].value = 0.8;
35 |
36 | const cubeRenderTarget = new WebGLCubeRenderTarget(512, {
37 | format: RGBAFormat,
38 | generateMipmaps: true,
39 | minFilter: LinearMipmapLinearFilter,
40 | });
41 |
42 | const cubeCamera = new CubeCamera(0.1, 1000, cubeRenderTarget);
43 | cubeCamera.renderTarget.texture.generateMipmaps = true;
44 | cubeCamera.renderTarget.texture.minFilter = LinearMipmapLinearFilter;
45 | scene.background = cubeCamera.renderTarget;
46 |
47 | XRInput.onSelect = e => {
48 | scene.background == cubeCamera.renderTarget
49 | ? (scene.background = null)
50 | : (scene.background = cubeCamera.renderTarget);
51 | };
52 |
53 | const parameters = {
54 | distance: 400,
55 | inclination: 0.49,
56 | azimuth: 0.205,
57 | };
58 | let sunTheta = Math.PI * (parameters.inclination - 0.5);
59 | let sunPhi = 2 * Math.PI * (parameters.azimuth - 0.5);
60 |
61 | const rAF = new Object3D();
62 | rAF.Update = () => {
63 | // day/night cycle
64 | // long nights are boring
65 | parameters.inclination =
66 | parameters.inclination <= -0.55 ? 0.55 : parameters.inclination - 0.00025;
67 | sunTheta = Math.PI * (parameters.inclination - 0.5);
68 | sunPhi = 2 * Math.PI * (parameters.azimuth - 0.5);
69 | light.position.x = parameters.distance * Math.cos(sunPhi);
70 | light.position.y =
71 | parameters.distance * Math.sin(sunPhi) * Math.sin(sunTheta);
72 | light.position.z =
73 | parameters.distance * Math.sin(sunPhi) * Math.cos(sunTheta);
74 | sky.material.uniforms["sunPosition"].value = light.position.copy(
75 | light.position
76 | );
77 | cubeCamera.update(Renderer, sky);
78 | };
79 | scene.add(rAF);
80 | };
81 | scene.init();
82 |
--------------------------------------------------------------------------------
/src/examples/daynightcycle/sky.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author zz85 / https://github.com/zz85
3 | *
4 | * Based on "A Practical Analytic Model for Daylight"
5 | * aka The Preetham Model, the de facto standard analytic skydome model
6 | * http://www.cs.utah.edu/~shirley/papers/sunsky/sunsky.pdf
7 | *
8 | * First implemented by Simon Wallner
9 | * http://www.simonwallner.at/projects/atmospheric-scattering
10 | *
11 | * Improved by Martin Upitis
12 | * http://blenderartists.org/forum/showthread.php?245954-preethams-sky-impementation-HDR
13 | *
14 | * Three.js integration by zz85 http://twitter.com/blurspline
15 | */
16 |
17 | import {
18 | BackSide,
19 | BoxBufferGeometry,
20 | Mesh,
21 | ShaderMaterial,
22 | UniformsUtils,
23 | Vector3,
24 | } from "three";
25 |
26 | var Sky = function () {
27 | var shader = Sky.SkyShader;
28 |
29 | var material = new ShaderMaterial({
30 | fragmentShader: shader.fragmentShader,
31 | vertexShader: shader.vertexShader,
32 | uniforms: UniformsUtils.clone(shader.uniforms),
33 | side: BackSide,
34 | depthWrite: false,
35 | });
36 |
37 | Mesh.call(this, new BoxBufferGeometry(1, 1, 1), material);
38 | };
39 |
40 | Sky.prototype = Object.create(Mesh.prototype);
41 |
42 | Sky.SkyShader = {
43 | uniforms: {
44 | luminance: { value: 1 },
45 | turbidity: { value: 2 },
46 | rayleigh: { value: 1 },
47 | mieCoefficient: { value: 0.005 },
48 | mieDirectionalG: { value: 0.8 },
49 | sunPosition: { value: new Vector3() },
50 | up: { value: new Vector3(0, 1, 0) },
51 | },
52 |
53 | vertexShader: [
54 | "uniform vec3 sunPosition;",
55 | "uniform float rayleigh;",
56 | "uniform float turbidity;",
57 | "uniform float mieCoefficient;",
58 | "uniform vec3 up;",
59 |
60 | "varying vec3 vWorldPosition;",
61 | "varying vec3 vSunDirection;",
62 | "varying float vSunfade;",
63 | "varying vec3 vBetaR;",
64 | "varying vec3 vBetaM;",
65 | "varying float vSunE;",
66 |
67 | // constants for atmospheric scattering
68 | "const float e = 2.71828182845904523536028747135266249775724709369995957;",
69 | "const float pi = 3.141592653589793238462643383279502884197169;",
70 |
71 | // wavelength of used primaries, according to preetham
72 | "const vec3 lambda = vec3( 680E-9, 550E-9, 450E-9 );",
73 | // this pre-calcuation replaces older TotalRayleigh(vec3 lambda) function:
74 | // (8.0 * pow(pi, 3.0) * pow(pow(n, 2.0) - 1.0, 2.0) * (6.0 + 3.0 * pn)) / (3.0 * N * pow(lambda, vec3(4.0)) * (6.0 - 7.0 * pn))
75 | "const vec3 totalRayleigh = vec3( 5.804542996261093E-6, 1.3562911419845635E-5, 3.0265902468824876E-5 );",
76 |
77 | // mie stuff
78 | // K coefficient for the primaries
79 | "const float v = 4.0;",
80 | "const vec3 K = vec3( 0.686, 0.678, 0.666 );",
81 | // MieConst = pi * pow( ( 2.0 * pi ) / lambda, vec3( v - 2.0 ) ) * K
82 | "const vec3 MieConst = vec3( 1.8399918514433978E14, 2.7798023919660528E14, 4.0790479543861094E14 );",
83 |
84 | // earth shadow hack
85 | // cutoffAngle = pi / 1.95;
86 | "const float cutoffAngle = 1.6110731556870734;",
87 | "const float steepness = 1.5;",
88 | "const float EE = 1000.0;",
89 |
90 | "float sunIntensity( float zenithAngleCos ) {",
91 | " zenithAngleCos = clamp( zenithAngleCos, -1.0, 1.0 );",
92 | " return EE * max( 0.0, 1.0 - pow( e, -( ( cutoffAngle - acos( zenithAngleCos ) ) / steepness ) ) );",
93 | "}",
94 |
95 | "vec3 totalMie( float T ) {",
96 | " float c = ( 0.2 * T ) * 10E-18;",
97 | " return 0.434 * c * MieConst;",
98 | "}",
99 |
100 | "void main() {",
101 |
102 | " vec4 worldPosition = modelMatrix * vec4( position, 1.0 );",
103 | " vWorldPosition = worldPosition.xyz;",
104 |
105 | " gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
106 | " gl_Position.z = gl_Position.w;", // set z to camera.far
107 |
108 | " vSunDirection = normalize( sunPosition );",
109 |
110 | " vSunE = sunIntensity( dot( vSunDirection, up ) );",
111 |
112 | " vSunfade = 1.0 - clamp( 1.0 - exp( ( sunPosition.y / 450000.0 ) ), 0.0, 1.0 );",
113 |
114 | " float rayleighCoefficient = rayleigh - ( 1.0 * ( 1.0 - vSunfade ) );",
115 |
116 | // extinction (absorbtion + out scattering)
117 | // rayleigh coefficients
118 | " vBetaR = totalRayleigh * rayleighCoefficient;",
119 |
120 | // mie coefficients
121 | " vBetaM = totalMie( turbidity ) * mieCoefficient;",
122 |
123 | "}",
124 | ].join("\n"),
125 |
126 | fragmentShader: [
127 | "varying vec3 vWorldPosition;",
128 | "varying vec3 vSunDirection;",
129 | "varying float vSunfade;",
130 | "varying vec3 vBetaR;",
131 | "varying vec3 vBetaM;",
132 | "varying float vSunE;",
133 |
134 | "uniform float luminance;",
135 | "uniform float mieDirectionalG;",
136 | "uniform vec3 up;",
137 |
138 | "const vec3 cameraPos = vec3( 0.0, 0.0, 0.0 );",
139 |
140 | // constants for atmospheric scattering
141 | "const float pi = 3.141592653589793238462643383279502884197169;",
142 |
143 | "const float n = 1.0003;", // refractive index of air
144 | "const float N = 2.545E25;", // number of molecules per unit volume for air at 288.15K and 1013mb (sea level -45 celsius)
145 |
146 | // optical length at zenith for molecules
147 | "const float rayleighZenithLength = 8.4E3;",
148 | "const float mieZenithLength = 1.25E3;",
149 | // 66 arc seconds -> degrees, and the cosine of that
150 | "const float sunAngularDiameterCos = 0.999956676946448443553574619906976478926848692873900859324;",
151 |
152 | // 3.0 / ( 16.0 * pi )
153 | "const float THREE_OVER_SIXTEENPI = 0.05968310365946075;",
154 | // 1.0 / ( 4.0 * pi )
155 | "const float ONE_OVER_FOURPI = 0.07957747154594767;",
156 |
157 | "float rayleighPhase( float cosTheta ) {",
158 | " return THREE_OVER_SIXTEENPI * ( 1.0 + pow( cosTheta, 2.0 ) );",
159 | "}",
160 |
161 | "float hgPhase( float cosTheta, float g ) {",
162 | " float g2 = pow( g, 2.0 );",
163 | " float inverse = 1.0 / pow( 1.0 - 2.0 * g * cosTheta + g2, 1.5 );",
164 | " return ONE_OVER_FOURPI * ( ( 1.0 - g2 ) * inverse );",
165 | "}",
166 |
167 | // Filmic ToneMapping http://filmicgames.com/archives/75
168 | "const float A = 0.15;",
169 | "const float B = 0.50;",
170 | "const float C = 0.10;",
171 | "const float D = 0.20;",
172 | "const float E = 0.02;",
173 | "const float F = 0.30;",
174 |
175 | "const float whiteScale = 1.0748724675633854;", // 1.0 / Uncharted2Tonemap(1000.0)
176 |
177 | "vec3 Uncharted2Tonemap( vec3 x ) {",
178 | " return ( ( x * ( A * x + C * B ) + D * E ) / ( x * ( A * x + B ) + D * F ) ) - E / F;",
179 | "}",
180 |
181 | "void main() {",
182 |
183 | " vec3 direction = normalize( vWorldPosition - cameraPos );",
184 |
185 | // optical length
186 | // cutoff angle at 90 to avoid singularity in next formula.
187 | " float zenithAngle = acos( max( 0.0, dot( up, direction ) ) );",
188 | " float inverse = 1.0 / ( cos( zenithAngle ) + 0.15 * pow( 93.885 - ( ( zenithAngle * 180.0 ) / pi ), -1.253 ) );",
189 | " float sR = rayleighZenithLength * inverse;",
190 | " float sM = mieZenithLength * inverse;",
191 |
192 | // combined extinction factor
193 | " vec3 Fex = exp( -( vBetaR * sR + vBetaM * sM ) );",
194 |
195 | // in scattering
196 | " float cosTheta = dot( direction, vSunDirection );",
197 |
198 | " float rPhase = rayleighPhase( cosTheta * 0.5 + 0.5 );",
199 | " vec3 betaRTheta = vBetaR * rPhase;",
200 |
201 | " float mPhase = hgPhase( cosTheta, mieDirectionalG );",
202 | " vec3 betaMTheta = vBetaM * mPhase;",
203 |
204 | " vec3 Lin = pow( vSunE * ( ( betaRTheta + betaMTheta ) / ( vBetaR + vBetaM ) ) * ( 1.0 - Fex ), vec3( 1.5 ) );",
205 | " Lin *= mix( vec3( 1.0 ), pow( vSunE * ( ( betaRTheta + betaMTheta ) / ( vBetaR + vBetaM ) ) * Fex, vec3( 1.0 / 2.0 ) ), clamp( pow( 1.0 - dot( up, vSunDirection ), 5.0 ), 0.0, 1.0 ) );",
206 |
207 | // nightsky
208 | " float theta = acos( direction.y ); // elevation --> y-axis, [-pi/2, pi/2]",
209 | " float phi = atan( direction.z, direction.x ); // azimuth --> x-axis [-pi/2, pi/2]",
210 | " vec2 uv = vec2( phi, theta ) / vec2( 2.0 * pi, pi ) + vec2( 0.5, 0.0 );",
211 | " vec3 L0 = vec3( 0.1 ) * Fex;",
212 |
213 | // composition + solar disc
214 | " float sundisk = smoothstep( sunAngularDiameterCos, sunAngularDiameterCos + 0.00002, cosTheta );",
215 | " L0 += ( vSunE * 19000.0 * Fex ) * sundisk;",
216 |
217 | " vec3 texColor = ( Lin + L0 ) * 0.04 + vec3( 0.0, 0.0003, 0.00075 );",
218 |
219 | " vec3 curr = Uncharted2Tonemap( ( log2( 2.0 / pow( luminance, 4.0 ) ) ) * texColor );",
220 | " vec3 color = curr * whiteScale;",
221 |
222 | " vec3 retColor = pow( color, vec3( 1.0 / ( 1.2 + ( 1.2 * vSunfade ) ) ) );",
223 |
224 | " gl_FragColor = vec4( retColor, 1.0 );",
225 |
226 | "}",
227 | ].join("\n"),
228 | };
229 |
230 | export { Sky };
231 |
--------------------------------------------------------------------------------
/src/examples/defaultscene.js:
--------------------------------------------------------------------------------
1 | // default scene loaded in src/engine/engine.js
2 | import {
3 | Scene,
4 | TorusBufferGeometry,
5 | DirectionalLight,
6 | Mesh,
7 | Vector3,
8 | MeshStandardMaterial,
9 | Color,
10 | } from "three";
11 |
12 | const scene = new Scene();
13 |
14 | const ringsData = [
15 | { axis: new Vector3(1, 0, 1), color: new Color(0xff0000), scale: 0.2 },
16 | { axis: new Vector3(1, -1, 0), color: new Color(0x00ff00), scale: 0.15 },
17 | { axis: new Vector3(0, 1, 0), color: new Color(0x0000ff), scale: 0.1 },
18 | ];
19 |
20 | ringsData.forEach((ringData, i) => {
21 | const ring = new Mesh(
22 | new TorusBufferGeometry(1, 0.065, 64, 64),
23 | new MeshStandardMaterial({
24 | metalness: 0.5,
25 | roughness: 0.5,
26 | color: ringData.color,
27 | })
28 | );
29 | ring.position.z -= 1;
30 | ring.scale.set(ringData.scale, ringData.scale, ringData.scale);
31 |
32 | ring.Update = () => {
33 | ring.rotateOnAxis(ringData.axis, 0.0033 * (i + 1));
34 | };
35 | scene.add(ring);
36 | });
37 | const light = new DirectionalLight(0xffffff, 3.5);
38 | light.position.set(0, 13, 3);
39 | scene.add(light);
40 |
41 | export { scene };
42 |
--------------------------------------------------------------------------------
/src/examples/physicsexample/brickcustomshader.js:
--------------------------------------------------------------------------------
1 | import { BoxGeometry, ShaderMaterial, Mesh } from "three";
2 |
3 | // glsl
4 | const vs = require("./shaders/vs_defaultVertex.glsl");
5 | const fs_neon = require("./shaders/fs_neonGrid.glsl");
6 | const fs_matrix = require("./shaders/fs_matrixLetters.glsl");
7 | const fs_puddles = require("./shaders/fs_puddles.glsl");
8 | const fs_pastelCheckers = require("./shaders/fs_pastelCheckers.glsl");
9 | const fs_bloomFireflies = require("./shaders/fs_bloomFireflies.glsl");
10 |
11 | const geometry = new BoxGeometry(0.5, 0.5, 1.5);
12 | const uniforms = { time: { value: 0.0 } };
13 | const shaderArr = [
14 | new ShaderMaterial({
15 | uniforms,
16 | vertexShader: vs,
17 | fragmentShader: fs_neon,
18 | }),
19 | new ShaderMaterial({
20 | uniforms,
21 | vertexShader: vs,
22 | fragmentShader: fs_matrix,
23 | }),
24 | new ShaderMaterial({
25 | uniforms,
26 | vertexShader: vs,
27 | fragmentShader: fs_puddles,
28 | }),
29 | new ShaderMaterial({
30 | uniforms,
31 | vertexShader: vs,
32 | fragmentShader: fs_pastelCheckers,
33 | }),
34 | new ShaderMaterial({
35 | uniforms,
36 | vertexShader: vs,
37 | fragmentShader: fs_bloomFireflies,
38 | }),
39 | ];
40 | const selectRandomShader = () => {
41 | return shaderArr[Math.floor(Math.random() * shaderArr.length)];
42 | };
43 |
44 | export default class JP {
45 | constructor(position, material) {
46 | const mesh = new Mesh(geometry, shaderArr[material]);
47 |
48 | //hook into render update method
49 |
50 | mesh.Update = () => {
51 | if (mesh.material.uniforms == undefined) return;
52 | mesh.material.uniforms.time.value = (6 * (Date.now() - startTime)) / 100;
53 | };
54 | const startTime = Date.now();
55 | if (position) mesh.position.copy(position);
56 | // if (rotation) mesh.rotation.copy(rotation);
57 | mesh.hasPhysics = true;
58 | return mesh;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/examples/physicsexample/scene.js:
--------------------------------------------------------------------------------
1 | // A simple physics-based game we all know
2 | // demonstrates basic RigidBody creation
3 | // (Note the Physics.addControllerRigidBody method, as well)
4 |
5 | import State from "../../engine/state";
6 | import {
7 | Scene,
8 | Vector3,
9 | Group,
10 | PlaneBufferGeometry,
11 | MeshBasicMaterial,
12 | Mesh,
13 | } from "three";
14 | import Brick from "./brickcustomshader";
15 | import Physics from "../../engine/physics/physics";
16 | import XRInput from "../../engine/xrinput";
17 |
18 | const scene = new Scene();
19 |
20 | Physics.enableDebugger(scene);
21 |
22 | // Ground Plane
23 | const groundShape = new PlaneBufferGeometry(10, 10);
24 | const planeMat = new MeshBasicMaterial({ color: 0x000000, wireframe: true });
25 | const plane = new Mesh(groundShape, planeMat);
26 | plane.quaternion.setFromAxisAngle(new Vector3(1, 0, 0), -Math.PI / 2);
27 | scene.add(plane);
28 | Physics.addRigidBody(
29 | plane,
30 | Physics.RigidBodyShape.Plane,
31 | Physics.Body.STATIC,
32 | 0
33 | );
34 |
35 | // once XR controllers are registered, add RigidBodies
36 | State.eventHandler.addEventListener("inputsourceschange", () => {
37 | XRInput.controllerGrips.forEach(controller => {
38 | Physics.addControllerRigidBody(controller);
39 | });
40 | });
41 |
42 | // BLOCK TOWER
43 | const tower = new Group();
44 |
45 | for (let y = 0; y < 13; y++) {
46 | const level = new Group();
47 | for (let x = 0; x < 3; x++) {
48 | const brickPos = new Vector3((-1 + x) / 2 + x * 0.01, y / 1.9, 0);
49 | const brick = new Brick(brickPos, y % 5);
50 | level.add(brick);
51 | }
52 | if (y % 2 == 0) {
53 | level.rotateOnAxis(new Vector3(0, 1, 0), 1.5708);
54 | }
55 | tower.add(level);
56 |
57 | // 0 pos is more likely to clash w/viewer
58 | tower.position.set(0, 0, -0.5);
59 | scene.updateMatrixWorld();
60 | tower.children.forEach((level, x) => {
61 | level.children.forEach((brick, y) => {
62 | if (!(brick instanceof Mesh)) return;
63 | scene.attach(brick);
64 |
65 | Physics.addRigidBody(brick, Physics.RigidBodyShape.Box);
66 | });
67 | });
68 | }
69 |
70 | export { scene };
71 |
--------------------------------------------------------------------------------
/src/examples/physicsexample/shaders/fs_bloomFireflies.glsl:
--------------------------------------------------------------------------------
1 | precision highp float;
2 | varying vec2 v_uv;
3 | uniform float time;
4 |
5 | void main(){
6 | vec2 r = v_uv-vec2(.5);
7 | float t = time*.005;
8 | gl_FragColor=vec4(.1);
9 | vec3 d=vec3((2.*v_uv.xy-r)/r.y,1.);
10 | for(float i=0.;i<200.;i++){
11 | vec3 p=(
12 | abs(
13 | fract(fract(99.*sin((vec3(1,5,9)+i*9.)))+t*.02)*2.-1.
14 | )*2.-1.
15 | )*8.;
16 | gl_FragColor+=vec4(
17 | mix(vec3(1),(cos((vec3(0,2,-2)/3.+i*.01)*6.283)*.5+.5),.8)
18 | *exp(-3.*length(cross(p,d))),
19 | 1
20 | );
21 | }
22 | }
--------------------------------------------------------------------------------
/src/examples/physicsexample/shaders/fs_matrixLetters.glsl:
--------------------------------------------------------------------------------
1 | // adapted from http://glslsandbox.com/e#63927.0
2 |
3 |
4 | /*
5 | * Original shader from: https://www.shadertoy.com/view/XljBW3
6 | */
7 |
8 | // glslsandbox uniforms
9 | uniform float time;
10 | varying vec2 v_uv;
11 | uniform vec2 resolution;
12 |
13 |
14 | // --------[ Original ShaderToy begins here ]---------- //
15 |
16 | uniform float ratio;
17 |
18 | #define PI2 6.28318530718
19 | #define PI 3.1416
20 |
21 | float vorocloud(vec2 p){
22 | float f = 0.0;
23 | vec2 pp = cos(vec2(p.x * 14.0, (16.0 * p.y + cos(floor(p.x * 30.0)) + time/26. * PI2)) );
24 | p = cos(p * 12.1 + pp * 10.0 + 0.5 * cos(pp.x * 10.0));
25 |
26 | vec2 pts[4];
27 |
28 | pts[0] = vec2(0.5, 0.6);
29 | pts[1] = vec2(-0.4, 0.4);
30 | pts[2] = vec2(0.2, -0.7);
31 | pts[3] = vec2(-0.3, -0.4);
32 |
33 | float d = 5.0;
34 |
35 | for(int i = 0; i < 4; i++){
36 | pts[i].x += 0.03 * cos(float(i)) + p.x;
37 | pts[i].y += 0.03 * sin(float(i)) + p.y;
38 | d = min(d, distance(pts[i], pp));
39 | }
40 |
41 | f = 2.0 * pow(1.0 - 0.3 * d, 13.0);
42 |
43 | f = min(f, 1.0);
44 |
45 | return f;
46 | }
47 |
48 | vec4 scene(vec2 UV){
49 | float x = UV.x;
50 | float y = UV.y;
51 |
52 | vec2 p = vec2(x, y) - vec2(0.5);
53 |
54 | vec4 col = vec4(0.0);
55 | col.g += 0.02;
56 |
57 | float v = vorocloud(p);
58 | v = 0.2 * floor(v * 5.0);
59 |
60 | col.r += 0.1 * v;
61 | col.g += 0.6 * v;
62 | col.b += 0.5 * pow(v, 5.0);
63 |
64 |
65 | v = vorocloud(p * 2.0);
66 | v = 0.2 * floor(v * 5.0);
67 |
68 | col.r += 0.1 * v;
69 | col.g += 0.2 * v;
70 | col.b += 0.01 * pow(v, 5.0);
71 |
72 | col.a = 1.0;
73 |
74 | return col;
75 | }
76 | void main(void)
77 | {
78 | gl_FragColor = vec4(scene(v_uv));
79 | }
80 |
--------------------------------------------------------------------------------
/src/examples/physicsexample/shaders/fs_neonGrid.glsl:
--------------------------------------------------------------------------------
1 | // adapted from http://glslsandbox.com/e#64274.1
2 | uniform float time;
3 | varying vec2 v_uv;
4 |
5 | void main( void ) {
6 |
7 | vec2 position = (v_uv.xy) -1.5;
8 | float y = .2*position.y * sin(90.0*position.y - time/10.);
9 | float x = 0.2*position.x * sin(90.0*position.x - time/10.);
10 | y = 1.0 / (300. * abs(x - y));
11 | float saule = 1./(35.*length(position - vec2(1, 0.8)));
12 | vec4 vsaule = vec4(saule, saule, saule*5., 1.0);
13 | vec4 vstari = vec4(position.y*0.5 - y, y, y*5.7, 2.7);
14 | gl_FragColor = mix(vsaule, vstari, 1.78);
15 | }
--------------------------------------------------------------------------------
/src/examples/physicsexample/shaders/fs_pastelCheckers.glsl:
--------------------------------------------------------------------------------
1 | // adapted from http://glslsandbox.com/e#63519.0
2 | uniform float time;
3 | varying vec2 v_uv;
4 |
5 | void main( void ) {
6 | float sc = 0.1;
7 | vec2 position = ( v_uv.xy ) + sin(time*.002) / 5.0;
8 | vec2 flooredPosition = floor(position / sc) * sc;
9 | gl_FragColor = vec4( flooredPosition.x, flooredPosition.y, 1, 1 );
10 | // gl_FragColor = vec4(.2,.6,.1,1.);
11 | }
--------------------------------------------------------------------------------
/src/examples/physicsexample/shaders/fs_puddles.glsl:
--------------------------------------------------------------------------------
1 | //adapted from http://glslsandbox.com/e#63997.0
2 |
3 | uniform float time;
4 | varying vec2 v_uv;
5 |
6 | // Readability
7 | #define globalTime time * 0.005
8 |
9 | mat2 Rotate2D(float angle) {
10 | return mat2(cos(angle), sin(angle), -sin(angle), cos(angle));
11 | }
12 |
13 | float rand(vec2 n) {
14 | return fract(sin(dot(n, vec2(12.9898, 4.1414))) * 43758.5453);
15 | }
16 |
17 | float noise(vec2 p){
18 |
19 | p = floor(48.0*p)/48.0;
20 |
21 | vec2 ip = floor(p);
22 | vec2 u = fract(p);
23 | u = u*u*(3.0-2.0*u);
24 |
25 | float res = mix(
26 | mix(rand(ip),rand(ip+vec2(1.0,0.0)),u.x),
27 | mix(rand(ip+vec2(0.0,1.0)),rand(ip+vec2(1.0,1.0)),u.x),u.y);
28 | return res*res;
29 | }
30 |
31 | float TextureCoordinate(vec2 position) {
32 |
33 | float zoomingFactor = 280.0;
34 |
35 | float levelOfDetail = 0.0;
36 |
37 | return noise(position);
38 | }
39 |
40 | float FBM(vec2 uv) {
41 | // Represents the number of levels sub-textures present in each texture (fractal power)
42 | #define fractalDimension 5
43 |
44 | float innerPower = 1.7;
45 |
46 | float noiseValue = 0.0; // Final noise to return at this pixel
47 | float brightness = 1.0; // Starting brightness of the first fractal power.
48 | // Subsequent powers will have lower brightness contributions.
49 | float dampeningFactor = 1.5; // How much of an impact subsequent fractal powers have on the result
50 |
51 | float offset = 0.5;
52 |
53 | float difference = 3.0;
54 |
55 | for (int i = 0; i < fractalDimension; ++i) {
56 |
57 | noiseValue += abs((TextureCoordinate(uv) - offset) * difference) / brightness;
58 |
59 | brightness *= dampeningFactor;
60 |
61 | uv *= innerPower;
62 | }
63 |
64 | return noiseValue;
65 | }
66 |
67 | float Turbulence(vec2 uv,float sp) {
68 | float activityLevel = 1.0; // How fast the tendrils of electricity move around
69 |
70 | vec2 noiseBasisDiag = vec2(FBM(uv - 2.0*globalTime * activityLevel), FBM(uv + globalTime * activityLevel));
71 |
72 | uv += noiseBasisDiag;
73 |
74 | float rotationSpeed = sp;
75 | return FBM(uv * Rotate2D(time/100. * rotationSpeed));
76 | }
77 |
78 | float Ring(vec2 uv) {
79 | float circleRadius = sqrt(length(uv));
80 |
81 | float range = 2.3;
82 | float functionSlope = 1.0;
83 | float offset = 0.5;
84 |
85 | return abs(mod(circleRadius, range) - range / 2.) * functionSlope + offset;
86 | }
87 |
88 | void main() {
89 |
90 | vec2 uv = v_uv.xy-0.5;
91 | // uv.x *= v_uv.x/v_uv.y;
92 |
93 | float distanceFromCenter = length(uv); // Distance away from the center the normalized uv coordinate is
94 | float radius = 0.9; // Maximum radius of the effect
95 | float alpha = 1.0; // Alpha starting value (full brightness)
96 | float alphaFalloffSpeed = 0.5; // How quickly alpha values fade to 0.0
97 |
98 | if(distanceFromCenter > radius) {
99 | alpha = max(0.0, 1.0 - (distanceFromCenter - radius) / alphaFalloffSpeed);
100 | }
101 |
102 |
103 | float zoom = 4.0;
104 | vec2 uvZoomed = uv * zoom*0.7;
105 |
106 | float fc1 = Turbulence(uvZoomed*5.0,0.001);
107 | float fc2 = Turbulence(uvZoomed*2.0,-0.001);
108 |
109 | //fractalColor *= Ring(uvZoomed*0.5);
110 |
111 | vec3 col = vec3(0.0,0.0,0.8) / fc1 + vec3(0.6,0.0,0.0) / fc2;
112 | col *= alpha;
113 |
114 | gl_FragColor = vec4(col,1.);
115 | }
116 |
117 |
--------------------------------------------------------------------------------
/src/examples/physicsexample/shaders/vs_defaultVertex.glsl:
--------------------------------------------------------------------------------
1 | uniform float time;
2 | varying vec2 v_uv;
3 | void main() {
4 | v_uv = uv;
5 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
6 | }
--------------------------------------------------------------------------------
/src/examples/pongxr/assets/audio/elecping.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PlutoVR/sandcastle/060b6ddb0dba5225479a6860febfa11c96f3e340/src/examples/pongxr/assets/audio/elecping.ogg
--------------------------------------------------------------------------------
/src/examples/pongxr/assets/audio/hitgoal.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PlutoVR/sandcastle/060b6ddb0dba5225479a6860febfa11c96f3e340/src/examples/pongxr/assets/audio/hitgoal.ogg
--------------------------------------------------------------------------------
/src/examples/pongxr/assets/shaders/fs_goal.glsl:
--------------------------------------------------------------------------------
1 | // from http://glslsandbox.com/e#65181.0
2 |
3 | precision highp float;
4 | varying vec2 v_uv;
5 | uniform float time;
6 |
7 | // void main(){
8 |
9 | // gl_FragColor=vec4(0.,1.,0.,sin(time/13.)+.5);
10 | // }
11 | #ifdef GL_ES
12 | precision mediump float;
13 | #endif
14 |
15 | #define NUM_OCTAVES 16
16 |
17 | // uniform float time;
18 | // uniform vec2 v_uv;
19 |
20 | mat3 rotX(float a) {
21 | float c = cos(a);
22 | float s = sin(a);
23 | return mat3(
24 | 1, 0, 0,
25 | 0, c, -s,
26 | 0, s, c
27 | );
28 | }
29 | mat3 rotY(float a) {
30 | float c = cos(a);
31 | float s = sin(a);
32 | return mat3(
33 | c, 0, -s,
34 | 0, 1, 0,
35 | s, 0, c
36 | );
37 | }
38 |
39 | float random(vec2 pos) {
40 | return fract(sin(dot(pos.xy, vec2(12.9898, 78.233))) * 43758.5453123);
41 | }
42 |
43 | float noise(vec2 pos) {
44 | vec2 i = floor(pos);
45 | vec2 f = fract(pos);
46 | float a = random(i + vec2(0.0, 0.0));
47 | float b = random(i + vec2(1.0, 0.0));
48 | float c = random(i + vec2(0.0, 1.0));
49 | float d = random(i + vec2(1.0, 1.0));
50 | vec2 u = f * f * (3.0 - 2.0 * f);
51 | return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
52 | }
53 |
54 | float fbm(vec2 pos) {
55 | float v = 0.0;
56 | float a = 0.5;
57 | vec2 shift = vec2(100.0);
58 | mat2 rot = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));
59 | for (int i=0; i radius) {
99 | alpha = max(0.0, 1.0 - (distanceFromCenter - radius) / alphaFalloffSpeed);
100 | }
101 |
102 |
103 | float zoom = 4.0;
104 | vec2 uvZoomed = uv * zoom*0.7;
105 |
106 | float fc1 = Turbulence(uvZoomed*5.0,0.001);
107 | float fc2 = Turbulence(uvZoomed*2.0,-0.001);
108 |
109 | //fractalColor *= Ring(uvZoomed*0.5);
110 |
111 | vec3 col = vec3(0.0,0.0,0.8) / fc1 + vec3(0.6,0.0,0.0) / fc2;
112 | col *= alpha;
113 |
114 | gl_FragColor = vec4(col,1.);
115 | }
116 |
117 |
--------------------------------------------------------------------------------
/src/examples/pongxr/assets/shaders/vs_defaultVertex.glsl:
--------------------------------------------------------------------------------
1 | uniform float time;
2 | varying vec2 v_uv;
3 | void main() {
4 | v_uv = uv;
5 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
6 | }
--------------------------------------------------------------------------------
/src/examples/pongxr/ball.js:
--------------------------------------------------------------------------------
1 | import {
2 | SphereBufferGeometry,
3 | ShaderMaterial,
4 | PositionalAudio,
5 | AudioLoader,
6 | Mesh,
7 | PointLight,
8 | } from "three";
9 |
10 | import { Camera } from "../../engine/engine";
11 | import Physics from "../../engine/physics/physics";
12 | import frictionlessMat from "./frictionlessmaterial";
13 |
14 | const hitAudioFile = require("./assets/audio/elecping.ogg");
15 |
16 | const vs = require("./assets/shaders/vs_defaultVertex.glsl");
17 | const fs_puddles = require("./assets/shaders/fs_puddles.glsl");
18 |
19 | class Ball extends Mesh {
20 | constructor(position, addRigidBody, params) {
21 | super(position, addRigidBody, params);
22 | const geometry = new SphereBufferGeometry(0.2, 13, 13);
23 | const material = new ShaderMaterial({
24 | uniforms: { time: { value: 0.0 } },
25 | vertexShader: vs,
26 | fragmentShader: fs_puddles,
27 | });
28 | this.geometry = geometry;
29 | this.material = material;
30 |
31 | this.position.copy(position);
32 | this.name = "ball";
33 | this.initPos = position;
34 |
35 | // physics
36 | if (addRigidBody == true) {
37 | // console.log("adding RB to ballRef");
38 | this.rb = Physics.addRigidBody(
39 | this,
40 | Physics.RigidBodyShape.Sphere,
41 | Physics.Body.DYNAMIC,
42 | 1
43 | );
44 |
45 | this.rb.material = frictionlessMat;
46 |
47 | // audio
48 | const ballRef = this;
49 | let hitAudio;
50 | hitAudio = new PositionalAudio(Camera.audioListener);
51 | const audioLoader = new AudioLoader();
52 | audioLoader.load(hitAudioFile, function (buffer) {
53 | hitAudio.setBuffer(buffer);
54 | hitAudio.setRefDistance(20);
55 | ballRef.rb.addEventListener("collide", function (e) {
56 | if (hitAudio.isPlaying) hitAudio.stop();
57 | hitAudio.play();
58 | });
59 | });
60 |
61 | if (hitAudio === undefined) console.error("no AudioListener found!");
62 | }
63 |
64 | // innerlight
65 | const bLight = new PointLight(0x6a0dad, 3);
66 | this.add(bLight);
67 |
68 | this.startTime = Date.now();
69 | }
70 |
71 | Update() {
72 | if (this.material.uniforms.time == undefined) return;
73 | this.material.uniforms.time.value =
74 | (6 * (Date.now() - this.startTime)) / 100;
75 | }
76 |
77 | reset() {
78 | Physics.resetRigidbody(this.rb);
79 | this.rb.position.copy(this.initPos);
80 | }
81 |
82 | kickoff() {
83 | this.rb.velocity.set(this.rnd(-2, 2), this.rnd(-2, 2), this.rnd(-2, 2));
84 | }
85 |
86 | rnd(min, max) {
87 | return Math.floor(Math.random() * (max - min + 1)) + min;
88 | }
89 | }
90 |
91 | export default Ball;
92 |
--------------------------------------------------------------------------------
/src/examples/pongxr/frictionlessMaterial.js:
--------------------------------------------------------------------------------
1 | import { Material, ContactMaterial } from "cannon";
2 | import Physics from "../../engine/physics/physics";
3 |
4 | // physics materials
5 | // Create a slippery material (friction coefficient = 0.0)
6 | const frictionlessMat = new Material("frictionlessMat");
7 |
8 | // The ContactMaterial defines what happens when two materials meet.
9 | // In this case we want friction coefficient = 0.0 when the slippery material touches ground.
10 | const frictionlessContactMaterial = new ContactMaterial(
11 | frictionlessMat,
12 | frictionlessMat,
13 | {
14 | friction: 0,
15 | restitution: 0.3,
16 | contactEquationStiffness: 1e8,
17 | contactEquationRelaxation: 3,
18 | }
19 | );
20 |
21 | // We must add the contact materials to the world
22 | Physics.cannonWorld.addContactMaterial(frictionlessContactMaterial);
23 |
24 | export default frictionlessMat;
25 |
--------------------------------------------------------------------------------
/src/examples/pongxr/level.js:
--------------------------------------------------------------------------------
1 | import State from "../../engine/state";
2 | import Physics from "../../engine/physics/physics";
3 | import frictionlessMat from "./frictionlessmaterial";
4 | import {
5 | Object3D,
6 | PointLight,
7 | BoxBufferGeometry,
8 | MeshStandardMaterial,
9 | DoubleSide,
10 | MathUtils,
11 | Mesh,
12 | Vector3,
13 | Quaternion as THREEQuaternion,
14 | PositionalAudio,
15 | AudioLoader,
16 | ShaderMaterial,
17 | } from "three";
18 | import { Quaternion } from "cannon";
19 | import { Camera } from "../../engine/engine";
20 |
21 | const vs = require("./assets/shaders/vs_defaultVertex.glsl");
22 | const fs_goal = require("./assets/shaders/fs_goal.glsl");
23 |
24 | const crashAudioFile = require("./assets/audio/hitgoal.ogg");
25 |
26 | class Level extends Object3D {
27 | constructor(posRot, params) {
28 | super(params);
29 | const light = new PointLight(0xffffff, 4);
30 | this.add(light);
31 |
32 | const geometry1 = new BoxBufferGeometry(4, 2, 0.02);
33 | const material = new MeshStandardMaterial({
34 | color: 0x222222,
35 | wireframe: false,
36 | side: DoubleSide,
37 | });
38 | const sideLength = new Mesh(geometry1, material);
39 |
40 | const side1 = sideLength.clone();
41 | side1.name = "side1";
42 | side1.position.set(1, 0, 0);
43 | side1.rotateOnAxis(new Vector3(0, 1, 0), MathUtils.degToRad(90));
44 | this.add(side1);
45 |
46 | const side2 = sideLength.clone();
47 | side2.name = "side2";
48 | side2.position.set(-1, 0, 0);
49 | side2.rotateOnAxis(new Vector3(0, 1, 0), MathUtils.degToRad(90));
50 | this.add(side2);
51 |
52 | const top = sideLength.clone();
53 | top.name = "top";
54 | top.rotateOnAxis(new Vector3(1, 0, 0), MathUtils.degToRad(90));
55 | top.rotateOnAxis(new Vector3(0, 0, 1), MathUtils.degToRad(90));
56 | top.position.y -= 1;
57 | this.add(top);
58 |
59 | const bottom = sideLength.clone();
60 | bottom.name = "bottom";
61 | bottom.rotateOnAxis(new Vector3(1, 0, 0), MathUtils.degToRad(90));
62 | bottom.rotateOnAxis(new Vector3(0, 0, 1), MathUtils.degToRad(90));
63 | bottom.position.y += 1;
64 | this.add(bottom);
65 |
66 | const uniforms = { time: { value: 0.0 } };
67 | const goalGeo = new BoxBufferGeometry(4, 4, 0.002, 2, 2);
68 | const goalMat = new ShaderMaterial({
69 | uniforms,
70 | vertexShader: vs,
71 | fragmentShader: fs_goal,
72 | transparent: true,
73 | });
74 |
75 | const goal = new Mesh(goalGeo, goalMat);
76 | goal.name = "goal";
77 | goal.rotateOnAxis(new Vector3(0, 0, 1), MathUtils.degToRad(90));
78 | goal.position.z = 3;
79 |
80 | const startTime = Date.now();
81 | goal.Update = () => {
82 | if (goalMat.uniforms == undefined) return;
83 | goalMat.uniforms.time.value = (6 * (Date.now() - startTime)) / 1000;
84 | };
85 | this.add(goal);
86 |
87 | const goal2 = goal.clone();
88 | goal2.position.z = -3;
89 | this.add(goal2);
90 |
91 | this.name = "levelInstance";
92 | this.position.copy(posRot.position);
93 | this.rotation.copy(posRot.rotation);
94 |
95 | this.updateMatrixWorld();
96 |
97 | // transfer sceneCube offset directly to children
98 | // necessary for RigidBody alignment
99 | // since mesh parent offset isn't a factor
100 | this.children.forEach(e => {
101 | var wPos = new Vector3();
102 | var wQua = new THREEQuaternion();
103 | var wSca = new Vector3();
104 | e.matrixWorld.decompose(wPos, wQua, wSca);
105 | e.position.copy(wPos);
106 | e.quaternion.copy(wQua);
107 | e.scale.copy(wSca);
108 | });
109 | this.position.copy(new Vector3());
110 | this.quaternion.copy(new Quaternion());
111 |
112 | //audio
113 | const levelRef = this;
114 | this.crashAudio = new PositionalAudio(Camera.audioListener);
115 | this.audioLoader = new AudioLoader();
116 | this.audioLoader.load(crashAudioFile, function (buffer) {
117 | levelRef.crashAudio.setBuffer(buffer);
118 | levelRef.crashAudio.setRefDistance(20);
119 | });
120 |
121 | this.children.forEach(e => {
122 | e.rb = Physics.addRigidBody(
123 | e,
124 | Physics.RigidBodyShape.Box,
125 | Physics.Body.STATIC,
126 | 0
127 | );
128 | if (e.rb != undefined) {
129 | // not light, etc
130 | e.rb.material = frictionlessMat;
131 | }
132 |
133 | // game logic
134 | if (e.name == "goal") {
135 | e.rb.addEventListener("collide", this.endGame.bind(this));
136 | }
137 | });
138 | }
139 |
140 | endGame() {
141 | State.eventHandler.dispatchEvent("gameover");
142 | console.log(this);
143 | if (this.crashAudio == undefined) return;
144 | if (this.crashAudio.isPlaying) this.crashAudio.stop();
145 | this.crashAudio.play();
146 | }
147 | }
148 |
149 | export default Level;
150 |
--------------------------------------------------------------------------------
/src/examples/pongxr/paddle.js:
--------------------------------------------------------------------------------
1 | import { BoxBufferGeometry, ShaderMaterial, Mesh } from "three";
2 | import Physics from "../../engine/physics/physics";
3 | import { Vec3 } from "cannon";
4 | const vs = require("./assets/shaders/vs_defaultVertex.glsl");
5 | const fs_puddles = require("./assets/shaders/fs_puddles.glsl");
6 |
7 | class Paddle extends Mesh {
8 | constructor(params) {
9 | super(params);
10 | const paddleGeo = new BoxBufferGeometry(0.25, 0.001, 0.25);
11 | const paddleMat = new ShaderMaterial({
12 | uniforms: { time: { value: 0.0 } },
13 | vertexShader: vs,
14 | fragmentShader: fs_puddles,
15 | });
16 |
17 | this.geometry = paddleGeo;
18 | this.material = paddleMat;
19 | this.name = "paddle";
20 | this.rb = Physics.addRigidBody(
21 | this,
22 | Physics.RigidBodyShape.Box,
23 | Physics.Body.KINEMATIC,
24 | 0
25 | );
26 |
27 | this.curPos = new Vec3();
28 |
29 | this.startTime = Date.now();
30 | }
31 |
32 | Update() {
33 | this.rb.position.copy(this.position);
34 | this.rb.quaternion.copy(this.quaternion);
35 |
36 | // shader update
37 | if (this.material.uniforms.time == undefined) return;
38 | this.material.uniforms.time.value =
39 | (6 * (Date.now() - this.startTime)) / 500;
40 | }
41 | }
42 |
43 | export default Paddle;
44 |
--------------------------------------------------------------------------------
/src/examples/pongxr/placementcube.js:
--------------------------------------------------------------------------------
1 | import {
2 | MeshBasicMaterial,
3 | BoxBufferGeometry,
4 | Color,
5 | Clock,
6 | Vector3,
7 | Mesh,
8 | Object3D,
9 | } from "three";
10 | import { Camera } from "../../engine/engine";
11 | import State from "../../engine/state";
12 | import XRInput from "../../engine/xrinput";
13 |
14 | class PlacementCube extends Mesh {
15 | constructor(params) {
16 | super(params);
17 |
18 | this.c = new Clock();
19 |
20 | // fix for world cam dir querying
21 | this.forwardOffset = new Vector3();
22 | this.CamForward = new Vector3();
23 | let empty = new Object3D();
24 | Camera.add(empty);
25 |
26 | const pCubeGeo = new BoxBufferGeometry(2, 2, 4, 8, 8, 16);
27 | const pCubeMat = new MeshBasicMaterial({
28 | color: new Color("rgb(0, 255, 0)"),
29 | wireframe: true,
30 | });
31 |
32 | this.geometry = pCubeGeo;
33 | this.material = pCubeMat;
34 | }
35 | Update() {
36 | this.material.color.g = Math.cos(this.c.getElapsedTime() * 5) / 2 + 0.5;
37 | this.position.add(this.forwardOffset);
38 | if (State.isPrimary && XRInput.inputSources != null) {
39 | XRInput.inputSources.forEach((e, i) => {
40 | e.gamepad.axes.forEach((axis, axisIndex) => {
41 | if (axis != 0) {
42 | if (axisIndex % 2 == 0) {
43 | // X
44 | if (Math.abs(axis) > 0.5)
45 | this.rotation.y += axis > 0 ? -0.01 : 0.01;
46 | } // Y
47 | else {
48 | XRInput.controllerGrips[i].getWorldDirection(this.CamForward);
49 | this.forwardOffset = this.CamForward.multiplyScalar(
50 | axis > 0 ? 0.02 : -0.02
51 | );
52 | this.position.add(this.forwardOffset);
53 | this.forwardOffset.x = this.forwardOffset.y = this.forwardOffset.z = 0;
54 | }
55 | this.position.y = XRInput.controllerGrips[0].position.y;
56 | }
57 | });
58 | });
59 | }
60 | }
61 | }
62 |
63 | export default PlacementCube;
64 |
--------------------------------------------------------------------------------
/src/examples/pongxr/scene.js:
--------------------------------------------------------------------------------
1 | // A multiuser pong-clone
2 | // note the decoupling of XR Input data and networking - we use the inputs locally to control
3 | // the position and rotation of networked objects.
4 | // demonstrates both a Shared Object (plaecement cube) and a Local Object (paddle)
5 | // for more details, see: https://github.com/takahirox/ThreeNetwork
6 |
7 | import { Scene } from "three";
8 | import XRInput from "../../engine/xrinput";
9 | import PeerConnection from "../../engine/networking/peerconnection";
10 | import State from "../../engine/state";
11 | import Physics from "../../engine/physics/physics";
12 |
13 | import HostBot from "../../engine/util/hostbot";
14 | import Level from "./level";
15 | import Paddle from "./paddle";
16 | import Ball from "./ball";
17 | import PlacementCube from "./placementcube";
18 |
19 | const PLACEMENTCUBEID = 3;
20 | const scene = new Scene();
21 | const networking = new PeerConnection(scene);
22 | const hostBot = new HostBot(networking);
23 | let ball, placementCube;
24 | let paddles = [];
25 |
26 | // custom States & events
27 | State.eventHandler.registerEvent("gameover");
28 | const GameState = { placement: 1, play: 2 };
29 |
30 | Physics.enableDebugger(scene);
31 |
32 | const createPongLevel = placementCube => {
33 | const targetPosition = placementCube.position;
34 | const targetRotation = placementCube.rotation;
35 |
36 | State.GameState = GameState.play;
37 | const curPosRot = { position: targetPosition, rotation: targetRotation };
38 | const level = new Level(curPosRot);
39 | scene.add(level);
40 | networking.remoteSync.addLocalObject(
41 | level,
42 | { type: "level", posRot: curPosRot },
43 | true
44 | );
45 |
46 | // dumb hack for the other side
47 | placementCube.scale.set(0, 0, 0);
48 |
49 | // local
50 | scene.remove(placementCube);
51 |
52 | // remote placement cube removal, not currently working on metachromium
53 | networking.remoteSync.removeSharedObject(PLACEMENTCUBEID);
54 |
55 | // BALL
56 | if (State.isPrimary) {
57 | ball = new Ball(targetPosition, true);
58 | ball.initPos = targetPosition;
59 | scene.add(ball);
60 | networking.remoteSync.addLocalObject(
61 | ball,
62 | { type: "ball", position: targetPosition },
63 | true
64 | );
65 | }
66 | };
67 |
68 | const initPlacement = () => {
69 | State.GameState = GameState.placement;
70 | placementCube = new PlacementCube();
71 | scene.add(placementCube);
72 | networking.remoteSync.addSharedObject(placementCube, PLACEMENTCUBEID, true);
73 | };
74 |
75 | /// GAME STATE
76 |
77 | State.eventHandler.addEventListener("gameover", e => {
78 | if (ball != undefined) {
79 | ball.reset();
80 | } else {
81 | console.error("can't reset; no ball found!");
82 | }
83 | });
84 |
85 | /// INPUT
86 |
87 | let doubleClick = false;
88 |
89 | State.eventHandler.addEventListener("select", e => {
90 | switch (State.GameState) {
91 | case GameState.placement:
92 | if (State.isPrimary) {
93 | createPongLevel(placementCube);
94 | }
95 | break;
96 |
97 | case GameState.play:
98 | default:
99 | if (!doubleClick) {
100 | doubleClick = true;
101 | setTimeout(function () {
102 | if (ball == undefined) {
103 | console.error("can't kickoff; no ball found!");
104 | return;
105 | }
106 | ball.kickoff();
107 | doubleClick = false;
108 | }, 200);
109 | } else {
110 | if (State.debugMode) console.log("doubleclick");
111 | // TOOD: implement dclick reset
112 | // location.reload();
113 | }
114 | break;
115 | }
116 | });
117 |
118 | // Add paddles when we know our inputs
119 | State.eventHandler.addEventListener("inputsourceschange", e => {
120 | if (paddles.length != 0) return; // avoid false positives, i.e headset put down, but paddles already instantiated
121 |
122 | XRInput.controllerGrips.forEach((e, i) => {
123 | const paddle = new Paddle();
124 | scene.add(paddle);
125 | paddles.push(paddle);
126 | networking.remoteSync.addLocalObject(paddle, { type: "paddle" }, true);
127 | });
128 |
129 | // local paddle controller component to control player's networked paddle
130 | // note: *all* XRInput data is local.
131 | // only the objects sync'd to it are networked.
132 | // note2: we don't add it to rAF via Update() due to accumulated rAF lagginess over time.
133 |
134 | setInterval(function () {
135 | paddles.forEach((paddle, i) => {
136 | if (XRInput.controllerGrips[i] != undefined) {
137 | paddle.position.lerp(XRInput.controllerGrips[i].position, 0.5);
138 | paddle.quaternion.slerp(XRInput.controllerGrips[i].quaternion, 0.5);
139 | }
140 | });
141 | }, 50);
142 | });
143 |
144 | /// NETWORKING
145 |
146 | networking.remoteSync.addEventListener("open", e => {
147 | initPlacement();
148 | });
149 |
150 | networking.remoteSync.addEventListener("add", (destId, objectId, info) => {
151 | switch (info.type) {
152 | case "ball":
153 | const ball = new Ball(info.position, false); // only add RB once to fake server-client physics model
154 | networking.remoteSync.addRemoteObject(destId, objectId, ball);
155 | scene.add(ball);
156 | break;
157 |
158 | case "paddle":
159 | const p = new Paddle();
160 | networking.remoteSync.addRemoteObject(destId, objectId, p);
161 | scene.add(p);
162 | break;
163 |
164 | case "level":
165 | const l = new Level(info.posRot);
166 | networking.remoteSync.addRemoteObject(destId, objectId, l);
167 | scene.add(l);
168 | break;
169 |
170 | case "placementcube":
171 | const pc = PlacementCube();
172 | networking.remoteSync.addRemoteObject(destId, objectId, pc);
173 | scene.add(pc);
174 | default:
175 | return;
176 | }
177 | });
178 |
179 | networking.remoteSync.addEventListener(
180 | "remove",
181 | (remotePeerId, objectId, object) => {
182 | if (State.debugMode) console.log("removing");
183 | scene.remove(object);
184 | if (object.parent !== null) object.parent.remove(object);
185 | }
186 | );
187 |
188 | export { scene };
189 |
--------------------------------------------------------------------------------
/src/examples/voicestreaming/scene.js:
--------------------------------------------------------------------------------
1 | // demonstrating basic voice streaming and networking shared objects over WebRTC
2 | // everything happens automatically once you enter the same room on two instances
3 | // (XR not required for streaming)
4 |
5 | import {
6 | Scene,
7 | Color,
8 | Mesh,
9 | SphereBufferGeometry,
10 | MeshNormalMaterial,
11 | Object3D,
12 | ShaderMaterial,
13 | AdditiveBlending,
14 | BufferGeometry,
15 | TextureLoader,
16 | Float32BufferAttribute,
17 | Points,
18 | DynamicDrawUsage,
19 | } from "three";
20 | import State from "../../engine/state";
21 | import XRInput from "../../engine/xrinput";
22 | import PeerConnection from "../../engine/networking/peerconnection";
23 | const fs_partycles = require("./shaders/fs_partycles.glsl");
24 | const vs_partycles = require("./shaders/vs_partycles.glsl");
25 | const spark = require("./textures/spark1.png");
26 | const scene = new Scene();
27 |
28 | let networking;
29 |
30 | // 1. get access to mic
31 |
32 | navigator.getUserMedia =
33 | navigator.getUserMedia ||
34 | navigator.webkitGetUserMedia ||
35 | navigator.mozGetUserMedia ||
36 | navigator.msGetUserMedia;
37 |
38 | navigator.mediaDevices
39 | .getUserMedia({ audio: true, video: false })
40 | .then(function (stream) {
41 | // 2. create new peer connection instance, pass audiostream.
42 | networking = new PeerConnection(scene, stream);
43 |
44 | // on stream, play incoming audio
45 | networking.remoteSync.addEventListener("remote_stream", function (
46 | remoteStream
47 | ) {
48 | var audio = document.createElement("AUDIO");
49 | audio.srcObject = remoteStream;
50 | audio.autoplay = true;
51 | });
52 | });
53 |
54 | scene.init = () => {
55 | let particleSystem, uniforms, geometry;
56 |
57 | const radius = 200;
58 | const particles = 100000;
59 |
60 | const geo = new SphereBufferGeometry(0.1, 13, 13);
61 | const mat = new MeshNormalMaterial({ wireframe: true });
62 | const mesh = new Mesh(geo, mat);
63 |
64 | XRInput.controllerGrips.forEach(e => {
65 | const sp = mesh.clone();
66 |
67 | sp.Update = () => {
68 | sp.position.copy(e.position);
69 | sp.quaternion.copy(e.quaternion);
70 | };
71 | networking.remoteSync.addSharedObject(sp);
72 | scene.add(sp);
73 | });
74 |
75 | uniforms = {
76 | pointTexture: { value: new TextureLoader().load(spark) },
77 | };
78 |
79 | var shaderMaterial = new ShaderMaterial({
80 | uniforms: uniforms,
81 | vertexShader: vs_partycles,
82 | fragmentShader: fs_partycles,
83 |
84 | blending: AdditiveBlending,
85 | depthTest: false,
86 | transparent: true,
87 | vertexColors: true,
88 | });
89 |
90 | geometry = new BufferGeometry();
91 |
92 | var positions = [];
93 | var colors = [];
94 | var sizes = [];
95 |
96 | var color = new Color();
97 |
98 | for (var i = 0; i < particles; i++) {
99 | positions.push((Math.random() * 2 - 1) * radius);
100 | positions.push((Math.random() * 2 - 1) * radius);
101 | positions.push((Math.random() * 2 - 1) * radius);
102 |
103 | color.setHSL(i / particles, 1.0, 0.5);
104 |
105 | colors.push(color.r, color.g, color.b);
106 |
107 | sizes.push(20);
108 | }
109 |
110 | geometry.setAttribute("position", new Float32BufferAttribute(positions, 3));
111 | geometry.setAttribute("color", new Float32BufferAttribute(colors, 3));
112 | geometry.setAttribute(
113 | "size",
114 | new Float32BufferAttribute(sizes, 1).setUsage(DynamicDrawUsage)
115 | );
116 |
117 | particleSystem = new Points(geometry, shaderMaterial);
118 |
119 | // networking.remoteSync.addSharedObject(partchromeicleSystem);
120 | scene.add(particleSystem);
121 |
122 | const data = new Object3D();
123 | data.Update = e => {
124 | const time = Date.now() * 0.005;
125 |
126 | particleSystem.rotation.z = 0.01 * time;
127 |
128 | const sizes = geometry.attributes.size.array;
129 |
130 | for (var i = 0; i < particles; i++) {
131 | sizes[i] = 10 * (1 + Math.sin(0.1 * i + time / 3));
132 | }
133 |
134 | geometry.attributes.size.needsUpdate = true;
135 | };
136 | // networking.remoteSync.addSharedObject(data);
137 | scene.add(data);
138 | };
139 | State.eventHandler.addEventListener("peerconnected", e => {
140 | scene.init();
141 | });
142 |
143 | export { scene };
144 |
--------------------------------------------------------------------------------
/src/examples/voicestreaming/shaders/fs_partycles.glsl:
--------------------------------------------------------------------------------
1 |
2 | uniform sampler2D pointTexture;
3 |
4 | varying vec3 vColor;
5 |
6 | void main() {
7 |
8 | gl_FragColor = vec4( vColor, 1.0 );
9 | gl_FragColor *= texture2D( pointTexture, gl_PointCoord );
10 | }
11 |
--------------------------------------------------------------------------------
/src/examples/voicestreaming/shaders/vs_partycles.glsl:
--------------------------------------------------------------------------------
1 | attribute float size;
2 | varying vec3 vColor;
3 |
4 | void main() {
5 |
6 | vColor = color;
7 |
8 | vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
9 |
10 | gl_PointSize = size * ( 300.0 / -mvPosition.z );
11 |
12 | gl_Position = projectionMatrix * mvPosition;
13 |
14 | }
--------------------------------------------------------------------------------
/src/examples/voicestreaming/textures/spark1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PlutoVR/sandcastle/060b6ddb0dba5225479a6860febfa11c96f3e340/src/examples/voicestreaming/textures/spark1.png
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Sandcastle
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import "./index.html";
2 | import "./style.css";
3 | import { loadScene } from "./engine/engine";
4 | import { scene } from "./examples/defaultscene";
5 |
6 | loadScene(scene);
7 |
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@100;200&display=swap");
2 | body {
3 | font-family: "Montserrat", sans-serif;
4 | font-size: 1.25em;
5 | overflow: hidden;
6 | }
7 |
8 | .info {
9 | width: 100%;
10 | z-index: 9;
11 | position: absolute;
12 | bottom: 4em;
13 | text-align: center;
14 | }
15 |
16 | .app {
17 | width: 100%;
18 | height: 100%;
19 | position: absolute;
20 | top: 0;
21 | left: 0;
22 | }
23 |
24 | a {
25 | color: hotpink;
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "allowJs": true,
5 | "typeRoots": ["node_modules/@types/"],
6 | "noImplicitAny": true,
7 | "module": "es6",
8 | "target": "es6",
9 | "jsx": "react",
10 | "moduleResolution": "node",
11 | "types": ["three"]
12 | },
13 | "include": ["./src/**/*.ts"],
14 | "exclude": ["node_modules", "**/*.spec.ts"]
15 | }
16 |
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const HtmlWebPackPlugin = require("html-webpack-plugin");
3 | const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
4 |
5 | const APP_DIR = path.resolve(__dirname, "src/");
6 | const BUILD_DIR = path.resolve(__dirname, "dist/");
7 |
8 | module.exports = {
9 | entry: APP_DIR + "/index.ts",
10 | output: {
11 | path: BUILD_DIR,
12 | filename: "./bundle.js",
13 | },
14 | module: {
15 | rules: [
16 | // {
17 | // test: /\.worker\.js$/,
18 | // use: { loader: 'worker-loader' }
19 | // },
20 | {
21 | test: /\.tsx?$/,
22 | use: "ts-loader",
23 | exclude: /node_modules/,
24 | },
25 | {
26 | test: /\.html$/,
27 | use: [
28 | {
29 | loader: "html-loader",
30 | options: {
31 | minimize: true,
32 | },
33 | },
34 | ],
35 | },
36 | {
37 | test: /\.css$/i,
38 | use: ["style-loader", "css-loader"],
39 | },
40 | {
41 | test: /\.(js|jsx)$/,
42 | exclude: /node_modules/,
43 | use: {
44 | loader: "babel-loader",
45 | options: {
46 | presets: ["@babel/preset-env"],
47 | },
48 | },
49 | },
50 | {
51 | test: /\.(jpe?g|png|gif|bmp|svg)$/,
52 | use: [
53 | {
54 | loader: "file-loader",
55 | options: {
56 | esModule: false,
57 | name: "[name].[ext]",
58 | outputPath: "assets/images/",
59 | publicPath: "assets/images/",
60 | },
61 | },
62 | ],
63 | },
64 | {
65 | test: /\.(gltf)$/,
66 | use: [
67 | {
68 | loader: "gltf-webpack-loader",
69 | },
70 | ],
71 | },
72 | {
73 | test: /\.(glb|obj|mtl|fbx|dae|bin|vrm)$/,
74 | use: [
75 | {
76 | loader: "file-loader",
77 | options: {
78 | esModule: false,
79 | name: "[name].[ext]",
80 | outputPath: "assets/models/",
81 | publicPath: "assets/models/",
82 | },
83 | },
84 | ],
85 | },
86 | {
87 | test: /\.(ogg|mp3|wav|mpe?g)$/,
88 | use: [
89 | {
90 | loader: "file-loader",
91 | options: {
92 | esModule: false,
93 | name: "[name].[ext]",
94 | outputPath: "assets/audio/",
95 | publicPath: "assets/audio/",
96 | },
97 | },
98 | ],
99 | },
100 | {
101 | test: /\.(glsl|vs|fs)$/,
102 | loader: "shader-loader",
103 | },
104 | ],
105 | },
106 | watchOptions: {
107 | ignored: /node_modules/,
108 | },
109 | plugins: [
110 | new HtmlWebPackPlugin({
111 | template: "./src/index.html",
112 | filename: "./index.html",
113 | }),
114 | new ScriptExtHtmlWebpackPlugin({
115 | defaultAttribute: "defer",
116 | }),
117 | ],
118 | resolve: {
119 | extensions: [".js", ".es6", ".ts"],
120 | },
121 | };
122 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const merge = require("webpack-merge");
3 | const common = require("./webpack.common");
4 |
5 | const APP_DIR = path.resolve(__dirname, "src/");
6 |
7 | module.exports = merge(common, {
8 | mode: "development",
9 | devtool: "eval-source-map",
10 | devServer: {
11 | contentBase: APP_DIR,
12 | hot: true,
13 | open: true,
14 | disableHostCheck: true,
15 | port: 1234,
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const merge = require("webpack-merge");
2 | const ImageminPlugin = require("imagemin-webpack-plugin").default;
3 | const imageminMozjpeg = require("imagemin-mozjpeg");
4 | const CompressionPlugin = require("compression-webpack-plugin");
5 | const { CleanWebpackPlugin } = require("clean-webpack-plugin");
6 |
7 | const common = require("./webpack.common");
8 |
9 | module.exports = merge(common, {
10 | mode: "production",
11 | performance: {
12 | hints: false,
13 | },
14 | optimization: {
15 | minimize: true,
16 | splitChunks: {
17 | chunks: "all",
18 | },
19 | },
20 | plugins: [
21 | new ImageminPlugin({
22 | test: /\.(jpe?g|png|gif|svg)$/i,
23 | pngquant: {
24 | // lossy png compressor, remove for default lossless
25 | quality: "75",
26 | },
27 | plugins: [
28 | imageminMozjpeg({
29 | // lossy jpg compressor, remove for default lossless
30 | quality: "75",
31 | }),
32 | ],
33 | }),
34 | new CompressionPlugin({
35 | test: /\.(html|css|js)(\?.*)?$/i, // only compressed html/css/js, skips compressing sourcemaps etc
36 | }),
37 | new CleanWebpackPlugin({
38 | verbose: true,
39 | cleanStaleWebpackAssets: true,
40 | }),
41 | ],
42 | });
43 |
--------------------------------------------------------------------------------