diff --git a/scripts/developer/automaticLookAt.js b/scripts/developer/automaticLookAt.js new file mode 100644 index 0000000000..542d0df0d8 --- /dev/null +++ b/scripts/developer/automaticLookAt.js @@ -0,0 +1,1344 @@ +// +// automaticLookAt.js +// This script controls the avatar's look-at-target for the head and eyes, according to other avatar's actions +// It tries to simulate human interaction during group conversations +// +// Created by Luis Cuenca on 11/11/19 +// Copyright 2019 High Fidelity, Inc. +// +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + + ////////////////////////////////////////////// + // debugger.js /////// + ////////////////////////////////////////////// + + var TEXT_BOX_WIDTH = 350; + var TEXT_BOX_MIN_HEIGHT = 40; + var TEXT_BOX_TOP_MARGIN = 100; + var TEXT_CAPTION_HEIGHT = 30; + var TEXT_CAPTION_COLOR = { red: 0, green: 0, blue: 0 }; + var TEXT_CAPTION_SIZE = 18; + var TEXT_CAPTION_MARGIN = 6; + var CHECKBOX_MARK_MARGIN = 3; + + var DEGREE_TO_RADIAN = 0.0174533; + var ENGAGED_AVATARS_DEBUG_COLOR = { red: 0, green: 255, blue: 255 }; + var ENGAGED_AVATARS_DEBUG_ALPHA = 0.3; + var FOCUS_AVATAR_DEBUG_COLOR = { red: 255, green: 0, blue: 0 }; + var FOCUS_AVATAR_DEBUG_ALPHA = 1.0; + var TALKER_AVATAR_DEBUG_COLOR = { red: 0, green: 0, blue: 0 }; + var TALKER_AVATAR_DEBUG_ALPHA = 0.8; + var DEFAULT_OUTLINE_COLOR = { red: 155, green: 155, blue: 255 }; + var DEFAULT_OUTLINE_WIDTH = 2; + + var LookAtDebugger = function() { + var self = this; + var IMAGE_DIMENSIONS = {x: 0.2, y: 0.2, z:0.2}; + var TARGET_ICON_PATH = "https://hifi-content.s3.amazonaws.com/luis/test_scripts/LookAtApp/eyeFocus.png"; + var INFINITY_ICON_PATH = "https://hifi-content.s3.amazonaws.com/luis/test_scripts/LookAtApp/noFocus.png"; + this.items = {}; + this.active = false; + + // UI elements + this.textBox; + this.activeCheckBox; + this.activeCheckBoxMark; + this.activeCaption; + this.textBoxHeight = 0.0; + this.logs = []; + + this.getStyleProps = function(color, alpha) { + return { + fillUnoccludedColor: color, + fillUnoccludedAlpha: alpha, + fillOccludedColor: color, + fillOccludedAlpha: alpha, + outlineUnoccludedColor: DEFAULT_OUTLINE_COLOR, + outlineUnoccludedAlpha: alpha, + outlineOccludedColor: DEFAULT_OUTLINE_COLOR, + outlineOccludedAlpha: alpha, + outlineWidth: DEFAULT_OUTLINE_WIDTH, + isOutlineSmooth: false + } + } + this.Styles = { + "engaged" : { + "style": self.getStyleProps(ENGAGED_AVATARS_DEBUG_COLOR, ENGAGED_AVATARS_DEBUG_ALPHA), + "name" : "engagedSel" + }, + "focus" : { + "style": self.getStyleProps(FOCUS_AVATAR_DEBUG_COLOR, FOCUS_AVATAR_DEBUG_ALPHA), + "name" : "focusSel" + }, + "talker" : { + "style": self.getStyleProps(TALKER_AVATAR_DEBUG_COLOR, TALKER_AVATAR_DEBUG_ALPHA), + "name" : "talkerSel" + } + }; + + this.eyeTarget; + this.eyesTargetProps = { + name: "Eyes-Target-Image", + position: { x: 0.0, y: 0.0, z: 0.0 }, + color: {red: 255, green: 0, blue: 255}, + url: TARGET_ICON_PATH, + dimensions: IMAGE_DIMENSIONS, + alpha: 1.0, + visible: false, + emissive: true, + ignoreRayIntersection: true, + drawInFront: true, + grabbable: false, + isFacingAvatar: true + }; + this.headTarget; + this.headTargetProps = { + name: "Head-Target-Image", + position: { x: 0.0, y: 0.0, z: 0.0 }, + color: {red: 0, green: 255, blue: 255}, + url: TARGET_ICON_PATH, + dimensions: IMAGE_DIMENSIONS, + alpha: 1.0, + visible: false, + emissive: true, + ignoreRayIntersection: true, + drawInFront: true, + grabbable: false, + isFacingAvatar: true + }; + + + this.textBoxProps = { + x: Window.innerWidth - TEXT_BOX_WIDTH, + y: TEXT_BOX_TOP_MARGIN, + width: TEXT_BOX_WIDTH, + height: TEXT_BOX_MIN_HEIGHT, + alpha: 0.7, + color: { red: 255, green: 255, blue: 255 } + }; + + this.activeCheckBoxProps = { + x: TEXT_CAPTION_MARGIN + self.textBoxProps.x, + y: 2 * TEXT_CAPTION_MARGIN + self.textBoxProps.y, + width: TEXT_CAPTION_SIZE, + height: TEXT_CAPTION_SIZE, + alpha: 0.0, + borderWidth: 2, + borderColor: TEXT_CAPTION_COLOR + }; + + this.activeCheckBoxMarkProps = { + x: self.activeCheckBoxProps.x + CHECKBOX_MARK_MARGIN, + y: self.activeCheckBoxProps.y + CHECKBOX_MARK_MARGIN, + width: TEXT_CAPTION_SIZE - 2.0 * CHECKBOX_MARK_MARGIN, + height: TEXT_CAPTION_SIZE - 2.0 * CHECKBOX_MARK_MARGIN, + visible: false, + alpha: 1.0, + color: TEXT_CAPTION_COLOR + } + + this.captionProps = { + x: 2 * TEXT_CAPTION_SIZE + self.textBoxProps.x, + y: TEXT_CAPTION_MARGIN + self.textBoxProps.y, + width: self.textBoxProps.width, + height: 30, + alpha: 1.0, + backgroundAlpha: 0.0, + visible: true, + text: "Debug Auto Look At", + font: { + size: TEXT_CAPTION_SIZE + }, + color: TEXT_CAPTION_COLOR, + topMargin: 0.5 * TEXT_CAPTION_MARGIN + }; + + this.log = function(txt) { + if (self.active) { + self.logs.push(Overlays.addOverlay("text", self.captionProps)); + var y = self.textBoxProps.y + self.captionProps.height * (self.logs.length); + Overlays.editOverlay(self.logs[self.logs.length - 1], {y: y, text: txt}); + var height = (TEXT_CAPTION_SIZE + 2.0 * TEXT_CAPTION_MARGIN) * (self.logs.length + 2); + if (this.textBoxHeight !== height) { + this.textBoxHeight = height + Overlays.editOverlay(self.textBox, {height: height}); + } + } + } + + this.clearLog = function() { + for (var n = 0; n < self.logs.length; n++) { + Overlays.deleteOverlay(self.logs[n]); + } + Overlays.editOverlay(self.textBox, self.textBoxProps); + self.logs = []; + self.log("____________________________"); + } + + this.setActive = function(isActive) { + self.active = isActive; + if (!self.active) { + self.turnOff(); + } else { + self.turnOn(); + } + }; + + + this.onClick = function(event) { + if (event.x > self.activeCheckBoxProps.x && event.x < (self.activeCheckBoxProps.x + self.activeCheckBoxProps.width) && + event.y > self.activeCheckBoxProps.y && event.y < (self.activeCheckBoxProps.y + self.activeCheckBoxProps.height)) { + self.setActive(!self.active); + Overlays.editOverlay(self.activeCheckBoxMark, {visible: self.active}); + } + } + + this.turnOn = function() { + for (var key in self.Styles) { + var selStyle = self.Styles[key]; + Selection.enableListHighlight(selStyle.name, selStyle.style); + } + if (!self.eyeTarget) { + self.eyeTarget = Overlays.addOverlay("image3d", self.eyesTargetProps); + } + if (!self.headTarget) { + self.headTarget = Overlays.addOverlay("image3d", self.headTargetProps); + } + } + + this.init = function() { + if (!self.textBox) { + self.textBox = Overlays.addOverlay("rectangle", self.textBoxProps); + } + if (!self.activeCheckBox) { + self.activeCheckBox = Overlays.addOverlay("rectangle", self.activeCheckBoxProps); + } + if (!self.activeCheckBoxMark) { + self.activeCheckBoxMark = Overlays.addOverlay("rectangle", self.activeCheckBoxMarkProps); + } + if (!self.activeCaption) { + self.activeCaption = Overlays.addOverlay("text", self.captionProps); + } + }; + + this.highLightAvatars = function(engagedIDs, focusID, talkerID) { + if (self.active) { + self.clearSelection(); + engagedIDs = !engagedIDs ? [] : engagedIDs; + var focusIDs = !focusID ? [] : [focusID]; + var talkerIDs = !talkerID ? [] : [talkerID]; + self.highLightIDs(self.Styles.engaged.name, engagedIDs); + self.highLightIDs(self.Styles.focus.name, focusIDs); + self.highLightIDs(self.Styles.talker.name, talkerIDs); + return true; + } else { + return false; + } + }; + + this.getTargetProps = function(target) { + var distance = Vec3.length(Vec3.subtract(target, Camera.getPosition())); + RETARGET_MAX_DISTANCE = 100.0; + DIMENSION_SCALE = 0.05; + var isInfinite = false; + if (distance > RETARGET_MAX_DISTANCE) { + isInfinite = true; + var eyesToTarget = Vec3.multiply(RETARGET_MAX_DISTANCE, Vec3.normalize(Vec3.subtract(target, MyAvatar.getDefaultEyePosition()))); + var newTarget = Vec3.sum(MyAvatar.getDefaultEyePosition(), eyesToTarget); + var cameraToTarget = Vec3.normalize(Vec3.subtract(newTarget, Camera.getPosition())); + target = Vec3.sum(Camera.getPosition(), cameraToTarget); + distance = Vec3.length(cameraToTarget); + } + // Scale the target to appear always with the same size on screen + var fov = DEGREE_TO_RADIAN * Camera.frustum.fieldOfView; + var scale = (Camera.frustum.aspectRatio < 1.0 ? Camera.frustum.aspectRatio : 1.0) * DIMENSION_SCALE; + var dimensionRatio = scale * (distance / (0.5 * Math.tan(0.5 * fov))); + var dimensions = Vec3.multiply(dimensionRatio, IMAGE_DIMENSIONS); + return {"dimensions": dimensions, "visible": true, "position": target, "url": isInfinite ? INFINITY_ICON_PATH : TARGET_ICON_PATH}; + } + + this.showTarget = function(headTarget, eyeTarget) { + if (!self.active) { + return; + } + var targetProps = self.getTargetProps(eyeTarget); + Overlays.editOverlay(self.eyeTarget, targetProps); + var headTargetProps = self.getTargetProps(headTarget); + Overlays.editOverlay(self.headTarget, headTargetProps); + } + + this.hideEyeTarget = function() { + Overlays.editOverlay(self.eyeTarget, {"visible": false}); + } + + this.highLightIDs = function(selectionName, ids) { + self.items[selectionName] = ids; + for (var idx in ids) { + Selection.addToSelectedItemsList(selectionName, "avatar", ids[idx]); + } + } + this.clearSelection = function() { + for (key in self.Styles) { + var selStyle = self.Styles[key]; + Selection.clearSelectedItemsList(selStyle.name); + } + } + + this.turnOff = function() { + self.clearLog(); + for (var key in self.Styles) { + var selStyle = self.Styles[key]; + Selection.disableListHighlight(selStyle.name); + } + Overlays.deleteOverlay(self.headTarget); + self.headTarget = undefined; + Overlays.deleteOverlay(self.eyeTarget); + self.eyeTarget = undefined; + } + + this.finish = function() { + if (self.active) { + self.setActive(false); + } + Overlays.deleteOverlay(self.textBox); + self.textBox = undefined; + Overlays.deleteOverlay(self.activeCheckBox); + self.activeCheckBox = undefined; + Overlays.deleteOverlay(self.activeCaption); + self.activeCaption = undefined; + Overlays.deleteOverlay(self.activeCheckBoxMark); + self.activeCheckBoxMark = undefined; + } + + this.init(); + } + + ////////////////////////////////////////////// + // randomHelper.js ////////// + ////////////////////////////////////////////// + + var RandomHelper = function() { + var self = this; + + this.createRandomIndexes = function(count) { + var indexes = []; + for (var n = 0; n < count; n++) { + indexes.push(n); + } + var randomIndexes = []; + for (var n = 0; n < count; n++) { + var indexesCount = indexes.length; + var randomIndex = 0; + if (indexesCount > 1) { + var randFactor = Math.random(); + randomIndex = randFactor !== 1.0 ? Math.floor(randFactor * indexes.length) : indexes.length - 1; + } + randomIndexes.push(indexes[randomIndex]); + indexes.splice(randomIndex, 1); + } + return randomIndexes; + } + + this.getRandomKey = function(keypool) { + // keypool can be an object. {key1: percentage1, key2: percentage2} or + // keypool can be an array. [key1, key2] Equal percentage each component + + var equalChance = Array.isArray(keypool); + var totalPercentage = 0.0; + var percentages = {}; + var normalizedPercentages = {}; + var keys = equalChance ? keypool : Object.keys(keypool); + for (var n = 0; n < keys.length; n++) { + var key = keys[n]; + percentages[key] = equalChance ? 1.0 / keys.length : keypool[key].chance; + totalPercentage += equalChance ? percentages[key] : keypool[key].chance; + } + var accumulatedVal = 0.0; + for (var n = 0; n < keys.length; n++) { + var key = keys[n]; + var val = accumulatedVal + (percentages[key] / totalPercentage); + normalizedPercentages[key] = val; + accumulatedVal = val; + } + var dice = Math.random(); + var floor = 0.0; + var hit = normalizedPercentages[keys[0]]; + for (var n = 0; n < keys.length; n++) { + var key = keys[n]; + if (dice > floor && dice < normalizedPercentages[key]) { + hit = key; + break; + } + floor = normalizedPercentages[key]; + } + return { randomKey: hit, chance: percentages[hit]}; + } + } + + + ////////////////////////////////////////////// + // automaticMachine.js ////////// + ////////////////////////////////////////////// + var MIN_LOOKAT_HEAD_MIX_ALPHA = 0.04; + var MAX_LOOKAT_HEAD_MIX_ALPHA = 0.08; + var CAMERA_HEAD_MIX_ALPHA = 0.06; + + var TargetType = { + "unknown" : 0, + "avatar" : 1, + "entity" : 2 + } + + var TargetOffsetMode = { + "noOffset" : 0, + "onlyHead" : 1, + "onlyEyes" : 2, + "headAndEyes" : 3, + "print" : function(sta) { + return ("OffsetMode: " + (Object.keys(TargetOffsetMode))[sta]); + } + } + + var TargetMode = { + "noTarget" : 0, + "leftEye" : 1, + "rightEye" : 2, + "mouth" : 3, + "leftHand" : 4, + "rightHand" : 5, + "random" : 6, + "print" : function(sta) { + return ("TargetMode: " + (Object.keys(TargetMode))[sta]); + } + } + + var ACTION_CONFIGURATION = { + "TargetMode.mouth": { + "joint": "Head", + "stareTimeRange" : {"min": 0.2, "max": 2.0}, + "headSpeedRange" : {"min": MAX_LOOKAT_HEAD_MIX_ALPHA, "max": MIN_LOOKAT_HEAD_MIX_ALPHA}, + "offsetChances" : { + "TargetOffsetMode.noOffset": {"chance": 0.7}, + "TargetOffsetMode.onlyHead": {"chance": 0.3}, + "TargetOffsetMode.onlyEyes": {"chance": 0.0}, + "TargetOffsetMode.headAndEyes": {"chance": 0.0} + }, + "offsetAngleRange" : {"min": 1.0, "max": 5.0}, + "chanceWhileListening" : 0.45, + "chanceWhileTalking" : 0.25 + }, + "TargetMode.leftEye": { + "joint": "LeftEye", + "stareTimeRange" : {"min": 0.2, "max": 2.0}, + "headSpeedRange" : {"min": MAX_LOOKAT_HEAD_MIX_ALPHA, "max": MIN_LOOKAT_HEAD_MIX_ALPHA}, + "offsetChances" : { + "TargetOffsetMode.noOffset": {"chance": 0.5}, + "TargetOffsetMode.onlyHead": {"chance": 0.3}, + "TargetOffsetMode.onlyEyes": {"chance": 0.1}, + "TargetOffsetMode.headAndEyes": {"chance": 0.1} + }, + "offsetAngleRange" : {"min": 1.0, "max": 5.0}, + "chanceWhileListening" : 0.20, + "chanceWhileTalking" : 0.30 + }, + "TargetMode.rightEye": { + "joint": "RightEye", + "stareTimeRange" : {"min": 0.2, "max": 2.0}, + "headSpeedRange" : {"min": MAX_LOOKAT_HEAD_MIX_ALPHA, "max": MIN_LOOKAT_HEAD_MIX_ALPHA}, + "offsetChances" : { + "TargetOffsetMode.noOffset": {"chance": 0.5}, + "TargetOffsetMode.onlyHead": {"chance": 0.3}, + "TargetOffsetMode.onlyEyes": {"chance": 0.1}, + "TargetOffsetMode.headAndEyes": {"chance": 0.1} + }, + "offsetAngleRange" : {"min": 1.0, "max": 5.0}, + "chanceWhileListening" : 0.20, + "chanceWhileTalking" : 0.30 + }, + "TargetMode.leftHand": { + "joint": "LeftHand", + "stareTimeRange" : {"min": 0.2, "max": 2.0}, + "headSpeedRange" : {"min": MAX_LOOKAT_HEAD_MIX_ALPHA, "max": MIN_LOOKAT_HEAD_MIX_ALPHA}, + "offsetChances" : { + "TargetOffsetMode.noOffset": {"chance": 0.9}, + "TargetOffsetMode.onlyHead": {"chance": 0.1}, + "TargetOffsetMode.onlyEyes": {"chance": 0.0}, + "TargetOffsetMode.headAndEyes": {"chance": 0.0} + }, + "offsetAngleRange" : {"min": 1.0, "max": 10.0}, + "chanceWhileListening" : 0.05, + "chanceWhileTalking" : 0.05 + }, + "TargetMode.rightHand": { + "joint": "RightHand", + "stareTimeRange" : {"min": 0.2, "max": 2.0}, + "headSpeedRange" : {"min": MAX_LOOKAT_HEAD_MIX_ALPHA, "max": MIN_LOOKAT_HEAD_MIX_ALPHA}, + "offsetChances" : { + "TargetOffsetMode.noOffset": {"chance": 0.9}, + "TargetOffsetMode.onlyHead": {"chance": 0.1}, + "TargetOffsetMode.onlyEyes": {"chance": 0.0}, + "TargetOffsetMode.headAndEyes": {"chance": 0.0} + }, + "offsetAngleRange" : {"min": 1.0, "max": 10.0}, + "chanceWhileListening" : 0.05, + "chanceWhileTalking" : 0.05 + }, + "TargetMode.random": { + "joint": "Head", + "stareTimeRange" : {"min": 0.2, "max": 1.0}, + "headSpeedRange" : {"min": MAX_LOOKAT_HEAD_MIX_ALPHA, "max": MIN_LOOKAT_HEAD_MIX_ALPHA}, + "offsetChances" : { + "TargetOffsetMode.noOffset": {"chance": 0.0}, + "TargetOffsetMode.onlyHead": {"chance": 0.0}, + "TargetOffsetMode.onlyEyes": {"chance": 0.4}, + "TargetOffsetMode.headAndEyes": {"chance": 0.6} + }, + "offsetAngleRange" : {"min": 5.0, "max": 12.0}, + "chanceWhileListening" : 0.05, + "chanceWhileTalking" : 0.05 + }, + "TargetMode.noTarget": { + "joint": undefined, + "stareTimeRange" : {"min": 0.1, "max": 2.0}, + "headSpeedRange" : {"min": MAX_LOOKAT_HEAD_MIX_ALPHA, "max": MIN_LOOKAT_HEAD_MIX_ALPHA}, + "offsetChances" : { + "TargetOffsetMode.noOffset": {"chance": 0.5}, + "TargetOffsetMode.onlyHead": {"chance": 0.0}, + "TargetOffsetMode.onlyEyes": {"chance": 0.5}, + "TargetOffsetMode.headAndEyes": {"chance": 0.0} + }, + "offsetAngleRange" : {"min": 1.0, "max": 15.0}, + "chanceWhileListening" : 0.0, + "chanceWhileTalking" : 0.0 + } + } + + var FOCUS_MODE_CHANCES = { + "idle" : { + "TargetMode.mouth": {"chance": 0.2}, + "TargetMode.rightEye": {"chance": 0.4}, + "TargetMode.leftEye": {"chance": 0.4} + }, + "talking" : ["TargetMode.mouth", "TargetMode.rightEye", "TargetMode.leftEye"] // Equal chances (33.33% each) + } + + var LookAction = function() { + var self = this; + this.targetType = TargetType.unknown; + this.speed = MIN_LOOKAT_HEAD_MIX_ALPHA; + this.id = ""; + this.focusName = "None"; + this.focusChance = 0.0; + this.config = undefined; + this.targetMode = undefined; + this.lookAtJoint = undefined; + this.targetPoint = undefined; + this.elapseTime = 0.0; + this.totalTime = 1.0; + this.eyesHeadOffset = Vec3.ZERO; + this.eyesForward = false; + this.offsetEyes = false; + this.offsetHead = false; + this.offsetModeName = ""; + this.offsetChance = 0.0; + this.confortAngle = 0.0; + this.printChance = function(chance) { + return "" + Math.floor(100 * chance) + "%"; + } + this.print = function() { + var lines = []; + lines.push(TargetMode.print(eval(self.targetMode)) + " P: " + self.printChance(self.focusChance)); + lines.push(TargetOffsetMode.print(eval(self.offsetModeName)) + " P: " + self.printChance(self.offsetChance)); + lines.push("Action time: " + self.totalTime.toFixed(2) + " seconds"); + return lines; + } + } + + var AudienceAvatar = function(id) { + var self = this; + this.id = id; + this.name = ""; + this.engaged = true; + this.moved = true; + this.isTalking = false; + this.isListening = false; + this.position = Vec3.ZERO; + this.headPosition = Vec3.ZERO; + this.leftPalmPosition = Vec3.ZERO; + this.rightPalmPosition = Vec3.ZERO; + this.leftHandSpeed = 0.0; + this.rightHandSpeed = 0.0; + this.velocity = 0.0; + this.reactionTime = 0.0; + this.distance = 0.0; + this.loudness = 0.0; + this.talkingTime = 0.0; + } + + + var SmartLookMachine = function() { + var self = this; + this.myAvatarID = MyAvatar.sessionUUID; + + this.nearAvatarList = {}; + this.dice = new RandomHelper(); + + var LOOK_FOR_AVATARS_MAX_DISTANCE = 15.0; + var MIN_FOCUS_TO_LISTENER_TIME = 3.0; + var MAX_FOCUS_TO_LISTENER_TIME = 5.0; + var MIN_FOCUS_TO_TALKER_TIME = 0.5; + var MAX_FOCUS_TO_TALKER_RANGE = 1.5; + var TRIGGER_FOCUS_WHILE_IDLE_CHANCE = 0.1; + + + + this.currentAvatarFocusID = undefined; + this.currentTalker; + this.currentAction = new LookAction(); + + this.eyesTargetPoint = Vec3.ZERO; + this.headTargetPoint = Vec3.ZERO; + + + this.timeScale = 1.0; + this.lookAtDebugger = new LookAtDebugger(); + + this.active = true; + this.headTargetSpeed = 0.0; + + this.avatarFocusTotalTime = 0.0; + this.avatarFocusMax = 0.0; + this.lockedFocusID = undefined; + + this.visibilityCount = 0; + + this.shouldUpdateDebug = false; + + var TalkingState = { + "noTalking" : 0, + "meTalkingFirst" : 1, + "meTalkingAgain" : 2, + "otherTalkingFirst" : 3, + "otherTalkingAgain" : 4, + "othersTalking": 5, + "print" : function(sta) { + return ("TalkingState: " + (Object.keys(TalkingState))[sta]); + } + } + this.talkingState = TalkingState.noTalking; + var FocusState = { + "onNobody" : 0, + "onTalker" : 1, + "onRandomAudience" : 2, + "onLastTalker" : 3, + "onRandomLastTalker" : 4, + "onLastFocus" : 5, + "onSelected" : 6, + "onMovement" : 7, + "print" : function(sta) { + return ("FocusState: " + (Object.keys(FocusState))[sta]); + } + } + + this.focusState = FocusState.onNobody; + + var LockFocusType = { + "none" : 0, + "click" : 1, + "movement" : 2 + } + self.lockFocusType = LockFocusType.none; + + this.wasMeTalking = false; + this.nearAvatarIDs = []; + + this.updateAvatarVisibility = function() { + if (self.nearAvatarIDs.length > 0) { + if (self.nearAvatarList[self.myAvatarID] && self.nearAvatarList[self.myAvatarID].moved) { + for (id in self.nearAvatarList) { + self.nearAvatarList[id].moved = true; + } + self.nearAvatarList[self.myAvatarID].moved = false; + } + self.visibilityCount = ((self.visibilityCount + 1) >= self.nearAvatarIDs.length) ? 0 : self.visibilityCount + 1; + var id = self.nearAvatarIDs[self.visibilityCount]; + var avatar = self.nearAvatarList[id]; + if (id !== self.myAvatarID && avatar !== undefined && avatar.moved) { + self.nearAvatarList[id].moved = false; + var eyePos = MyAvatar.getDefaultEyePosition(); + var avatarSight = Vec3.subtract(avatar.headPosition, eyePos); + var intersection = Entities.findRayIntersection({origin: eyePos, direction: Vec3.normalize(avatarSight)}, true); + self.nearAvatarList[avatar.id].engaged = !intersection.intersects || intersection.distance > Vec3.length(avatarSight); + } + } + } + + this.getEngagedAvatars = function() { + var engagedAvatarIDs = []; + for (var id in self.nearAvatarList) { + if (self.nearAvatarList[id].engaged) { + engagedAvatarIDs.push(id); + } + } + return engagedAvatarIDs; + } + + this.updateAvatarList = function(deltaTime) { + var TALKING_LOUDNESS_THRESHOLD = 50.0; + var SILENCE_TALK_ATTENUATION = 0.5; + var ATTENTION_HANDS_SPEED = 5.0; + var ATTENTION_AVATAR_SPEED = 2.0; + var MAX_TALKING_TIME = 5.0; + var talkingAvatarID; + var maxLoudness = 0.0; + var count = 0; + var previousTalkers = []; + var fastHands = []; + var fastMovers = []; + var lookupCenter = Vec3.sum(MyAvatar.position, Vec3.multiply(0.8 * LOOK_FOR_AVATARS_MAX_DISTANCE, Quat.getFront(MyAvatar.orientation))); + var nearbyAvatars = AvatarManager.getAvatarsInRange(lookupCenter, LOOK_FOR_AVATARS_MAX_DISTANCE); + for (var n = 0; n < nearbyAvatars.length; n++) { + var avatar = AvatarManager.getAvatar(nearbyAvatars[n]); + var distance = Vec3.distance(MyAvatar.position, avatar.position); + var loudness = avatar.audioLoudness; + loudness = avatar.audioLoudness > 30.0 ? 100.0 : 0.0; + var TALKING_TAU = 0.01; + if (self.nearAvatarList[avatar.sessionUUID] === undefined) { + self.nearAvatarList[avatar.sessionUUID] = new AudienceAvatar(avatar.sessionUUID); + self.nearAvatarList[avatar.sessionUUID].name = avatar.displayName; + } else { + if (Vec3.distance(self.nearAvatarList[avatar.sessionUUID].position, avatar.position) > 0.0) { + self.nearAvatarList[avatar.sessionUUID].moved = true; + } else { + self.nearAvatarList[avatar.sessionUUID].velocity = Vec3.length(avatar.velocity); + if (self.nearAvatarList[avatar.sessionUUID].velocity > 0.0) { + self.nearAvatarList[avatar.sessionUUID].moved = true; + } + } + self.nearAvatarList[avatar.sessionUUID].position = avatar.position; + self.nearAvatarList[avatar.sessionUUID].headPosition = avatar.getJointPosition("Head"); + if (self.nearAvatarList[avatar.sessionUUID].engaged) { + self.nearAvatarList[avatar.sessionUUID].loudness = self.nearAvatarList[avatar.sessionUUID].loudness + TALKING_TAU * (loudness - self.nearAvatarList[avatar.sessionUUID].loudness); + self.nearAvatarList[avatar.sessionUUID].orientation = avatar.orientation; + var leftPalmPos = avatar.getJointPosition("LeftHand"); + var rightPalmPos = avatar.getJointPosition("RightHand"); + + var distanceAttenuation = (distance > 1.0) ? (1.0 / distance) : 1.0; + var leftPalmSpeed = distanceAttenuation * Vec3.distance(self.nearAvatarList[avatar.sessionUUID].leftPalmPosition, leftPalmPos) / deltaTime; + var rightPalmSpeed = distanceAttenuation * Vec3.distance(self.nearAvatarList[avatar.sessionUUID].rightPalmPosition, rightPalmPos) / deltaTime; + self.nearAvatarList[avatar.sessionUUID].leftPalmSpeed = leftPalmSpeed; + self.nearAvatarList[avatar.sessionUUID].rightPalmSpeed = rightPalmSpeed; + + self.nearAvatarList[avatar.sessionUUID].leftPalmPosition = leftPalmPos; + self.nearAvatarList[avatar.sessionUUID].rightPalmPosition = rightPalmPos; + if (self.nearAvatarList[avatar.sessionUUID] && self.nearAvatarList[avatar.sessionUUID].loudness) { + self.nearAvatarList[avatar.sessionUUID].loudness += 0.1; + } + self.nearAvatarList[avatar.sessionUUID].isTalking = false; + self.nearAvatarList[avatar.sessionUUID].isTalker = false; + if (self.nearAvatarList[avatar.sessionUUID].loudness > TALKING_LOUDNESS_THRESHOLD) { + if (self.nearAvatarList[avatar.sessionUUID].talkingTime < MAX_TALKING_TIME) { + self.nearAvatarList[avatar.sessionUUID].talkingTime += deltaTime; + } + self.nearAvatarList[avatar.sessionUUID].isTalking = true; + count++; + if (maxLoudness < self.nearAvatarList[avatar.sessionUUID].loudness) { + maxLoudness = self.nearAvatarList[avatar.sessionUUID].loudness; + talkingAvatarID = avatar.sessionUUID; + } + } else if (self.nearAvatarList[avatar.sessionUUID].talkingTime > 0.0){ + self.nearAvatarList[avatar.sessionUUID].talkingTime -= SILENCE_TALK_ATTENUATION * deltaTime; + if (self.nearAvatarList[avatar.sessionUUID].talkingTime < 0.0) { + self.nearAvatarList[avatar.sessionUUID].talkingTime = 0.0; + } + } + if (!self.nearAvatarList[avatar.sessionUUID].isTalking && self.nearAvatarList[avatar.sessionUUID].talkingTime > 0.0){ + previousTalkers.push(avatar.sessionUUID); + } + if ((leftPalmSpeed > ATTENTION_AVATAR_SPEED || rightPalmSpeed > ATTENTION_AVATAR_SPEED) && avatar.sessionUUID !== self.myAvatarID) { + if (!self.nearAvatarList[avatar.sessionUUID].isTalking) { + fastHands.push(avatar.sessionUUID); + } else if (self.nearAvatarList[avatar.sessionUUID].isTalking) { + var headPos = avatar.getJointPosition("Neck"); + var raisedHand = Math.max(leftPalmPos.y, rightPalmPos.y) > headPos.y; + if (raisedHand) { + // If the talker raise the hands it will trigger attention + fastHands.push(avatar.sessionUUID); + } + } + } + } + } + } + + for (var id in self.nearAvatarList) { + if (nearbyAvatars.indexOf(id) == -1) { + delete self.nearAvatarList[id]; + } + } + self.nearAvatarIDs = Object.keys(self.nearAvatarList); + if (self.nearAvatarList[self.myAvatarID] === undefined) { + self.myAvatarID = MyAvatar.sessionUUID; + } + if (talkingAvatarID !== undefined) { + self.nearAvatarList[talkingAvatarID].isTalker = true; + } + self.updateAvatarVisibility(); + return { talker: talkingAvatarID, talkingCount: count, previousTalkers: previousTalkers, fastHands: fastHands, fastMovers: fastMovers }; + } + + this.getHeadConfortAngle = function(point) { + var eyesToPoint = Vec3.subtract(point, MyAvatar.getDefaultEyePosition()); + var angle = Vec3.getAngle(eyesToPoint, Quat.getFront(MyAvatar.orientation)) / DEGREE_TO_RADIAN; + angle = Math.min(angle, 90.0); + var MAX_OFFSET_DEGREES = 20.0; + var offsetMultiplier = MAX_OFFSET_DEGREES / 90.0; + var facingRight = Vec3.dot(eyesToPoint, Quat.getRight(MyAvatar.orientation)) > 0.0; + angle = (facingRight ? 1.0 : -1.0) * offsetMultiplier * angle; + return angle; + } + + this.updateCurrentAction = function(deltaTime) { + if (self.currentAction.lookAtJoint !== undefined) { + var avatar = AvatarList.getAvatar(self.currentAction.id); + self.currentAction.targetPoint = avatar.getJointPosition(self.currentAction.lookAtJoint); + } + self.currentAction.elapseTime += deltaTime; + } + + this.requestNewAction = function(targetType, id) { + + var HAND_ATTENTION_TRIGGER_SPEED = 0.2; + var action = new LookAction(); + action.targetType = targetType; + action.id = id; + var sortStare = false; + action.targetMode = "TargetMode.noTarget"; + if (targetType == TargetType.avatar && id !== undefined && self.nearAvatarList[id] !== undefined) { + var avatar = AvatarList.getAvatar(id); + action.focusName = self.nearAvatarList[id].name; + if (self.nearAvatarList[id].rightPalmSpeed > HAND_ATTENTION_TRIGGER_SPEED || self.nearAvatarList[id].leftPalmSpeed > HAND_ATTENTION_TRIGGER_SPEED) { + if (self.nearAvatarList[id].rightPalmSpeed < self.nearAvatarList[id].leftPalmSpeed) { + action.targetMode = "TargetMode.leftHand"; + } else { + action.targetMode = "TargetMode.rightHand"; + } + action.focusChance = 1.0; + } else { + var faceChances = self.nearAvatarList[id].isTalking ? FOCUS_MODE_CHANCES.talking : FOCUS_MODE_CHANCES.idle; + var randomFaceTargetMode = self.dice.getRandomKey(faceChances); + action.targetMode = randomFaceTargetMode.randomKey; + action.focusChance = randomFaceTargetMode.chance; + } + + } else if (targetType == TargetType.entity) { + // TODO + // Randomize around the entity size + } + var actionConfig = ACTION_CONFIGURATION[action.targetMode]; + action.config = actionConfig; + action.lookAtJoint = actionConfig.joint; + action.targetPoint = action.lookAtJoint !== undefined ? avatar.getJointPosition(action.lookAtJoint) : action.targetPoint = MyAvatar.getHeadLookAt(); + var randomKeyResult = self.dice.getRandomKey(actionConfig.offsetChances); + action.offsetModeName = randomKeyResult.randomKey; + action.offsetChance = randomKeyResult.chance; + var offsetMode = eval(action.offsetModeName); + if (offsetMode !== TargetOffsetMode.noOffset) { + var headPosition = MyAvatar.getJointPosition("Head"); + var headToTarget = Vec3.subtract(action.targetPoint, headPosition); + var randAngle = actionConfig.offsetAngleRange.min + Math.random() * (actionConfig.offsetAngleRange.max - actionConfig.offsetAngleRange.min); + var randAngle = Math.random() < 0.5 ? -randAngle : randAngle; + if (self.nearAvatarList[self.myAvatarID]) { + var randOffsetRotation = Quat.angleAxis(randAngle, Vec3.UNIT_Y); + action.eyesHeadOffset = Vec3.subtract(Vec3.sum(headPosition, Vec3.multiplyQbyV(randOffsetRotation, headToTarget)), action.targetPoint); + } + action.offsetEyes = offsetMode === TargetOffsetMode.onlyEyes || offsetMode === TargetOffsetMode.headAndEyes; + action.offsetHead = offsetMode === TargetOffsetMode.onlyHead || offsetMode === TargetOffsetMode.headAndEyes; + } + action.totalTime = actionConfig.stareTimeRange.min + Math.random() * (actionConfig.stareTimeRange.max - actionConfig.stareTimeRange.min); + action.confortAngle = self.getHeadConfortAngle(action.targetPoint); + action.speed = actionConfig.headSpeedRange.min + Math.random() * (actionConfig.headSpeedRange.max - actionConfig.headSpeedRange.min); + + return action; + } + + this.findAudienceAvatar = function(avatarIDs) { + // We look for avatars on the avatarIDs array if provided + // If not avatarIDs becomes the array with all the engaged avatars nearAvatarList + if (avatarIDs === undefined) { + avatarIDs = self.nearAvatarIDs; + } + var randAvatarID; + var MAX_AUDIENCE_DISTANCE = 8; + var firstAnyOther = undefined; + var firstNearOther = undefined; + if (avatarIDs.length > 1) { + var randomIndexes = self.dice.createRandomIndexes(avatarIDs.length); + for (var n = 0; n < randomIndexes.length; n++) { + var avatarID = avatarIDs[randomIndexes[n]]; + var avatar = self.nearAvatarList[avatarID]; + if (avatarID != self.myAvatarID && avatar.engaged) { + firstAnyOther = !firstAnyOther ? avatarID : firstAnyOther; + + if (avatar.distance < MAX_AUDIENCE_DISTANCE) { + firstNearOther = !firstNearOther ? avatarID : firstNearOther; + var otherToMe = Vec3.normalize(Vec3.subtract(self.nearAvatarList[self.myAvatarID].position, avatar.position)); + var myFront = Quat.getFront(self.nearAvatarList[self.myAvatarID].orientation); + var otherFront = Quat.getFront(avatar.orientation); + if (Vec3.dot(otherToMe, otherFront) > 0.0 && Vec3.dot(myFront, otherToMe) < 0.0) { + randAvatarID = avatarID; + break; + } + } + if (n === randomIndexes.length - 1) { + // We have not found a valid candidate facing us + // return the first id different from out avatar's id + randAvatarID = firstNearOther !== undefined ? firstNearOther : firstAnyOther; + } + } + } + } else if (avatarIDs.length > 0 && avatarIDs[0] != self.myAvatarID && self.nearAvatarList[avatarIDs[0]].engaged){ + // If the array provided only has one ID + randAvatarID = avatarIDs[0]; + } + return randAvatarID; + } + + this.applyHeadOffset = function(point, angle) { + var eyesToPoint = Vec3.subtract(point, MyAvatar.getDefaultEyePosition()); + var offsetRot = Quat.angleAxis(angle, Quat.getUp(MyAvatar.orientation)); + var offsetPoint = Vec3.sum(MyAvatar.getDefaultEyePosition(), Vec3.multiplyQbyV(offsetRot, eyesToPoint)); + return offsetPoint; + } + + this.computeTalkingState = function(sceneData, myAvatarID, currentTalker, currentFocus) { + var talkingState = TalkingState.noTalking; + if (sceneData.talker === myAvatarID) { + if (currentTalker != myAvatarID) { + talkingState = TalkingState.meTalkingFirst; + } else { + talkingState = TalkingState.meTalkingAgain; + } + } else if (sceneData.talkingCount > 1) { + talkingState = TalkingState.othersTalking; + } else if (sceneData.talkingCount > 0) { + if (sceneData.talker !== currentFocus) { + talkingState = TalkingState.otherTalkingFirst; + } else { + talkingState = TalkingState.otherTalkingAgain; + } + } + return talkingState; + } + + this.computeFocusState = function(sceneData, talkingState, currentFocus, lockedFocus, lockType) { + var focusState = FocusState.onNobody; + switch (talkingState) { + case TalkingState.noTalking : { + if (sceneData.previousTalkers.length > 0) { + focusState = FocusState.onLastTalker; + } else if (Math.random() < TRIGGER_FOCUS_WHILE_IDLE_CHANCE) { + // There is chance of triggering a random focus when nobody is talking + focusState = FocusState.onRandomAudience; + } else { + focusState = FocusState.onNobody; + } + break; + } + case TalkingState.meTalkingFirst : { + if (currentFocus !== undefined) { + // Look at the last focused avatar + focusState = FocusState.onLastFocus; + } else if (sceneData.previousTalkers.length > 0) { + // Look at one of the previous talkers + focusState = FocusState.onRandomLastTalker; + } else { + focusState = FocusState.onRandomAudience; + } + break; + } + case TalkingState.meTalkingAgain : { + // Look at any random avatar + focusState = FocusState.onRandomAudience; + break; + } + case TalkingState.otherTalkingAgain : { + // If we were focused already on the talker we have a 15% chance to look at somebody else + // randomly giving preference to the previous talkers + if (Math.random() < 0.15) { + focusState = FocusState.onRandomLastTalker; + } else { + focusState = FocusState.onTalker; + } + break; + } + case TalkingState.otherTalkingFirst : { + // Focus on the new talker + focusState = FocusState.onTalker; + break; + } + case TalkingState.othersTalking : { + // When multiple people talk at the same time we have a 50% chance of not changing focus + if (Math.random() < 0.5) { + focusState = FocusState.onLastFocus; + } else { + focusState = FocusState.onTalker; + } + break; + } + } + if (lockedFocus !== undefined) { + if (lockType === LockFocusType.click) { + focusState = FocusState.onSelected; + } else if (lockType === LockFocusType.movement) { + focusState = FocusState.onMovement; + } + } + return focusState; + } + + this.computeAvatarFocus = function(sceneData, focusState, currentFocus, lockedFocus) { + var avatarFocusID = undefined; + switch (focusState) { + case FocusState.onTalker: { + avatarFocusID = sceneData.talker; + break; + } + case FocusState.onRandomAudience: { + avatarFocusID = self.findAudienceAvatar(); + break; + } + case FocusState.onLastTalker: + case FocusState.onRandomLastTalker: { + if (sceneData.previousTalkers.length > 0) { + avatarFocusID = self.findAudienceAvatar(sceneData.previousTalkers); + } + if (avatarFocusID === undefined) { + // Guarantee a 20% chance of looking at somebody + if (focusState === FocusState.onRandomLastTalker || Math.random() < 0.2) { + avatarFocusID = self.findAudienceAvatar(); + } + } + break; + } + case FocusState.onLastFocus: { + avatarFocusID = currentFocus; + break; + } + case FocusState.onMovement: + case FocusState.onSelected: { + avatarFocusID = lockedFocus; + break; + } + } + return avatarFocusID; + } + + this.forceFocus = function(avatarID) { + if (self.nearAvatarList[avatarID] !== undefined) { + self.lockedFocusID = avatarID; + self.lockFocusType = LockFocusType.click; + } + } + + this.logAction = function(action) { + self.lookAtDebugger.clearLog(); + self.lookAtDebugger.log(TalkingState.print(self.talkingState)); + self.lookAtDebugger.log("________________________Focus"); + self.lookAtDebugger.log("On avatar: " + action.focusName); + self.lookAtDebugger.log(FocusState.print(self.focusState)); + self.lookAtDebugger.log("Focus time: " + self.avatarFocusMax.toFixed(2) + " seconds"); + self.lookAtDebugger.log("________________________Action"); + var extraLogs = action.print(); + for (var n = 0; n < extraLogs.length; n++) { + self.lookAtDebugger.log(extraLogs[n]); + } + } + + this.update = function(deltaTime) { + var FPS = 60.0; + var CLICKED_AVATAR_MAX_FOCUS_TIME = 10.0; + self.timeScale = deltaTime * FPS; + + var sceneData = self.updateAvatarList(deltaTime); + if (self.nearAvatarIDs.length === 0) { + return; + } + + var abortAction = self.lockFocusType === LockFocusType.click; + // Focus on any avatar moving their hands + if (sceneData.fastHands.length > 0 && self.lockFocusType === LockFocusType.none) { + var randomFastHands = self.findAudienceAvatar(sceneData.fastHands); + if (self.nearAvatarList[randomFastHands] && (!self.nearAvatarList[randomFastHands].isTalking || self.nearAvatarList[randomFastHands].isTalker)) { + abortAction = Math.random() < 0.3; + self.lockedFocusID = randomFastHands; + self.lockFocusType = LockFocusType.movement; + } + } else if (self.avatarFocusTotalTime >= self.avatarFocusMax) { + self.lockFocusType = LockFocusType.none; + } + + // Set the talking status + self.talkingState = self.computeTalkingState(sceneData, self.myAvatarID, self.currentTalker, self.currentAvatarFocusID); + + // If the talker change, we have a 50% chance to focus on them once the next action is completed. + var otherTalkerTriggerRefocus = self.talkingState === TalkingState.otherTalkingFirst && Math.random() < 0.5; + if (self.lockedFocusID !== undefined || otherTalkerTriggerRefocus) { + // Force a new focus + self.avatarFocusTotalTime = self.avatarFocusMax; + if (abortAction) { + // Force a new action + self.currentAction.elapseTime = self.currentAction.totalTime; + } + } + var needsNewFocus = self.avatarFocusTotalTime >= self.avatarFocusMax; + var needsNewAction = self.currentAction.elapseTime >= self.currentAction.totalTime; + var newAvatarFocusID = self.currentAvatarFocusID; + + if (needsNewAction && needsNewFocus) { + self.focusState = self.computeFocusState(sceneData, self.talkingState, self.currentAvatarFocusID, self.lockedFocusID, self.lockFocusType); + newAvatarFocusID = self.computeAvatarFocus(sceneData, self.focusState, self.currentAvatarFocusID, self.lockedFocusID); + self.lockedFocusID = undefined; + if (self.lockFocusType !== LockFocusType.click) { + if (self.talkingState === TalkingState.meTalkingAgain) { + self.avatarFocusMax = MIN_FOCUS_TO_TALKER_TIME + Math.random() * MAX_FOCUS_TO_TALKER_RANGE; + } else { + self.avatarFocusMax = MIN_FOCUS_TO_LISTENER_TIME + Math.random() * MAX_FOCUS_TO_LISTENER_TIME; + } + } else { + self.avatarFocusMax = CLICKED_AVATAR_MAX_FOCUS_TIME; + } + self.avatarFocusTotalTime = 0.0; + self.currentTalker = sceneData.talker; + self.shouldUpdateDebug = true; + } else { + self.avatarFocusTotalTime += deltaTime; + } + if (needsNewAction) { + var currentFocus = newAvatarFocusID; + if (otherTalkerTriggerRefocus) { + // Reset the last action on the previous focus to provide a random delay + currentFocus = self.currentAction.id; + self.currentAction.elapseTime = 0.0; + } else { + // Create a new action + self.currentAction = self.requestNewAction(TargetType.avatar, newAvatarFocusID); + } + + if (self.currentAvatarFocusID !== newAvatarFocusID) { + self.currentAvatarFocusID = newAvatarFocusID; + } + + if (self.currentAvatarFocusID === undefined || + self.talkingState === TalkingState.meTalkingAgain || + self.talkingState === TalkingState.meTalkingFirst) { + // Minimize the head speed when we are talking or looking nowhere + self.currentAction.speed = MIN_LOOKAT_HEAD_MIX_ALPHA; + } + + self.logAction(self.currentAction); + } else { + self.updateCurrentAction(deltaTime); + } + + self.headTargetPoint = self.currentAction.targetPoint; + self.eyesTargetPoint = self.currentAction.targetPoint; + if (self.currentAction.offsetHead) { + self.headTargetPoint = Vec3.sum(self.headTargetPoint, self.currentAction.eyesHeadOffset); + } + self.headTargetPoint = self.applyHeadOffset(self.headTargetPoint, self.currentAction.confortAngle); + if (self.currentAction.offsetEyes) { + self.eyesTargetPoint = Vec3.sum(self.eyesTargetPoint, self.currentAction.eyesHeadOffset); + } + self.headTargetSpeed = Math.min(1.0, self.currentAction.speed * self.timeScale); + } + + this.getResults = function() { + return { + "eyesTarget" : self.eyesTargetPoint, + "headTarget" : self.headTargetPoint, + "headSpeed" : self.headTargetSpeed + } + } + + } + + ////////////////////////////////////////////// + // autoLook.js /////// + ////////////////////////////////////////////// + + var LookAtController = function() { + var CAMERA_HEAD_MIX_ALPHA = 0.06; + var LookState = { + "CameraLookActivating": 0, + "CameraLookActive":1, + "ClickToLookDeactivating":2, + "ClickToLookActive":3, + "AutomaticLook": 4 + } + + var self = this; + this.smartLookAt = new SmartLookMachine(); + this.currentState = LookState.AutomaticLook; + this.lookAtTarget = undefined; + this.lookingAtAvatarID = undefined; + this.lookingAvatarJointIndex = undefined; + + this.interpolatedHeadLookAt = MyAvatar.getHeadLookAt(); + + var CLICK_TO_LOOK_TOTAL_TIME = 5.0; + this.clickToLookTimer = 0; + + var CAMERA_LOOK_TOTAL_TIME = 5.0; + this.cameraLookTimer = 0; + + this.timeScale = 1.0; + this.eyesTarget = Vec3.ZERO; + this.headTarget = Vec3.ZERO; + + this.mousePressEvent = function(event) { + if (event.isLeftButton) { + self.smartLookAt.lookAtDebugger.onClick(event); + if (self.currentState === LookState.AutomaticLook) { + var pickRay = Camera.computePickRay(event.x, event.y); + var intersection = AvatarManager.findRayIntersection({origin: pickRay.origin, direction: pickRay.direction}, [], [self.smartLookAt.myAvatarID], false); + self.lookingAtAvatarID = intersection.intersects ? intersection.avatarID : undefined; + if (self.lookingAtAvatarID) { + self.smartLookAt.forceFocus(self.lookingAtAvatarID); + } + } + } + } + + this.mouseMoveEvent = function(event) { + if (event.isRightButton && self.currentState === LookState.AutomaticLook) { + self.currentState = LookState.CameraLookActivating; + self.cameraLookTimer = 0.0; + } + } + + this.updateHeadLookAtTarget = function(target, interpolatedTarget, speed, noPitch) { + var eyesPosition = MyAvatar.getDefaultEyePosition(); + var targetRot = Quat.lookAt(eyesPosition, target, Quat.getUp(MyAvatar.orientation)); + var interpolatedRot = Quat.lookAt(eyesPosition, interpolatedTarget, Quat.getUp(MyAvatar.orientation)); + var newInterpolatedRot = Quat.mix(interpolatedRot, targetRot, speed); + var newInterpolatedTarget = Vec3.sum(eyesPosition, Vec3.multiply(Vec3.distance(eyesPosition, target), Quat.getFront(newInterpolatedRot))); + // avoid pitch + if (noPitch) { + newInterpolatedTarget.y = eyesPosition.y; + } + MyAvatar.setHeadLookAt(newInterpolatedTarget); + return newInterpolatedTarget; + } + + this.retargetHeadTo = function(target, speed, noPitch, tolerance) { + var eyePos = MyAvatar.getDefaultEyePosition(); + var localTarget = Vec3.normalize(Vec3.subtract(target, eyePos)); + self.interpolatedHeadLookAt = self.updateHeadLookAtTarget(target, self.interpolatedHeadLookAt, speed, noPitch); + return (Vec3.dot(Vec3.normalize(Vec3.subtract(self.interpolatedHeadLookAt, eyePos)), Vec3.normalize(localTarget)) > tolerance); + } + + var MAX_INTERPOLING_STEPS = 100; + this.interpolatingSteps = 0.0; + this.automaticResults = {}; + + this.update = function(deltaTime) { + // Update timeScale + var FPS = 60.0; + self.timeScale = deltaTime * FPS; + var stateTransitSpeed = Math.min(1.0, CAMERA_HEAD_MIX_ALPHA * self.timeScale); + + var CLICK_RETARGET_TOLERANCE = 0.98; + var CAMERA_RETARGET_TOLERANCE = 0.98; + + var headTarget = MyAvatar.getHeadLookAt(); + var eyesTarget = MyAvatar.getEyesLookAt(); + + var isTargetValid = (self.lookAtTarget || self.lookingAtAvatarID); + if (isTargetValid && self.currentState === LookState.ClickToLookActive) { + if (self.lookingAtAvatarID) { + self.lookAtTarget = AvatarList.getAvatar(self.lookingAtAvatarID).getJointPosition(self.lookingAvatarJointIndex); + } + self.interpolatedHeadLookAt = self.updateHeadLookAtTarget(self.lookAtTarget, self.interpolatedHeadLookAt, stateTransitSpeed); + MyAvatar.setEyesLookAt(self.lookAtTarget); + if (self.clickToLookTimer > CLICK_TO_LOOK_TOTAL_TIME) { + self.currentState = LookState.ClickToLookDeactivating; + } + self.clickToLookTimer += deltaTime; + } else if (self.currentState === LookState.ClickToLookDeactivating) { + var breakInterpolation = self.interpolatingSteps > MAX_INTERPOLING_STEPS; + if (breakInterpolation || self.retargetHeadTo(self.automaticResults.headTarget, stateTransitSpeed, false, CLICK_RETARGET_TOLERANCE)) { + self.currentState = LookState.AutomaticLook; + self.interpolatingSteps = 0.0; + } + if (breakInterpolation) { + console.log("Breaking look at interpolation"); + } else if (self.interpolatingSteps++ === 0.0){ + console.log("Interpolating click"); + } + } else if (self.currentState === LookState.CameraLookActivating) { + var cameraFront = Quat.getFront(Camera.getOrientation()); + if (Camera.mode === "selfie") { + cameraFront = Vec3.multiply(-1, cameraFront); + } + var breakInterpolation = self.interpolatingSteps > MAX_INTERPOLING_STEPS; + if (breakInterpolation || self.retargetHeadTo(Vec3.sum(MyAvatar.getDefaultEyePosition(), cameraFront), stateTransitSpeed, true, CAMERA_RETARGET_TOLERANCE)) { + self.currentState = LookState.CameraLookActive; + self.interpolatingSteps = 0.0; + MyAvatar.releaseHeadLookAtControl(); + MyAvatar.releaseEyesLookAtControl(); + } + if (breakInterpolation) { + console.log("Breaking camera interpolation"); + } else if (self.interpolatingSteps++ === 0.0){ + console.log("Interpolating camera"); + } + } else if (self.currentState === LookState.CameraLookActive) { + if (self.cameraLookTimer > CAMERA_LOOK_TOTAL_TIME) { + // Set as initial target the current head look at + self.interpolatedHeadLookAt = MyAvatar.getHeadLookAt(); + self.currentState = LookState.AutomaticLook; + } + self.cameraLookTimer += deltaTime; + } else if (self.currentState === LookState.AutomaticLook) { + var updateLookat = Vec3.length(MyAvatar.velocity) < 1.0; + self.smartLookAt.update(deltaTime); + if (updateLookat) { + self.automaticResults = self.smartLookAt.getResults(); + self.headTarget = self.automaticResults.headTarget; + self.eyesTarget = self.automaticResults.eyesTarget; + self.interpolatedHeadLookAt = self.updateHeadLookAtTarget(self.headTarget, self.interpolatedHeadLookAt, self.automaticResults.headSpeed, true); + MyAvatar.setEyesLookAt(self.eyesTarget); + headTarget = self.interpolatedHeadLookAt; + eyesTarget = self.eyesTarget; + } else { + // Too fast. Stand by automatic look + MyAvatar.setEyesLookAt(MyAvatar.getEyesLookAt()); + MyAvatar.setHeadLookAt(MyAvatar.getHeadLookAt()); + } + } + if (self.smartLookAt.shouldUpdateDebug) { + // Update engaged avatars debugging when a new action is created + var engagedAvatars = self.smartLookAt.getEngagedAvatars(); + self.smartLookAt.lookAtDebugger.highLightAvatars(engagedAvatars, + self.smartLookAt.currentAction.id, + self.smartLookAt.currentTalker !== self.smartLookAt.currentAction.id ? self.smartLookAt.currentTalker : undefined); + } + self.smartLookAt.lookAtDebugger.showTarget(headTarget, eyesTarget); + } + + this.finish = function() { + self.smartLookAt.lookAtDebugger.finish(); + } + } + var lookAtController = new LookAtController(); + Controller.mousePressEvent.connect(lookAtController.mousePressEvent); + Controller.mouseMoveEvent.connect(lookAtController.mouseMoveEvent); + Script.update.connect(lookAtController.update); + Script.scriptEnding.connect(lookAtController.finish); +})(); \ No newline at end of file