├── .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 | ![Sandcastle Samples](./sandcastleprojects.png) 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 | --------------------------------------------------------------------------------