From b7374964d1e70b91a92baa833d34ca9c44495d89 Mon Sep 17 00:00:00 2001 From: joni <johnfshaughnessy@gmail.com> Date: Thu, 12 Jul 2018 19:39:35 -0700 Subject: [PATCH] Add the media loading entity --- package.json | 4 +- src/components/auto-box-collider.js | 47 +--------- src/components/gltf-model-plus.js | 10 ++- src/components/image-plus.js | 44 +++++----- src/components/sticky-object.js | 10 +-- .../super-networked-interactable.js | 7 +- src/hub.html | 10 +-- src/hub.js | 2 +- src/utils/auto-box-collider.js | 79 +++++++++++++++++ src/utils/media-utils.js | 85 +++++++++++++------ yarn.lock | 8 +- 11 files changed, 187 insertions(+), 119 deletions(-) create mode 100644 src/utils/auto-box-collider.js diff --git a/package.json b/package.json index 744b8d815..afa65f979 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": "https://github.com/mozillareality/aframe-physics-system#hubs/master", + "aframe-physics-system": "https://github.com/mozillareality/aframe-physics-system#static-dynamic-mass-change", "aframe-rounded": "^1.0.3", "aframe-slice9-component": "^1.0.0", "aframe-teleport-controls": "https://github.com/mozillareality/aframe-teleport-controls#hubs/master", @@ -44,7 +44,7 @@ "moment-timezone": "^0.5.14", "moving-average": "^1.0.0", "naf-janus-adapter": "^0.9.0", - "networked-aframe": "https://github.com/mozillareality/networked-aframe#mr-social-client/master", + "networked-aframe": "https://github.com/mozillareality/networked-aframe#bug/aframe-initialization-race-condition", "nipplejs": "https://github.com/mozillareality/nipplejs#mr-social-client/master", "phoenix": "^1.3.0", "query-string": "^5.0.1", diff --git a/src/components/auto-box-collider.js b/src/components/auto-box-collider.js index 633fd0d44..f611c768a 100644 --- a/src/components/auto-box-collider.js +++ b/src/components/auto-box-collider.js @@ -1,52 +1,9 @@ +import { calculateBoxShape } from "../utils/auto-box-collider"; 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); - }, - - remove() { - this.el.removeEventListener("model-loaded", this.onLoaded); - }, - - 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); - const center = new THREE.Vector3().addVectors(min, max).multiplyScalar(0.5); - this.el.object3D.worldToLocal(center); - this.el.object3DMap.mesh.position.sub(center); - - if (this.data.resize) { - this.resize(min, max); - } - this.el.setAttribute("shape", { - shape: "box", - halfExtents: halfExtents - }); - 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 - }); - } + init() {} }); diff --git a/src/components/gltf-model-plus.js b/src/components/gltf-model-plus.js index b449ea9ae..26cd1fc81 100644 --- a/src/components/gltf-model-plus.js +++ b/src/components/gltf-model-plus.js @@ -191,7 +191,15 @@ function cachedLoadGLTF(src, basePath, preferredTechnique, onProgress) { gltfLoader.load(src, resolve, onProgress, reject); }); } - return GLTFCache[src].then(cloneGltf); + return GLTFCache[src] + .then(gltf => { + return new Promise((resolve, reject) => { + window.setTimeout(() => { + resolve(gltf); + }, 2000); + }); + }) + .then(cloneGltf); } /** diff --git a/src/components/image-plus.js b/src/components/image-plus.js index 2a0a2c5d0..08cf851b8 100644 --- a/src/components/image-plus.js +++ b/src/components/image-plus.js @@ -67,8 +67,6 @@ errorImage.onload = () => { }; AFRAME.registerComponent("image-plus", { - dependencies: ["geometry"], - schema: { src: { type: "string" }, contentType: { type: "string" } @@ -85,27 +83,13 @@ AFRAME.registerComponent("image-plus", { height = geo.height * ratio; } } else if (geo && geo.height) { + //TODO width = geo.width / ratio; } else { width = Math.min(1.0, 1.0 / ratio); height = Math.min(1.0, ratio); } - this.el.setAttribute("geometry", { width, height }); - this.el.setAttribute("shape", { - shape: "box", - halfExtents: { - x: width / 2, - y: height / 2, - z: 0.05 - } - }); - }, - - init() { - const material = new THREE.MeshBasicMaterial(); - material.side = THREE.DoubleSide; - material.transparent = true; - this.el.getObject3D("mesh").material = material; + return { width, height }; }, remove() { @@ -230,7 +214,7 @@ AFRAME.registerComponent("image-plus", { } else if (contentType.startsWith("video")) { texture = await this.loadVideo(url); } else { - throw new Error(`Unknown centent type: ${contentType}`); + throw new Error(`Unknown content type: ${contentType}`); } textureCache.set(url, { count: 1, texture }); @@ -246,9 +230,27 @@ AFRAME.registerComponent("image-plus", { texture = errorTexture; } - const material = this.el.getObject3D("mesh").material; + const { width, height } = this._fit( + texture.image.videoWidth || texture.image.width, + texture.image.videoHeight || texture.image.height + ); + const material = new THREE.MeshBasicMaterial(); + material.side = THREE.DoubleSide; + material.transparent = true; material.map = texture; material.needsUpdate = true; - this._fit(texture.image.videoWidth || texture.image.width, texture.image.videoHeight || texture.image.height); + + const geometry = new THREE.PlaneGeometry(width, height, 1, 1); + this.mesh = new THREE.Mesh(geometry, material); + this.el.setObject3D("mesh", this.mesh); + this.el.setAttribute("shape", { + shape: "box", + halfExtents: { x: width / 2, y: height / 2, z: 0.05 } + }); + if (this.el.components.body && this.el.components.body.body) { + this.el.components.body.syncToPhysics(); // not sure if necessary? + this.el.components.body.updateCannonScale(); + } + this.el.emit("image-loaded"); } }); diff --git a/src/components/sticky-object.js b/src/components/sticky-object.js index d41577907..22120af4a 100644 --- a/src/components/sticky-object.js +++ b/src/components/sticky-object.js @@ -1,6 +1,6 @@ /* global THREE, CANNON, AFRAME */ AFRAME.registerComponent("sticky-object", { - dependencies: ["body", "super-networked-interactable"], + dependencies: ["body"], schema: { autoLockOnLoad: { default: false }, @@ -27,14 +27,10 @@ AFRAME.registerComponent("sticky-object", { }, setLocked(locked) { - if (!NAF.utils.isMine(this.el)) return; + if (this.el.components.networked && !NAF.utils.isMine(this.el)) return; - const mass = this.el.components["super-networked-interactable"].data.mass; this.locked = locked; - this.el.body.type = locked ? window.CANNON.Body.STATIC : window.CANNON.Body.DYNAMIC; - this.el.setAttribute("body", { - mass: locked ? 0 : mass - }); + this.el.setAttribute("body", { type: locked ? "static" : "dynamic" }); }, _onBodyLoaded() { diff --git a/src/components/super-networked-interactable.js b/src/components/super-networked-interactable.js index 3b84bb7ac..844dc1df1 100644 --- a/src/components/super-networked-interactable.js +++ b/src/components/super-networked-interactable.js @@ -5,7 +5,6 @@ */ AFRAME.registerComponent("super-networked-interactable", { schema: { - mass: { default: 1 }, hapticsMassVelocityFactor: { default: 0.1 }, counter: { type: "selector" } }, @@ -18,7 +17,7 @@ AFRAME.registerComponent("super-networked-interactable", { NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { this.networkedEl = networkedEl; if (!NAF.utils.isMine(networkedEl)) { - this.el.setAttribute("body", { type: "static", mass: 0 }); + this.el.setAttribute("body", { type: "static" }); } else { this.counter.register(networkedEl); } @@ -51,7 +50,7 @@ AFRAME.registerComponent("super-networked-interactable", { this.hand = e.detail.hand; if (this.networkedEl && !NAF.utils.isMine(this.networkedEl)) { if (NAF.utils.takeOwnership(this.networkedEl)) { - this.el.setAttribute("body", { type: "dynamic", mass: this.data.mass }); + this.el.setAttribute("body", { type: "dynamic" }); this.counter.register(this.networkedEl); } else { this.el.emit("grab-end", { hand: this.hand }); @@ -61,7 +60,7 @@ AFRAME.registerComponent("super-networked-interactable", { }, _onOwnershipLost: function() { - this.el.setAttribute("body", { type: "static", mass: 0 }); + this.el.setAttribute("body", { type: "static" }); this.el.emit("grab-end", { hand: this.hand }); this.hand = null; this.counter.deregister(this.el); diff --git a/src/hub.html b/src/hub.html index 572ce73d4..eadce7db7 100644 --- a/src/hub.html +++ b/src/hub.html @@ -33,7 +33,7 @@ <a-scene renderer="antialias: true" networked-scene="adapter: janus; audio: true; debug: true; connectOnLoad: false;" - physics="gravity: -6;" + physics="gravity: -6; debug: true;" mute-mic="eventSrc: a-scene; toggleEvents: action_mute" freeze-controller="toggleEvent: action_freeze" personal-space-bubble="debug: false;" @@ -86,7 +86,6 @@ <img id="water-normal-map" crossorigin="anonymous" src="./assets/waternormals.jpg"> <!-- Templates --> - <template id="video-template"> <a-entity class="video" geometry="primitive: plane;" material="side: double; shader: flat;" networked-video-player></a-entity> </template> @@ -163,15 +162,13 @@ <a-entity gltf-model-plus="inflate: false;" class="interactable" - super-networked-interactable="counter: #media-counter; mass: 1;" + super-networked-interactable="counter: #media-counter;" 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 destroy-at-extreme-distances > <a-entity class="delete-button" visible-while-frozen scale="0.08 0.08 0.08"> @@ -184,13 +181,12 @@ <template id="interactable-image"> <a-entity class="interactable" - super-networked-interactable="counter: #media-counter; mass: 1;" + super-networked-interactable="counter: #media-counter;" body="type: dynamic; shape: none; mass: 1;" auto-scale-cannon-physics-body grabbable stretchable="useWorldPosition: true; usePhysics: never" hoverable - geometry="primitive: plane" image-plus sticky-object="autoLockOnLoad: true; autoLockOnRelease: true;" position-at-box-shape-border="target:.delete-button;dirs:forward,back" diff --git a/src/hub.js b/src/hub.js index 123af93bf..11b1bac1f 100644 --- a/src/hub.js +++ b/src/hub.js @@ -325,7 +325,7 @@ const onReady = async () => { e.preventDefault(); const imgUrl = e.dataTransfer.getData("url"); if (imgUrl) { - console.log("Droped: ", imgUrl); + console.log("Dropped: ", imgUrl); addMedia(imgUrl); } }); diff --git a/src/utils/auto-box-collider.js b/src/utils/auto-box-collider.js new file mode 100644 index 000000000..391607882 --- /dev/null +++ b/src/utils/auto-box-collider.js @@ -0,0 +1,79 @@ +export function getBox(entity) { + const box = new THREE.Box3(); + const mesh = entity.getObject3D("mesh"); + const rotation = entity.object3D.rotation.clone(); + entity.object3D.rotation.set(0, 0, 0); + entity.object3D.updateMatrixWorld(true); + + box.expandByObject = expandByObject; + box.setFromObject(mesh); + entity.object3D.rotation.copy(rotation); + return box; +} + +export function getCenterAndHalfExtents(entity, box, center, halfExtents) { + const { min, max } = box; + center.addVectors(min, max).multiplyScalar(0.5); + entity.object3D.worldToLocal(center); + halfExtents + .copy(min) + .negate() + .add(max) + .multiplyScalar(0.5 / entity.object3D.scale.x); +} + +export function getScaleCoefficient(length, box) { + const { max, min } = box; + 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); + return length / lengthOfLongestComponent; +} + +const expandByObject = (function() { + // Computes the world-axis-aligned bounding box of an object (including its children), + // accounting for both the object's, and children's, world transforms + + var scope, i, l; + + var v1 = new THREE.Vector3(); + + function traverse(node) { + var geometry = node.geometry; + + if (geometry !== undefined) { + if (geometry.isGeometry) { + var vertices = geometry.vertices; + + for (i = 0, l = vertices.length; i < l; i++) { + v1.copy(vertices[i]); + v1.applyMatrix4(node.matrixWorld); + + scope.expandByPoint(v1); + } + } else if (geometry.isBufferGeometry) { + var attribute = geometry.attributes.position; + + if (attribute !== undefined) { + for (i = 0, l = attribute.count; i < l; i++) { + v1.fromBufferAttribute(attribute, i).applyMatrix4(node.matrixWorld); + if (isNaN(v1.x) || isNaN(v1.y) || isNaN(v1.z)) { + continue; + } + + scope.expandByPoint(v1); + } + } + } + } + } + + return function expandByObject(object) { + scope = this; + + object.traverse(traverse); + + return this; + }; +})(); diff --git a/src/utils/media-utils.js b/src/utils/media-utils.js index 211ebd9d5..cd91971d8 100644 --- a/src/utils/media-utils.js +++ b/src/utils/media-utils.js @@ -1,3 +1,4 @@ +import { getBox, getCenterAndHalfExtents, getScaleCoefficient } from "./auto-box-collider"; const whitelistedHosts = [/^.*\.reticulum\.io$/, /^.*hubs\.mozilla\.com$/, /^hubs\.local$/]; const isHostWhitelisted = hostname => !!whitelistedHosts.filter(r => r.test(hostname)).length; let resolveMediaUrl = "/api/v1/media"; @@ -21,53 +22,83 @@ const fetchContentType = async url => fetch(url, { method: "HEAD" }).then(r => r let interactableId = 0; const offset = { x: 0, y: 0, z: -1.5 }; -export const spawnNetworkedImage = (src, contentType) => { - const scene = AFRAME.scenes[0]; - const image = document.createElement("a-entity"); - image.id = "interactable-image-" + interactableId++; - image.setAttribute("networked", { template: "#interactable-image" }); - image.setAttribute("offset-relative-to", { - target: "#player-camera", - offset: offset, - selfDestruct: true +export const spawnNetworkedImage = (entity, src, contentType) => { + entity.id = "interactable-image-" + interactableId++; + entity.setAttribute("networked", { template: "#interactable-image" }); + entity.addEventListener("image-loaded", function onBodyLoaded() { + entity.removeEventListener("image-loaded", onBodyLoaded); }); - image.setAttribute("image-plus", { src, contentType }); - scene.appendChild(image); - return image; + entity.setAttribute("image-plus", { src, contentType }); }; -export const spawnNetworkedInteractable = (src, basePath) => { +export const spawnNetworkedInteractable = (entity, src, basePath) => { + entity.id = "interactable-model-" + interactableId++; + entity.setAttribute("networked", { template: "#interactable-model" }); + entity.addEventListener("model-loaded", function onBodyLoaded() { + entity.removeEventListener("model-loaded", onBodyLoaded); + setShapeAndScale(entity); + }); + entity.setAttribute("gltf-model-plus", { src: src, basePath: basePath }); +}; + +export const addMedia = async url => { const scene = AFRAME.scenes[0]; - const model = document.createElement("a-entity"); - model.id = "interactable-model-" + interactableId++; - model.setAttribute("networked", { template: "#interactable-model" }); - model.setAttribute("offset-relative-to", { - // on: "model-loaded", + + const entity = document.createElement("a-entity"); + entity.setObject3D("mesh", new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial())); + entity.classList.add("interactable"); + entity.setAttribute("body", { type: "dynamic", shape: "none", mass: "1" }); + entity.setAttribute("grabbable", ""); + entity.setAttribute("hoverable", ""); + entity.setAttribute("stretchable", { useWorldPosition: true, usePhysics: "never" }); + entity.setAttribute("sticky-object", { autoLockOnRelease: true, autoLockOnLoad: true }); + entity.setAttribute("destroy-at-extreme-distances", ""); + entity.setAttribute("offset-relative-to", { target: "#player-camera", offset: offset, selfDestruct: true }); - model.setAttribute("gltf-model-plus", { src, basePath }); - model.setAttribute("auto-box-collider", { resize: true }); - scene.appendChild(model); - return model; -}; + setShapeAndScale(entity); + scene.appendChild(entity); -export const addMedia = async url => { try { const { raw, origin, meta } = await resolveFarsparkUrl(url); console.log("resolved", url, raw, origin, meta); const contentType = (meta && meta.expected_content_type) || (await fetchContentType(raw)); if (contentType.startsWith("image/") || contentType.startsWith("video/")) { - return spawnNetworkedImage(raw, contentType); + return spawnNetworkedImage(entity, raw, contentType); } else if (contentType.startsWith("model/gltf") || url.endsWith(".gltf") || url.endsWith(".glb")) { - return spawnNetworkedInteractable(raw, THREE.LoaderUtils.extractUrlBase(origin)); + return spawnNetworkedInteractable(entity, raw, THREE.LoaderUtils.extractUrlBase(origin)); } else { throw new Error(`Unsupported content type: ${contentType}`); } } catch (e) { console.error("Error adding media", e); - spawnNetworkedImage("error"); + return spawnNetworkedImage(entity, "error"); } }; + +function setShapeAndScale(entity) { + const box = getBox(entity); + const center = new THREE.Vector3(); + const halfExtents = new THREE.Vector3(); + getCenterAndHalfExtents(entity, box, center, halfExtents); + entity.getObject3D("mesh").position.sub(center); + const scaleCoefficient = getScaleCoefficient(0.5, box); + entity.setAttribute("shape", { + shape: "box", + halfExtents: halfExtents + }); + const scale = entity.object3D.scale; + entity.setAttribute("scale", { + x: scale.x * scaleCoefficient, + y: scale.y * scaleCoefficient, + z: scale.z * scaleCoefficient + }); + if (entity.components.body && entity.components.body.body) { + // TODO: Do this in shape component update + entity.components.body.syncToPhysics(); + entity.components.body.updateCannonScale(); + } +} diff --git a/yarn.lock b/yarn.lock index 0dc13a86a..d3a49e002 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@https://github.com/mozillareality/aframe-physics-system#hubs/master": +"aframe-physics-system@https://github.com/mozillareality/aframe-physics-system#static-dynamic-mass-change": version "3.1.2" - resolved "https://github.com/mozillareality/aframe-physics-system#50f5deb1134eb0d43c0435d287eef7037818d3cc" + resolved "https://github.com/mozillareality/aframe-physics-system#1fe4e3d8d325de7a30441cdad001459789efff05" dependencies: browserify "^14.3.0" budo "^10.0.3" @@ -5521,9 +5521,9 @@ neo-async@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.0.tgz#76b1c823130cca26acfbaccc8fbaf0a2fa33b18f" -"networked-aframe@https://github.com/mozillareality/networked-aframe#mr-social-client/master": +"networked-aframe@https://github.com/mozillareality/networked-aframe#bug/aframe-initialization-race-condition": version "0.6.1" - resolved "https://github.com/mozillareality/networked-aframe#06236f794f83cfebdc4ea9f3a9e8a5804f5bdcf9" + resolved "https://github.com/mozillareality/networked-aframe#1d93d17f95e877cf1efcec460d7f8a4bf59c3784" dependencies: buffered-interpolation "^0.2.4" easyrtc "1.1.0" -- GitLab