Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a shader effect when hovering over interactables #659

Merged
merged 30 commits into from
Nov 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2e12536
Hover visuals WIP
brianpeiris Oct 23, 2018
6819453
Inject shader into gltf-model-plus. Modify uniforms in hoverable-visuals
brianpeiris Oct 25, 2018
926c40b
Fix shader injection for cloned media
brianpeiris Oct 25, 2018
ced8ac3
Guard against null uniforms
brianpeiris Oct 25, 2018
7a54d0f
Add shader test harness
brianpeiris Oct 26, 2018
26b1815
debug tweaks
brianpeiris Oct 26, 2018
b9a932a
More complete shader experiment
brianpeiris Oct 27, 2018
2c4b6b3
grid effect and hand orientation
brianpeiris Oct 30, 2018
c31a9d4
Add pulsing effect
brianpeiris Oct 30, 2018
1a44c05
Merge branch 'master' of github.com:mozilla/hubs into feature/hover-v…
brianpeiris Oct 30, 2018
6a64c02
Apply hover effects to all media
brianpeiris Oct 31, 2018
2f44560
rename uniforms
brianpeiris Oct 31, 2018
9d81bfd
Don't trigger effect on disabled cursor
brianpeiris Oct 31, 2018
2906ac3
remove shader test harness
brianpeiris Oct 31, 2018
b3a6e4d
Revert unnecessary changes to gltf-model-plus
brianpeiris Oct 31, 2018
887ca4c
Move fragment shader to its own file
brianpeiris Oct 31, 2018
4c52e2d
Move fragment shader to its own file
brianpeiris Oct 31, 2018
16df921
Merge branch 'feature/hover-visuals' of github.com:mozilla/hubs into …
brianpeiris Oct 31, 2018
b52ecf4
Move shader injection to media utils. Attempt to add hover effect to …
brianpeiris Oct 31, 2018
52a32ae
Fix avatar hover effect. Use a sweeping gradient effect
brianpeiris Nov 2, 2018
ae18301
Merge branch 'master' of github.com:mozilla/hubs into feature/hover-v…
brianpeiris Nov 2, 2018
60d4879
Improve doc comment
brianpeiris Nov 2, 2018
c40d00a
Fix effect shader for MobileStandardMaterial
brianpeiris Nov 2, 2018
88ada83
Optimize shader uniforms
brianpeiris Nov 2, 2018
af37c97
Restore clearLoadingTimeout call
brianpeiris Nov 2, 2018
8de3388
minor shader optimization for avatar hands
brianpeiris Nov 2, 2018
850be3d
Minor shader refactor
brianpeiris Nov 2, 2018
8061d0a
Shader refactoring
brianpeiris Nov 2, 2018
b8dd5a0
Fix hand effect regression
brianpeiris Nov 2, 2018
5947ddc
Add hoverable-visuals to all super-spawners
brianpeiris Nov 2, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"htmlhint": "^0.9.13",
"node-sass": "^4.9.3",
"prettier": "^1.7.0",
"raw-loader": "^0.5.1",
"rimraf": "^2.6.2",
"sass-loader": "^6.0.7",
"selfsigned": "^1.10.2",
Expand Down
40 changes: 40 additions & 0 deletions src/components/hover-visuals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const interactorTransform = [];

/**
* Applies effects to a hoverer based on hover state.
* @namespace interactables
* @component hover-visuals
*/
AFRAME.registerComponent("hover-visuals", {
schema: {
hand: { type: "string" },
controller: { type: "selector" }
},
init() {
// uniforms are set from the component responsible for loading the mesh.
this.uniforms = null;
},
remove() {
this.uniforms = null;
},
tick() {
if (!this.uniforms || !this.uniforms.size) return;

this.el.object3D.matrixWorld.toArray(interactorTransform);
const hovering = this.data.controller.components["super-hands"].state.has("hover-start");

for (const uniform of this.uniforms.values()) {
if (this.data.hand === "left") {
uniform.hubs_HighlightInteractorOne.value = hovering;
uniform.hubs_InteractorOnePos.value[0] = interactorTransform[12];
uniform.hubs_InteractorOnePos.value[1] = interactorTransform[13];
uniform.hubs_InteractorOnePos.value[2] = interactorTransform[14];
} else {
uniform.hubs_HighlightInteractorTwo.value = hovering;
uniform.hubs_InteractorTwoPos.value[0] = interactorTransform[12];
uniform.hubs_InteractorTwoPos.value[1] = interactorTransform[13];
uniform.hubs_InteractorTwoPos.value[2] = interactorTransform[14];
}
}
}
});
76 changes: 76 additions & 0 deletions src/components/hoverable-visuals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const interactorOneTransform = [];
const interactorTwoTransform = [];

/**
* Applies effects to a hoverable based on hover state.
* @namespace interactables
* @component hoverable-visuals
*/
AFRAME.registerComponent("hoverable-visuals", {
schema: {
cursorController: { type: "selector" },
enableSweepingEffect: { type: "boolean", default: true }
},
init() {
// uniforms and boundingSphere are set from the component responsible for loading the mesh.
this.uniforms = null;
this.boundingSphere = new THREE.Sphere();

this.sweepParams = [0, 0];
},
remove() {
this.uniforms = null;
this.boundingBox = null;
},
tick(time) {
if (!this.uniforms || !this.uniforms.size) return;

const { hoverers } = this.el.components["hoverable"];

let interactorOne, interactorTwo;
for (const hoverer of hoverers) {
if (hoverer.id === "player-left-controller") {
interactorOne = hoverer.object3D;
} else if (hoverer.id === "cursor") {
if (this.data.cursorController.components["cursor-controller"].enabled) {
interactorTwo = hoverer.object3D;
}
} else {
interactorTwo = hoverer.object3D;
}
}

if (interactorOne) {
interactorOne.matrixWorld.toArray(interactorOneTransform);
}
if (interactorTwo) {
interactorTwo.matrixWorld.toArray(interactorTwoTransform);
}

if (interactorOne || interactorTwo) {
const worldY = this.el.object3D.matrixWorld.elements[13];
const scaledRadius = this.el.object3D.scale.y * this.boundingSphere.radius;
this.sweepParams[0] = worldY - scaledRadius;
this.sweepParams[1] = worldY + scaledRadius;
}

for (const uniform of this.uniforms.values()) {
uniform.hubs_EnableSweepingEffect.value = this.data.enableSweepingEffect;
uniform.hubs_SweepParams.value = this.sweepParams;

uniform.hubs_HighlightInteractorOne.value = !!interactorOne;
uniform.hubs_InteractorOnePos.value[0] = interactorOneTransform[12];
uniform.hubs_InteractorOnePos.value[1] = interactorOneTransform[13];
uniform.hubs_InteractorOnePos.value[2] = interactorOneTransform[14];

uniform.hubs_HighlightInteractorTwo.value = !!interactorTwo;
uniform.hubs_InteractorTwoPos.value[0] = interactorTwoTransform[12];
uniform.hubs_InteractorTwoPos.value[1] = interactorTwoTransform[13];
uniform.hubs_InteractorTwoPos.value[2] = interactorTwoTransform[14];

if (interactorOne || interactorTwo) {
uniform.hubs_Time.value = time;
}
}
}
});
27 changes: 23 additions & 4 deletions src/components/media-loader.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { getBox, getScaleCoefficient } from "../utils/auto-box-collider";
import { guessContentType, proxiedUrlFor, resolveUrl } from "../utils/media-utils";
import { guessContentType, proxiedUrlFor, resolveUrl, injectCustomShaderChunks } from "../utils/media-utils";
import { addAnimationComponents } from "../utils/animation";

import "three/examples/js/loaders/GLTFLoader";
import loadingObjectSrc from "../assets/LoadingObject_Atom.glb";

const gltfLoader = new THREE.GLTFLoader();
let loadingObject;
gltfLoader.load(loadingObjectSrc, gltf => {
Expand All @@ -18,6 +19,8 @@ const fetchMaxContentIndex = url => {
return fetch(url).then(r => parseInt(r.headers.get("x-max-content-index")));
};

const boundingBox = new THREE.Box3();

AFRAME.registerComponent("media-loader", {
schema: {
src: { type: "string" },
Expand All @@ -30,6 +33,7 @@ AFRAME.registerComponent("media-loader", {
this.onError = this.onError.bind(this);
this.showLoader = this.showLoader.bind(this);
this.clearLoadingTimeout = this.clearLoadingTimeout.bind(this);
this.onMediaLoaded = this.onMediaLoaded.bind(this);
this.shapeAdded = false;
this.hasBakedShapes = false;
},
Expand Down Expand Up @@ -100,6 +104,20 @@ AFRAME.registerComponent("media-loader", {
delete this.showLoaderTimeout;
},

setupHoverableVisuals() {
const hoverableVisuals = this.el.components["hoverable-visuals"];
if (hoverableVisuals) {
hoverableVisuals.uniforms = injectCustomShaderChunks(this.el.object3D);
boundingBox.setFromObject(this.el.object3DMap.mesh);
boundingBox.getBoundingSphere(hoverableVisuals.boundingSphere);
}
},

onMediaLoaded() {
this.clearLoadingTimeout();
this.setupHoverableVisuals();
},

async update(oldData) {
try {
const { src } = this.data;
Expand Down Expand Up @@ -135,13 +153,13 @@ AFRAME.registerComponent("media-loader", {
if (contentType.startsWith("video/") || contentType.startsWith("audio/")) {
this.el.removeAttribute("gltf-model-plus");
this.el.removeAttribute("media-image");
this.el.addEventListener("video-loaded", this.clearLoadingTimeout, { once: true });
this.el.addEventListener("video-loaded", this.onMediaLoaded, { once: true });
this.el.setAttribute("media-video", { src: accessibleUrl });
this.el.setAttribute("position-at-box-shape-border", { dirs: ["forward", "back"] });
} else if (contentType.startsWith("image/")) {
this.el.removeAttribute("gltf-model-plus");
this.el.removeAttribute("media-video");
this.el.addEventListener("image-loaded", this.clearLoadingTimeout, { once: true });
this.el.addEventListener("image-loaded", this.onMediaLoaded, { once: true });
this.el.removeAttribute("media-pager");
this.el.setAttribute("media-image", { src: accessibleUrl, contentType });
this.el.setAttribute("position-at-box-shape-border", { dirs: ["forward", "back"] });
Expand All @@ -152,7 +170,7 @@ AFRAME.registerComponent("media-loader", {
// 1. we pass the canonical URL to the pager so it can easily make subresource URLs
// 2. we don't remove the media-image component -- media-pager uses that internally
this.el.setAttribute("media-pager", { src: canonicalUrl });
this.el.addEventListener("preview-loaded", this.clearLoadingTimeout, { once: true });
this.el.addEventListener("preview-loaded", this.onMediaLoaded, { once: true });
this.el.setAttribute("position-at-box-shape-border", { dirs: ["forward", "back"] });
} else if (
contentType.includes("application/octet-stream") ||
Expand All @@ -168,6 +186,7 @@ AFRAME.registerComponent("media-loader", {
this.clearLoadingTimeout();
this.hasBakedShapes = !!(this.el.body && this.el.body.shapes.length > (this.shapeAdded ? 1 : 0));
this.setShapeAndScale(this.data.resize);
this.setupHoverableVisuals();
addAnimationComponents(this.el);
},
{ once: true }
Expand Down
7 changes: 7 additions & 0 deletions src/components/player-info.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { injectCustomShaderChunks } from "../utils/media-utils";

/**
* Sets player info state, including avatar choice and display name.
* @namespace avatar
Expand Down Expand Up @@ -32,5 +34,10 @@ AFRAME.registerComponent("player-info", {
if (this.data.avatarSrc && modelEl) {
modelEl.setAttribute("gltf-model-plus", "src", this.data.avatarSrc);
}

const uniforms = injectCustomShaderChunks(this.el.object3D);
this.el.querySelectorAll("[hover-visuals]").forEach(el => {
el.components["hover-visuals"].uniforms = uniforms;
});
}
});
2 changes: 2 additions & 0 deletions src/components/super-spawner.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ AFRAME.registerComponent("super-spawner", {
this.onSpawnEvent = this.onSpawnEvent.bind(this);

this.sceneEl = document.querySelector("a-scene");

this.el.setAttribute("hoverable-visuals", { cursorController: "#cursor-controller", enableSweepingEffect: false });
},

play() {
Expand Down
5 changes: 3 additions & 2 deletions src/hub.html
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
grabbable
stretchable="useWorldPosition: true; usePhysics: never"
hoverable
hoverable-visuals="cursorController: #cursor-controller"
auto-scale-cannon-physics-body
sticky-object="autoLockOnRelease: true; autoLockOnLoad: true;"
position-at-box-shape-border="target:.delete-button"
Expand Down Expand Up @@ -436,11 +437,11 @@
</template>

<template data-name="LeftHand">
<a-entity bone-visibility></a-entity>
<a-entity bone-visibility hover-visuals="hand: left; controller: #player-left-controller"></a-entity>
</template>

<template data-name="RightHand">
<a-entity bone-visibility></a-entity>
<a-entity bone-visibility hover-visuals="hand: right; controller: #player-right-controller"></a-entity>
</template>

</a-entity>
Expand Down
2 changes: 2 additions & 0 deletions src/hub.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import "./components/virtual-gamepad-controls";
import "./components/ik-controller";
import "./components/hand-controls2";
import "./components/character-controller";
import "./components/hoverable-visuals";
import "./components/hover-visuals";
import "./components/haptic-feedback";
import "./components/networked-video-player";
import "./components/offset-relative-to";
Expand Down
5 changes: 5 additions & 0 deletions src/materials/MobileStandardMaterial.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ void main() {
`;

export default class MobileStandardMaterial extends THREE.ShaderMaterial {
type = "MobileStandardMaterial";
isMobileStandardMaterial = true;
static fromStandardMaterial(material) {
const parameters = {
vertexShader: VERTEX_SHADER,
Expand Down Expand Up @@ -107,4 +109,7 @@ export default class MobileStandardMaterial extends THREE.ShaderMaterial {

return mobileMaterial;
}
clone() {
return MobileStandardMaterial.fromStandardMaterial(this);
}
}
33 changes: 33 additions & 0 deletions src/utils/media-highlight-frag.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
if (hubs_HighlightInteractorOne || hubs_HighlightInteractorTwo) {
float ratio = 0.0;

if (hubs_EnableSweepingEffect) {
float size = hubs_SweepParams.t - hubs_SweepParams.s;
float line = mod(hubs_Time / 3000.0 * size, size * 2.0) + hubs_SweepParams.s - size / 2.0;

if (hubs_WorldPosition.y < line) {
// Highlight with a sweeping gradient.
ratio = max(0.0, 1.0 - (line - hubs_WorldPosition.y) / size * 3.0);
}
}

// Highlight with a gradient falling off with distance.
float pulse = 9.0 + 3.0 * (sin(hubs_Time / 1000.0) + 1.0);

if (hubs_HighlightInteractorOne) {
float dist1 = distance(hubs_WorldPosition, hubs_InteractorOnePos);
ratio += -min(1.0, pow(dist1 * pulse, 3.0)) + 1.0;
}

if (hubs_HighlightInteractorTwo) {
float dist2 = distance(hubs_WorldPosition, hubs_InteractorTwoPos);
ratio += -min(1.0, pow(dist2 * pulse, 3.0)) + 1.0;
}

ratio = min(1.0, ratio);

// Gamma corrected highlight color
vec3 highlightColor = vec3(0.184, 0.499, 0.933);

gl_FragColor.rgb = (gl_FragColor.rgb * (1.0 - ratio)) + (highlightColor * ratio);
}
Loading