diff --git a/package.json b/package.json index d724d140eb838e392c7eaf4cfb483b039277c0ba..3212db6bdc3cf7928fbd2ed1269697920f67e258 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "aframe-input-mapping-component": "https://github.com/mozillareality/aframe-input-mapping-component#hubs/master", "aframe-motion-capture-components": "https://github.com/mozillareality/aframe-motion-capture-components#1ca616fa67b627e447b23b35a09da175d8387668", "aframe-physics-extras": "^0.1.3", - "aframe-physics-system": "github:donmccurdy/aframe-physics-system", + "aframe-physics-system": "https://github.com/mozillareality/aframe-physics-system#monitor-scale-on-body", "aframe-rounded": "^1.0.3", "aframe-slice9-component": "^1.0.0", "aframe-teleport-controls": "https://github.com/mozillareality/aframe-teleport-controls#hubs/master", diff --git a/src/components/auto-box-collider.js b/src/components/auto-box-collider.js new file mode 100644 index 0000000000000000000000000000000000000000..2d3d2f53bbbd4cbe18a3f9fd1f19603badd8f907 --- /dev/null +++ b/src/components/auto-box-collider.js @@ -0,0 +1,46 @@ +AFRAME.registerComponent("auto-box-collider", { + schema: { + resize: { default: false }, + resizeLength: { default: 0.5 } + }, + + init() { + this.onLoaded = this.onLoaded.bind(this); + this.el.addEventListener("model-loaded", this.onLoaded); + }, + + onLoaded() { + this.el.removeEventListener("model-loaded", this.onLoaded); + const rotation = this.el.object3D.rotation.clone(); + this.el.object3D.rotation.set(0, 0, 0); + const { min, max } = new THREE.Box3().setFromObject(this.el.object3DMap.mesh); + const halfExtents = new THREE.Vector3() + .addVectors(min.clone().negate(), max) + .multiplyScalar(0.5 / this.el.object3D.scale.x); + this.el.setAttribute("shape", { + shape: "box", + halfExtents: halfExtents, + offset: new THREE.Vector3(0, halfExtents.y, 0) + }); + if (this.data.resize) { + this.resize(min, max); + } + this.el.object3D.rotation.copy(rotation); + this.el.removeAttribute("auto-box-collider"); + }, + + // Adjust the scale such that the object fits within a box of a specified size. + resize(min, max) { + const dX = Math.abs(max.x - min.x); + const dY = Math.abs(max.y - min.y); + const dZ = Math.abs(max.z - min.z); + const lengthOfLongestComponent = Math.max(dX, dY, dZ); + const correctiveFactor = this.data.resizeLength / lengthOfLongestComponent; + const scale = this.el.object3D.scale; + this.el.setAttribute("scale", { + x: scale.x * correctiveFactor, + y: scale.y * correctiveFactor, + z: scale.z * correctiveFactor + }); + } +}); diff --git a/src/components/auto-scale-cannon-physics-body.js b/src/components/auto-scale-cannon-physics-body.js new file mode 100644 index 0000000000000000000000000000000000000000..e3ab60400e00f253e670736c2f94c44e301f4658 --- /dev/null +++ b/src/components/auto-scale-cannon-physics-body.js @@ -0,0 +1,27 @@ +function almostEquals(epsilon, u, v) { + return Math.abs(u.x - v.x) < epsilon && Math.abs(u.y - v.y) < epsilon && Math.abs(u.z - v.z) < epsilon; +} + +AFRAME.registerComponent("auto-scale-cannon-physics-body", { + dependencies: ["body"], + + init() { + this.body = this.el.components["body"]; + this.prevScale = this.el.object3D.scale.clone(); + this.nextUpdateTime = -1; + }, + + tick(t) { + const scale = this.el.object3D.scale; + // Note: This only checks if the LOCAL scale of the object3D changes. + if (!almostEquals(0.001, scale, this.prevScale)) { + this.prevScale.copy(scale); + this.nextUpdateTime = t + 100; + } + + if (this.nextUpdateTime > 0 && t > this.nextUpdateTime) { + this.nextUpdateTime = -1; + this.body.updateCannonScale(); + } + } +}); diff --git a/src/components/gltf-model-plus.js b/src/components/gltf-model-plus.js index 55d62b10e997a943d39c6e593cb35256c67b0e6c..686245b492555beaf311a7f2f9b94c07dd600d2a 100644 --- a/src/components/gltf-model-plus.js +++ b/src/components/gltf-model-plus.js @@ -203,11 +203,11 @@ function cachedLoadGLTF(src, preferredTechnique, onProgress) { AFRAME.registerComponent("gltf-model-plus", { schema: { src: { type: "string" }, - inflate: { default: false }, - preferredTechnique: { default: AFRAME.utils.device.isMobile() ? "KHR_materials_unlit" : "pbrMetallicRoughness" } + inflate: { default: false } }, init() { + this.preferredTechnique = AFRAME.utils.device.isMobile() ? "KHR_materials_unlit" : "pbrMetallicRoughness"; this.loadTemplates(); }, @@ -245,7 +245,7 @@ AFRAME.registerComponent("gltf-model-plus", { } const gltfPath = THREE.LoaderUtils.extractUrlBase(src); - const model = await cachedLoadGLTF(src, this.data.preferredTechnique); + const model = await cachedLoadGLTF(src, this.preferredTechnique); // If we started loading something else already // TODO: there should be a way to cancel loading instead diff --git a/src/components/offset-relative-to.js b/src/components/offset-relative-to.js index 3cd45b942ed4573ee9b588a467de456af801f62f..82a90717ba22137ac38e045a893804f60a183d3b 100644 --- a/src/components/offset-relative-to.js +++ b/src/components/offset-relative-to.js @@ -12,16 +12,38 @@ AFRAME.registerComponent("offset-relative-to", { }, on: { type: "string" + }, + selfDestruct: { + default: false } }, init() { - this.updateOffset(); - this.el.sceneEl.addEventListener(this.data.on, this.updateOffset.bind(this)); + this.updateOffset = this.updateOffset.bind(this); + if (this.data.on) { + this.el.sceneEl.addEventListener(this.data.on, this.updateOffset); + } else { + this.updateOffset(); + } }, - updateOffset() { - const offsetVector = new THREE.Vector3().copy(this.data.offset); - this.data.target.object3D.localToWorld(offsetVector); - this.el.setAttribute("position", offsetVector); - this.data.target.object3D.getWorldQuaternion(this.el.object3D.quaternion); - } + + updateOffset: (function() { + const offsetVector = new THREE.Vector3(); + return function() { + const obj = this.el.object3D; + const target = this.data.target.object3D; + offsetVector.copy(this.data.offset); + target.localToWorld(offsetVector); + if (obj.parent) { + obj.parent.worldToLocal(offsetVector); + } + obj.position.copy(offsetVector); + target.getWorldQuaternion(obj.quaternion); + if (this.data.selfDestruct) { + if (this.data.on) { + this.el.sceneEl.removeEventListener(this.data.on, this.updateOffset); + } + this.el.removeAttribute("offset-relative-to"); + } + }; + })() }); diff --git a/src/components/position-at-box-shape-border.js b/src/components/position-at-box-shape-border.js new file mode 100644 index 0000000000000000000000000000000000000000..f85d744a9f532290dccc0dfc799e0959831e4d6c --- /dev/null +++ b/src/components/position-at-box-shape-border.js @@ -0,0 +1,65 @@ +const PI = Math.PI; +const HALF_PI = PI / 2; +const THREE_HALF_PI = 3 * PI / 2; +const rotations = [THREE_HALF_PI, HALF_PI, 0, PI]; +const right = new THREE.Vector3(1, 0, 0); +const forward = new THREE.Vector3(0, 0, 1); +const left = new THREE.Vector3(-1, 0, 0); +const back = new THREE.Vector3(0, 0, -1); +const dirs = [left, right, forward, back]; + +AFRAME.registerComponent("position-at-box-shape-border", { + schema: { + target: { type: "string" } + }, + + init() { + this.cam = this.el.sceneEl.camera.el.object3D; + }, + + tick: (function() { + const camWorldPos = new THREE.Vector3(); + const targetPosition = new THREE.Vector3(); + const pointOnBoxFace = new THREE.Vector3(); + return function() { + if (!this.shape) { + this.shape = this.el.components["shape"]; + if (!this.shape) return; + } + if (!this.target) { + this.target = this.el.querySelector(this.data.target).object3D; + if (!this.target) return; + } + const halfExtents = this.shape.data.halfExtents; + const halfExtentDirs = ["x", "x", "z", "z"]; + this.cam.getWorldPosition(camWorldPos); + + let minSquareDistance = Infinity; + let targetDir = dirs[0]; + let targetHalfExtent = halfExtents[halfExtentDirs[0]]; + let targetRotation = rotations[0]; + + for (let i = 0; i < dirs.length; i++) { + const dir = dirs[i]; + const halfExtent = halfExtents[halfExtentDirs[i]]; + pointOnBoxFace.copy(dir).multiplyScalar(halfExtent); + this.el.object3D.localToWorld(pointOnBoxFace); + const squareDistance = pointOnBoxFace.distanceToSquared(camWorldPos); + if (squareDistance < minSquareDistance) { + minSquareDistance = squareDistance; + targetDir = dir; + targetHalfExtent = halfExtent; + targetRotation = rotations[i]; + } + } + + this.target.position.copy( + targetPosition + .copy(targetDir) + .multiplyScalar(targetHalfExtent) + .add(this.shape.data.offset) + ); + this.target.rotation.set(0, targetRotation, 0); + }; + })() +}); diff --git a/src/hub.html b/src/hub.html index 49723f07d5663cc4d03122e21c9477a63f3f6321..3d1eb594544eefa98ad90dbb0ce6b2a4566b1664 100644 --- a/src/hub.html +++ b/src/hub.html @@ -162,21 +162,44 @@ class="interactable" super-networked-interactable="counter: #counter; mass: 1;" body="type: dynamic; shape: none; mass: 1;" + auto-scale-cannon-physics-body grabbable - stretchable="useWorldPosition: true;" + stretchable="useWorldPosition: true; usePhysics: never" hoverable duck - sticky-object + sticky-object="autoLockOnRelease: true;" ></a-entity> </template> + <template id="interactable-model"> + <a-entity + gltf-model-plus="inflate: false;" + class="interactable" + super-networked-interactable="counter: #media-counter; mass: 1;" + body="type: dynamic; shape: none; mass: 1;" + grabbable + stretchable="useWorldPosition: true; usePhysics: never" + hoverable + sticky-object="autoLockOnRelease: true; autoLockOnLoad: true;" + auto-box-collider + position-at-box-shape-border="target:.delete-button" + auto-scale-cannon-physics-body + > + <a-entity class="delete-button" visible-while-frozen scale="0.08 0.08 0.08"> + <a-entity mixin="rounded-text-button" remove-object-button position="0 0 0"> </a-entity> + <a-entity text=" value:Delete; width:2.5; align:center;" text-raycast-hack position="0 0 0.01"></a-entity> + </a-entity> + </a-entity> + </template> + <template id="interactable-image"> <a-entity class="interactable" super-networked-interactable="counter: #media-counter; mass: 1;" body="type: dynamic; shape: none; mass: 1;" + auto-scale-cannon-physics-body grabbable - stretchable="useWorldPosition: true;" + stretchable="useWorldPosition: true; usePhysics: never" hoverable geometry="primitive: plane" material="shader: flat; side: double; transparent: true;" diff --git a/src/hub.js b/src/hub.js index 92fc1fdf833bb20e480cf5a947054df04ff8df0b..6631d12006ca3d4dd7bcc434eda054adedf075e7 100644 --- a/src/hub.js +++ b/src/hub.js @@ -64,11 +64,14 @@ import "./components/css-class"; import "./components/scene-shadow"; import "./components/avatar-replay"; import "./components/image-plus"; +import "./components/auto-box-collider"; import "./components/pinch-to-move"; import "./components/look-on-mobile"; import "./components/pitch-yaw-rotator"; import "./components/input-configurator"; import "./components/sticky-object"; +import "./components/auto-scale-cannon-physics-body"; +import "./components/position-at-box-shape-border"; import ReactDOM from "react-dom"; import React from "react"; @@ -298,12 +301,29 @@ const onReady = async () => { NAF.connection.entities.completeSync(ev.detail.clientId); }); + const offset = { x: 0, y: 0, z: -1.5 }; const addMedia = url => { - const image = document.createElement("a-entity"); - image.id = "interactable-image-" + Date.now(); - image.setAttribute("image-plus", "src", url); - image.setAttribute("networked", { template: "#interactable-image" }); - scene.appendChild(image); + const scene = AFRAME.scenes[0]; + if (url.endsWith(".gltf") || url.endsWith(".glb")) { + const model = document.createElement("a-entity"); + model.id = "interactable-model-" + Date.now(); + model.setAttribute("offset-relative-to", { + on: "model-loaded", + target: "#player-camera", + offset: offset, + selfDestruct: true + }); + model.setAttribute("gltf-model-plus", "src", url); + model.setAttribute("auto-box-collider", { resize: true }); + model.setAttribute("networked", { template: "#interactable-model" }); + scene.appendChild(model); + } else { + const image = document.createElement("a-entity"); + image.id = "interactable-image-" + Date.now(); + image.setAttribute("image-plus", "src", url); + image.setAttribute("networked", { template: "#interactable-image" }); + scene.appendChild(image); + } }; scene.addEventListener("add_media", e => { diff --git a/src/network-schemas.js b/src/network-schemas.js index 7addf6702740df5e67d4ebd0dd2f92849e91f07e..11c6e411dac5254b6382231f9f5013a2e78966fd 100644 --- a/src/network-schemas.js +++ b/src/network-schemas.js @@ -105,6 +105,11 @@ function registerNetworkSchemas() { "image-plus" ] }); + + NAF.schemas.add({ + template: "#interactable-model", + components: ["position", "rotation", "scale", "gltf-model-plus"] + }); } export default registerNetworkSchemas; diff --git a/src/react-components/info-dialog.js b/src/react-components/info-dialog.js index fe7c0e8df2419f751414bc05edcb003df64ce1b0..83b46b9dfee5569044581b44879566a13ec3c934 100644 --- a/src/react-components/info-dialog.js +++ b/src/react-components/info-dialog.js @@ -206,7 +206,7 @@ class InfoDialog extends Component { <div className="add-media-form"> <input type="url" - placeholder="Image or Video URL" + placeholder="Image, Video, or GLTF URL" className="add-media-form__link_field" value={this.state.addMediaUrl} onChange={e => this.setState({ addMediaUrl: e.target.value })} diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index e5c56aa01147f9680c3ee6aa7fcf6e44658f4523..c0733064a8fa05a513b040aeeb54401aca12ef1a 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -329,7 +329,7 @@ class UIRoot extends Component { mediaSource: "screen", // Work around BMO 1449832 by calculating the width. This will break for multi monitors if you share anything // other than your current monitor that has a different aspect ratio. - width: (screen.width / screen.height) * 720, + width: 720 * screen.width / screen.height, height: 720, frameRate: 30 } diff --git a/src/vendor/GLTFLoader.js b/src/vendor/GLTFLoader.js index 7610044b41b6dd9956b2d84984e46671de46e494..1df68673f4d773200f36899bba60be7b7c308a21 100644 --- a/src/vendor/GLTFLoader.js +++ b/src/vendor/GLTFLoader.js @@ -9,6 +9,19 @@ * @author Don McCurdy / https://www.donmccurdy.com */ +async function toFarsparkURL(url){ + var mediaJson = await fetch("https://smoke-dev.reticulum.io/api/v1/media", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + media: {url} + }) + }).then(r => r.json()); + return mediaJson.images.raw; +} + THREE.GLTFLoader = ( function () { function GLTFLoader( manager ) { @@ -25,7 +38,7 @@ THREE.GLTFLoader = ( function () { crossOrigin: 'Anonymous', - load: function ( url, onLoad, onProgress, onError ) { + load: async function ( url, onLoad, onProgress, onError ) { var scope = this; @@ -37,7 +50,9 @@ THREE.GLTFLoader = ( function () { loader.setResponseType( 'arraybuffer' ); - loader.load( url, function ( data ) { + var farsparkURL = await toFarsparkURL(url); + + loader.load( farsparkURL, function ( data ) { try { @@ -1598,7 +1613,7 @@ THREE.GLTFLoader = ( function () { * @param {number} bufferIndex * @return {Promise<ArrayBuffer>} */ - GLTFParser.prototype.loadBuffer = function ( bufferIndex ) { + GLTFParser.prototype.loadBuffer = async function ( bufferIndex ) { var bufferDef = this.json.buffers[ bufferIndex ]; var loader = this.fileLoader; @@ -1618,9 +1633,11 @@ THREE.GLTFLoader = ( function () { var options = this.options; + var farsparkURL = await toFarsparkURL(resolveURL(bufferDef.uri, options.path)); + return new Promise( function ( resolve, reject ) { - loader.load( resolveURL( bufferDef.uri, options.path ), resolve, undefined, function () { + loader.load( farsparkURL, resolve, undefined, function () { reject( new Error( 'THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".' ) ); @@ -1784,7 +1801,7 @@ THREE.GLTFLoader = ( function () { * @param {number} textureIndex * @return {Promise<THREE.Texture>} */ - GLTFParser.prototype.loadTexture = function ( textureIndex ) { + GLTFParser.prototype.loadTexture = async function ( textureIndex ) { var parser = this; var json = this.json; @@ -1798,11 +1815,12 @@ THREE.GLTFLoader = ( function () { var sourceURI = source.uri; var isObjectURL = false; - if ( source.bufferView !== undefined ) { + var hasBufferView = source.bufferView !== undefined; + if ( hasBufferView ) { // Load binary image data from bufferView, if provided. - sourceURI = parser.getDependency( 'bufferView', source.bufferView ).then( function ( bufferView ) { + sourceURI = await parser.getDependency( 'bufferView', source.bufferView ).then( function ( bufferView ) { isObjectURL = true; var blob = new Blob( [ bufferView ], { type: source.mimeType } ); @@ -1813,6 +1831,11 @@ THREE.GLTFLoader = ( function () { } + var urlToLoad = resolveURL(sourceURI, options.path); + if (!hasBufferView){ + urlToLoad = await toFarsparkURL(urlToLoad); + } + return Promise.resolve( sourceURI ).then( function ( sourceURI ) { // Load Texture resource. @@ -1821,7 +1844,7 @@ THREE.GLTFLoader = ( function () { return new Promise( function ( resolve, reject ) { - loader.load( resolveURL( sourceURI, options.path ), resolve, undefined, reject ); + loader.load( urlToLoad, resolve, undefined, reject ); } ); diff --git a/yarn.lock b/yarn.lock index 589589dc64406716bd1084f40581f5ad2b410af0..de13d49c2eec6911c15c7962adbb5776dc27a3da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -182,9 +182,9 @@ aframe-physics-extras@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/aframe-physics-extras/-/aframe-physics-extras-0.1.3.tgz#803e2164fb96c0a80f2d1a81458f3277f262b130" -"aframe-physics-system@github:donmccurdy/aframe-physics-system": +"aframe-physics-system@https://github.com/mozillareality/aframe-physics-system#monitor-scale-on-body": version "3.1.2" - resolved "https://codeload.github.com/donmccurdy/aframe-physics-system/tar.gz/c142a301e3ce76f88bab817c89daa5b3d4d97815" + resolved "https://github.com/mozillareality/aframe-physics-system#7cd2bf83d125194cc76aa48bce700884de113215" dependencies: browserify "^14.3.0" budo "^10.0.3" @@ -3637,6 +3637,10 @@ gh-got@^6.0.0: got "^7.0.0" is-plain-obj "^1.1.0" +gif-engine-js@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gif-engine-js/-/gif-engine-js-1.0.1.tgz#438dedd0bb1788e8b03cba4452524b015ec0158c" + github-username@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/github-username/-/github-username-4.1.0.tgz#cbe280041883206da4212ae9e4b5f169c30bf417" @@ -4952,7 +4956,7 @@ loader-utils@^0.2.16: json5 "^0.5.0" object-assign "^4.0.1" -loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: +loader-utils@^1.0.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" dependencies: @@ -7191,7 +7195,7 @@ sax@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" -schema-utils@^0.4.2, schema-utils@^0.4.3, schema-utils@^0.4.5: +schema-utils@^0.4.0, schema-utils@^0.4.2, schema-utils@^0.4.3, schema-utils@^0.4.5: version "0.4.5" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e" dependencies: @@ -8668,6 +8672,13 @@ worker-farm@^1.5.2: errno "^0.1.4" xtend "^4.0.1" +worker-loader@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-2.0.0.tgz#45fda3ef76aca815771a89107399ee4119b430ac" + dependencies: + loader-utils "^1.0.0" + schema-utils "^0.4.0" + wrap-ansi@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"