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 18 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
53 changes: 53 additions & 0 deletions src/components/interactables/hoverable-visuals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Listens for hoverable state changes and applies a visual effect to an entity
* @namespace interactables
* @component hoverable-visuals
*/
AFRAME.registerComponent("hoverable-visuals", {
schema: {
cursorController: { type: "selector" }
},
init: function() {
this.interactorOneTransform = [];
this.interactorTwoTransform = [];
},
remove() {
this.interactorOneTransform = null;
this.interactorTwoTransform = null;
},
tick(time) {
const uniforms = this.el.components["media-loader"].shaderUniforms;

if (!uniforms) 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(this.interactorOneTransform);
}
if (interactorTwo) {
interactorTwo.matrixWorld.toArray(this.interactorTwoTransform);
}

for (const uniform of uniforms) {
uniform.hubs_HighlightInteractorOne.value = !!interactorOne;
uniform.hubs_InteractorOneTransform.value = this.interactorOneTransform;
uniform.hubs_HighlightInteractorTwo.value = !!interactorTwo;
uniform.hubs_InteractorTwoTransform.value = this.interactorTwoTransform;
uniform.hubs_Time.value = time;
}
}
});
44 changes: 44 additions & 0 deletions src/components/media-highlight-frag.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
if (hubs_HighlightInteractorOne || hubs_HighlightInteractorTwo) {
mat4 it;
vec3 ip;
float dist1, dist2;

if (hubs_HighlightInteractorOne) {
it = hubs_InteractorOneTransform;
ip = vec3(it[3][0], it[3][1], it[3][2]);
dist1 = distance(hubs_WorldPosition, ip);
}

if (hubs_HighlightInteractorTwo) {
it = hubs_InteractorTwoTransform;
ip = vec3(it[3][0], it[3][1], it[3][2]);
dist2 = distance(hubs_WorldPosition, ip);
}

float ratio = 0.0;
float pulse = sin(hubs_Time / 1000.0) + 1.0;
float spacing = 0.5;
float line = spacing * pulse - spacing / 2.0;
float lineWidth= 0.01;
float mody = mod(hubs_WorldPosition.y, spacing);

if (-lineWidth + line < mody && mody < lineWidth + line) {
// Highlight with an animated line effect
ratio = 0.5;
} else {
// Highlight with a gradient falling off with distance.
if (hubs_HighlightInteractorOne) {
ratio = -min(1.0, pow(dist1 * (9.0 + 3.0 * pulse), 3.0)) + 1.0;
}
if (hubs_HighlightInteractorTwo) {
ratio += -min(1.0, pow(dist2 * (9.0 + 3.0 * 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);
}
77 changes: 73 additions & 4 deletions src/components/media-loader.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { getBox, getScaleCoefficient } from "../utils/auto-box-collider";
import { guessContentType, proxiedUrlFor, resolveUrl } from "../utils/media-utils";
import { addAnimationComponents } from "../utils/animation";
import mediaHighlightFrag from "./media-highlight-frag.glsl";

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 +20,67 @@ const fetchMaxContentIndex = url => {
return fetch(url).then(r => parseInt(r.headers.get("x-max-content-index")));
};

function injectCustomShaderChunks(obj) {
const vertexRegex = /\bbegin_vertex\b/;
const fragRegex = /\bgl_FragColor\b/;

const materialsSeen = new Set();
const shaderUniforms = [];

obj.traverse(object => {
if (!object.material || !["MeshStandardMaterial", "MeshBasicMaterial"].includes(object.material.type)) {
return;
}
object.material = object.material.clone();
object.material.onBeforeCompile = shader => {
if (!vertexRegex.test(shader.vertexShader)) return;

shader.uniforms.hubs_InteractorOneTransform = { value: [] };
shader.uniforms.hubs_InteractorTwoTransform = { value: [] };
shader.uniforms.hubs_InteractorTwoPos = { value: [] };
shader.uniforms.hubs_HighlightInteractorOne = { value: false };
shader.uniforms.hubs_HighlightInteractorTwo = { value: false };
shader.uniforms.hubs_Time = { value: 0 };

const vchunk = `
if (hubs_HighlightInteractorOne || hubs_HighlightInteractorTwo) {
vec4 wt = modelMatrix * vec4(transformed, 1);

// Used in the fragment shader below.
hubs_WorldPosition = wt.xyz;
}
`;

const vlines = shader.vertexShader.split("\n");
const vindex = vlines.findIndex(line => vertexRegex.test(line));
vlines.splice(vindex + 1, 0, vchunk);
vlines.unshift("varying vec3 hubs_WorldPosition;");
vlines.unshift("uniform bool hubs_HighlightInteractorOne;");
vlines.unshift("uniform bool hubs_HighlightInteractorTwo;");
shader.vertexShader = vlines.join("\n");

const flines = shader.fragmentShader.split("\n");
const findex = flines.findIndex(line => fragRegex.test(line));
flines.splice(findex + 1, 0, mediaHighlightFrag);
flines.unshift("varying vec3 hubs_WorldPosition;");
flines.unshift("uniform bool hubs_HighlightInteractorOne;");
flines.unshift("uniform mat4 hubs_InteractorOneTransform;");
flines.unshift("uniform bool hubs_HighlightInteractorTwo;");
flines.unshift("uniform mat4 hubs_InteractorTwoTransform;");
flines.unshift("uniform float hubs_Time;");
shader.fragmentShader = flines.join("\n");

if (!materialsSeen.has(object.material.uuid)) {
shaderUniforms.push(shader.uniforms);
materialsSeen.add(object.material.uuid);
}
};
object.material.needsUpdate = true;
});

return shaderUniforms;
}

AFRAME.registerComponent("media-loader", {
schema: {
src: { type: "string" },
Expand All @@ -30,6 +93,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 +164,11 @@ AFRAME.registerComponent("media-loader", {
delete this.showLoaderTimeout;
},

onMediaLoaded() {
this.clearLoadingTimeout();
this.shaderUniforms = injectCustomShaderChunks(this.el.object3D);
},

async update(oldData) {
try {
const { src } = this.data;
Expand Down Expand Up @@ -135,13 +204,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 +221,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 @@ -165,7 +234,7 @@ AFRAME.registerComponent("media-loader", {
this.el.addEventListener(
"model-loaded",
() => {
this.clearLoadingTimeout();
this.onMediaLoaded();
this.hasBakedShapes = !!(this.el.body && this.el.body.shapes.length > (this.shapeAdded ? 1 : 0));
this.setShapeAndScale(this.data.resize);
addAnimationComponents(this.el);
Expand Down
1 change: 1 addition & 0 deletions src/hub.html
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,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
1 change: 1 addition & 0 deletions src/hub.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import "./components/virtual-gamepad-controls";
import "./components/ik-controller";
import "./components/hand-controls2";
import "./components/character-controller";
import "./components/interactables/hoverable-visuals";
import "./components/haptic-feedback";
import "./components/networked-video-player";
import "./components/offset-relative-to";
Expand Down
5 changes: 5 additions & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ module.exports = (env, argv) => ({
devServer: {
https: createHTTPSConfig(),
host: "0.0.0.0",
public: "hubs.local:8080",
useLocalIp: true,
allowedHosts: ["hubs.local"],
before: function(app) {
Expand Down Expand Up @@ -153,6 +154,10 @@ module.exports = (env, argv) => ({
context: path.join(__dirname, "src")
}
}
},
{
test: /\.(glsl)$/,
use: { loader: "raw-loader" }
}
]
},
Expand Down