/* global Script */ /* Custom Avatar Blendshape / Script UI Trigger Released under Creative Commons Attribution, 2018 by Menithal https://creativecommons.org/licenses/by/3.0/ */ const APP_URL = "index.html?fggf"; const APP = Script.resolvePath(APP_URL); var setInterval = Script.setInterval; var setTimeout = Script.setTimeout; // Maps the "unconventional Blendshapes" that are not used by Hifi's audio system // To custom emoticon system, Modify to your hearts content var emotionMap = { "Normal": "", "Content": "LipsUpperUp", "Calm": "LipsLowerDown", "Stare": "LipsLowerOpen", "Suprised": "ChinLowerRaise", ">_<": "ChinUpperRaise", // >_< "Λ": "JawFwd", "o": "JawChew", "▲": "JawLeft" // Triangle }; function toggleEmote(target, state, booleanStates) { return { emote: target, state: state, textStates: booleanStates }; } var rightEyeStatic = { "x": -1.3767878215276141e-7, "y": 0.7071066498756409, "z": 0.70710688829422, "w": -1.376787679419067e-7 }; var leftEyeStatic = { "x": -8.429372400087232e-8, "y": 0.7071070671081543, "z": 0.7071065902709961, "w": -2.2476429606399506e-8 }; // These are modes that should stay on or off, instead of temporary "expressions" var toggleMap = { "CrossEye": toggleEmote(function (state) { var rightEyeOverride = MyAvatar.getJointIndex("RightEyeOverride"); var leftEyeOverride = MyAvatar.getJointIndex("LeftEyeOverride"); if (state) { MyAvatar.setJointRotation(rightEyeOverride, Quat.multiply(rightEyeStatic, Quat.fromVec3Degrees({ x: 15, y: 0, z: -35 }))); MyAvatar.setJointRotation(leftEyeOverride, Quat.multiply(leftEyeStatic, Quat.fromVec3Degrees({ x: -15, y: 0, z: 35 }))); } else { MyAvatar.setJointRotation(rightEyeOverride, rightEyeStatic); MyAvatar.setJointRotation(leftEyeOverride, leftEyeStatic); // Return Joint rotations back to 0. } }, false, ["Off", "On"]), "Glazed Pupil": toggleEmote("MouthDimple_L", false, ["Off", "On"]), "Tears": toggleEmote("MouthDimple_R", false, ["Off", "On"]), "Starry Eye": toggleEmote("LipsPucker", false, ["Off", "On"]), // Or was it pucker? "Highlight": toggleEmote("EyeIn_R", false, ["Off", "On"]), "Tiny Pupils": toggleEmote("EyeIn_L", false, ["Off", "On"]), "Teeth": toggleEmote("EyeDown_L", false, ["On", "Off"]), "Fangs": toggleEmote("EyeDown_R", false, ["Off", "On"]), "Teeth Lower": toggleEmote("EyeOut_L", false, ["Off", "On"]), "Serious": toggleEmote("EyeUp_L", false, ["Off", "On"]), "Sadness": toggleEmote("EyeUp_R", false, ["Off", "On"]), "Cheerful": toggleEmote("EyeSquint_L", false, ["Off", "On"]), "Anger": toggleEmote("EyeSquint_R", false, ["Off", "On"]), "Flat Brows": toggleEmote("CheekSquint_L", false, ["Off", "On"]), "Noseline": toggleEmote("Sneer", false, ["On", "Off"]), "Blush": toggleEmote("MouthFrown_L", false, ["Off", "On"]), "Big Highlight": toggleEmote("MouthFrown_R", false, ["Off", "On"]), "Clear Toggles": toggleEmote(function (state) { var values = Object.values(toggleMap); for (var x = 0; x < values.length; x++) { if (typeof values[x].emote !== "function") { MyAvatar.setBlendshape(values[x].emote, 0); values[x].state = false; } else {} } }, false, ["", ""]) }; function getCurrentStatus() { var keys = Object.keys(toggleMap); var values = Object.values(toggleMap); var results = []; for (var i = 0; i < values.length; i++) { results[i] = [keys[i], values[i].textStates[values[i].state ? 1 : 0]]; } return results; } function turnEmotesOff(list, blendshape) { for (var x = 0; x < list.length; x++) { if (list[x] !== blendshape) { MyAvatar.setBlendshape(list[x], 0); } else { MyAvatar.setBlendshape(list[x], 1); } } } var activeApp = false; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); // TODO: Investigate a better way to automate this var tabletButton = tablet.addButton({ text: "AVATAR", sortOrder: 13 }); var onWebEventReceived; function emitWebEvent(event) { tablet.emitScriptEvent(JSON.stringify(event)); } MyAvatar.setForceFaceTrackerConnected(true); var onclick = function (e) { MyAvatar.setForceFaceTrackerConnected(true); if (activeApp) { tablet.gotoHomeScreen(); } else { tablet.gotoWebScreen(APP); } activeApp = !activeApp; } // Hifi Polyfill if (!Object.values) { Object.prototype.values = function (val) { return Object.keys(val).map(function (key) { return val[key]; }); }; } function cap(value, max) { if (value > max) { return max; } return value; } var onWebEventReceived = function (event) { var data = JSON.parse(event); if (data.type === "emote_app_load_complete") { console.log("Build App", JSON.stringify(tablet)); emitWebEvent({ type: "emote_app_build", emotes: Object.keys(emotionMap), states: getCurrentStatus(toggleMap) }); return true; } else if (data.type === "emote_app_play") { console.log("emote app play", data.message); targetEmote = emotionMap[data.message]; emoteValues = Object.values(emotionMap); if (typeof targetEmote !== "function") { turnEmotesOff(emoteValues, targetEmote); } else { targetEmote(); } } else if (data.type === "emote_app_toggle") { console.log("emote app toggle", data.message); targetEmote = toggleMap[data.message]; targetEmote.state = !targetEmote.state; if (typeof targetEmote.emote !== "function") { MyAvatar.setBlendshape(targetEmote.emote, targetEmote.state); } else { targetEmote.emote(targetEmote.state); } emitWebEvent({ type: "emote_app_state_sync", states: getCurrentStatus(toggleMap) }); } return false; }; var controllerDirections = [ "LipsUpperUp", // Content UP "ChinLowerRaise", // Suprised DOWN "ChinUpperRaise", // >_< LEFT "EyeUp_R" // Sadness RIGHT ]; var currentCoordinates = { x: 0, y: 0 }; var DISABLED_CONTROL_GROUP = "com.menithal.avatar.emotions.disabled" var ENABLED_CONTROL_GROUP = "com.menithal.avatar.emotions.active" var disabledControls = Controller.newMapping(DISABLED_CONTROL_GROUP); var enabledControls = Controller.newMapping(ENABLED_CONTROL_GROUP); var Standard = Controller.Standard; var Vive = Controller.Hardware.Vive; var OculusTouch = Controller.Hardware.OculusTouch; var GamePad = Controller.Hardware.GamePad; function emotionControl(toggle) { console.log("Emote ", toggle); if (toggle) { Controller.disableMapping(DISABLED_CONTROL_GROUP); Controller.enableMapping(ENABLED_CONTROL_GROUP); } else { Controller.enableMapping(DISABLED_CONTROL_GROUP); Controller.disableMapping(ENABLED_CONTROL_GROUP); currentCoordinates = { x: 0, y: 0 }; // Clear Above blendShapes(currentCoordinates); var values = Object.values(toggleMap); for (var x = 0; x < toggleMap.length; x++) { for (var y = 0; y < controllerDirections; y++) { if (controllerDirections[y] === toggleMap[x].emote) { values[y].state = false; } } } } return true; } var MIN_TRESHOLD = 0.05; function blendShapes(coordinates) { console.log("coordinates ", coordinates); MyAvatar.setBlendshape(controllerDirections[2], cap(Math.abs(coordinates.x > MIN_TRESHOLD ? 0 : coordinates.x) * 2.5, 1.0)); MyAvatar.setBlendshape(controllerDirections[3], cap(Math.abs(coordinates.x > MIN_TRESHOLD ? coordinates.x : 0) * 2.5, 1.0)); MyAvatar.setBlendshape(controllerDirections[0], cap(Math.abs(coordinates.y > MIN_TRESHOLD ? 0 : coordinates.y) * 2.5, 1.0)); MyAvatar.setBlendshape(controllerDirections[1], cap(Math.abs(coordinates.y > MIN_TRESHOLD ? coordinates.y : 0) * 2.5, 1.0)); } function updateX(val) { if (Math.abs(val) > 0) { currentCoordinates.x = val; } blendShapes(currentCoordinates); } function updateY(val) { if (Math.abs(val) > 0) { currentCoordinates.y = val; } blendShapes(currentCoordinates); } function clear(val) { currentCoordinates = { x: 0, y: 0 }; } function empty(val) {} if (MyAvatar.getDominantHand() === "right") { disabledControls.from(Standard.RightSecondaryThumb).to(emotionControl); enabledControls.from(Standard.RightSecondaryThumb).to(emotionControl); enabledControls.from(Standard.LX).to(updateX); enabledControls.from(Standard.LY).to(updateY); if (GamePad) { disabledControls.from(GamePad.RT).to(emotionControl); enabledControls.from(GamePad.RT).to(emotionControl); enabledControls.from(GamePad.LX).to(updateX); enabledControls.from(GamePad.LY).to(updateY); } if (Vive) { enabledControls.from(Vive.LX).to(updateX); enabledControls.from(Vive.LY).to(updateY); enabledControls.from(Vive.RightApplicationMenu).to(emotionControl) } if (OculusTouch) { enabledControls.from(OculusTouch.LX).to(updateX); enabledControls.from(OculusTouch.LY).to(updateX); enabledControls.from(OculusTouch.B).to(emotionControl) } enabledControls.from(Standard.LS).to(clear); enabledControls.from(Standard.LeftPrimaryThumb).to(empty); } else { disabledControls.from(Standard.LeftSecondaryThumb).to(emotionControl); enabledControls.from(Standard.LeftSecondaryThumb).to(emotionControl); enabledControls.from(Standard.RY).to(updateX); enabledControls.from(Standard.RX).to(updateY); if (GamePad) { disabledControls.from(GamePad.LT).to(emotionControl); disabledControls.from(GamePad.LT).to(emotionControl); enabledControls.from(GamePad.RX).to(updateX); enabledControls.from(GamePad.RY).to(updateY); } if (Vive) { enabledControls.from(Vive.LeftSecondaryThumb).to(emotionControl) enabledControls.from(Vive.LX).to(updateX); enabledControls.from(Vive.LY).to(updateY); enabledControls.from(Vive.LeftApplicationMenu).to(emotionControl) } if (OculusTouch) { enabledControls.from(OculusTouch.RX).to(updateX); enabledControls.from(OculusTouch.RY).to(updateX); enabledControls.from(OculusTouch.Y).to(emotionControl) } enabledControls.from(Standard.RS).to(clear); enabledControls.from(Standard.RightPrimaryThumb).to(empty); } Controller.enableMapping(DISABLED_CONTROL_GROUP); tabletButton.clicked.connect(onclick); tablet.webEventReceived.connect(onWebEventReceived); Script.scriptEnding.connect(function () { if (activeApp) { tablet.webEventReceived.disconnect(onWebEventReceived); } activeApp = false; tabletButton.clicked.disconnect(onclick); tablet.removeButton(tabletButton); });