void Awake() { compass = compassBackground; if (isVRSeekbar) { instanceVR = this; } else { instance = this; } if (!instances.Contains(this)) { instances.Add(this); } }
void Update() { VRDevices.DetectDevices(); //NOTE(Kristof): VR specific behaviour { if (XRSettings.enabled) { videoController.transform.position = Camera.main.transform.position; //NOTE(Lander): enable the highlight in the tutorial mode VRDevices.SetControllersTutorialMode(new[] { controllerLeft, controllerRight }, videoController.videoState == VideoController.VideoState.Intro); //NOTE(Kristof): Rotating the seekbar { //NOTE(Kristof): Seekbar rotation is the same as the seekbar's angle on the circle var seekbarAngle = Vector2.SignedAngle(new Vector2(Canvass.seekbar.transform.position.x, Canvass.seekbar.transform.position.z), Vector2.up); var fov = Camera.main.fieldOfView; //NOTE(Kristof): Camera rotation tells you to which angle on the circle the camera is looking towards var cameraAngle = Camera.main.transform.eulerAngles.y; //NOTE(Kristof): Calculate the absolute degree angle from the camera to the seekbar var distanceLeft = Mathf.Abs((cameraAngle - seekbarAngle + 360) % 360); var distanceRight = Mathf.Abs((cameraAngle - seekbarAngle - 360) % 360); var angle = Mathf.Min(distanceLeft, distanceRight); if (isOutofView) { if (angle < 2.5f) { isOutofView = false; } } else { if (angle > fov) { isOutofView = true; } } if (isOutofView) { var newAngle = Mathf.LerpAngle(seekbarAngle, cameraAngle, 0.025f); //NOTE(Kristof): Angle needs to be reversed, in Unity postive angles go clockwise while they go counterclockwise in the unit circle (cos and sin) //NOTE(Kristof): We also need to add an offset of 90 degrees because in Unity 0 degrees is in front of you, in the unit circle it is (1,0) on the axis var radianAngle = (-newAngle + 90) * Mathf.PI / 180; var x = 1.8f * Mathf.Cos(radianAngle); var y = Camera.main.transform.position.y - 2f; var z = 1.8f * Mathf.Sin(radianAngle); Canvass.seekbar.transform.position = new Vector3(x, y, z); Canvass.seekbar.transform.eulerAngles = new Vector3(30, newAngle, 0); } } //NOTE(Kristof): Rotating the Crosshair canvas { Ray cameraRay = Camera.main.ViewportPointToRay(new Vector2(0.5f, 0.5f)); Canvass.crosshair.transform.position = cameraRay.GetPoint(90); Canvass.crosshair.transform.LookAt(Camera.main.transform); } } else { Canvass.seekbar.gameObject.SetActive(false); Canvass.crosshair.gameObject.SetActive(false); } } //NOTE(Kristof): Controller specific behaviour { if (VRDevices.loadedControllerSet != VRDevices.LoadedControllerSet.NoControllers) { crosshair.enabled = false; crosshairTimer.enabled = false; } else { crosshair.enabled = true; crosshairTimer.enabled = true; } if (Input.mouseScrollDelta.y != 0) { Camera.main.fieldOfView = Mathf.Clamp(Camera.main.fieldOfView - Input.mouseScrollDelta.y * 5, 20, 120); } } Ray ray; //NOTE(Kristof): Deciding on which object the Ray will be based on { Ray cameraRay = Camera.main.ViewportPointToRay(new Vector2(0.5f, 0.5f)); Ray controllerRay = new Ray(); const ulong ulTriggerValue = (ulong)1 << 33; if (trackedControllerLeft.controllerState.ulButtonPressed == controllerLeftOldState.ulButtonPressed + ulTriggerValue) { controllerRay = controllerLeft.GetComponent <Controller>().CastRay(); } if (trackedControllerRight.controllerState.ulButtonPressed == controllerRightOldState.ulButtonPressed + ulTriggerValue) { controllerRay = controllerRight.GetComponent <Controller>().CastRay(); } controllerLeftOldState = trackedControllerLeft.controllerState; controllerRightOldState = trackedControllerRight.controllerState; if (VRDevices.loadedControllerSet > VRDevices.LoadedControllerSet.NoControllers) { ray = controllerRay; } else { ray = cameraRay; } } interacting = false; if (playerState == PlayerState.Watching) { if (Input.GetKeyDown(KeyCode.Space) && VRDevices.loadedSdk == VRDevices.LoadedSdk.None) { videoController.TogglePlay(); } //Note(Simon): Interaction with points { var reversedRay = ray; //Note(Simon): Create a reversed raycast to find positions on the sphere with reversedRay.origin = ray.GetPoint(100); reversedRay.direction = -ray.direction; RaycastHit hit; Physics.Raycast(reversedRay, out hit, 100, 1 << LayerMask.NameToLayer("interactionPoints")); var left = controllerLeft.GetComponent <Controller>(); var right = controllerRight.GetComponent <Controller>(); float forwardAngle; //Note(lander): Turn the blips with the correct angle. { if (left || right) { forwardAngle = right.compassAttached ? right.transform.eulerAngles.y : left.transform.eulerAngles.y; } else { forwardAngle = Seekbar.compass.transform.parent.localEulerAngles.y; } } //NOTE(Kristof): The startpoints are removed in the for loop, so we need to loop in reverse for (var i = interactionPoints.Count - 1; i >= 0; i--) { var point = interactionPoints[i]; var pointActive = point.startTime <= videoController.currentTime && point.endTime >= videoController.currentTime; point.point.SetActive(pointActive); var textMesh = point.point.GetComponentInChildren <TextMesh>(); // Note(Lander): highlight the untouched interaction points if (!point.isStartPoint && pointActive && !point.isTouched) { if (textMesh != null) { textMesh.color = Color.black; } var blipAngle = point.point.transform.eulerAngles.y; // TODO(Lander): Rely on a start position of a video instead var angle = (XRSettings.enabled ? forwardAngle : 90) - blipAngle; if (point.blip == null) { point.blip = Seekbar.CreateBlip(-angle, Instantiate(compassBlipPrefab)); remainingPoints++; } point.blip.transform.localEulerAngles = new Vector3(0, 0, angle); } else { if (textMesh != null) { textMesh.color = Color.white; } if (point.blip != null) { Destroy(point.blip); point.blip = null; remainingPoints--; } } if (!point.isStartPoint && point.isTouched) { point.point.GetComponent <Renderer>().material.color = GRAY; } blipCounter.text = remainingPoints != 0 ? remainingPoints.ToString() : ""; //NOTE(Lander): current point is hit with the raycast if (hit.transform != null && hit.transform.gameObject == point.point) { //NOTE(Kristof): Interacting with controller if (VRDevices.loadedControllerSet > VRDevices.LoadedControllerSet.NoControllers) { //NOTE(Kristof): The controllers only raycast on trigger down //NOTE(Kristof): Interacting with StartPoints if (point.isStartPoint) { videoController.videoState = VideoController.VideoState.Watching; startPointGroup.SetActive(false); Togglecanvasses(); interactionPoints.RemoveRange(0, 4); } //NOTE(Kristof): Interacting with InteractionPoints else { point.panel.SetActive(!point.panel.activeSelf); if (point.panel.activeSelf) { activePoints++; point.isTouched = true; } else { activePoints--; } videoController.Pause(); //NOTE(Kristof): Play the video when you deactivate the last point if (activePoints == 0 && VideoController.autoResume) { videoController.TogglePlay(); } } } //NOTE(Kristof): Interacting without controllers else { interacting = true; if (timeToInteract < interactionTimer) { //NOTE(Kristof): Interacting with StartPoints if (point.isStartPoint) { videoController.videoState = VideoController.VideoState.Watching; startPointGroup.SetActive(false); Togglecanvasses(); } //NOTE(Kristof): Interacting with InteractionPoints else { //NOTE(Kristof): Making a panel active if (!point.panel.activeSelf) { if (VRDevices.loadedSdk > VRDevices.LoadedSdk.None) { interactionTimer = -1; activePoints++; } else { //HACK(Kristof): Set to to double of timeToInteract to ensure no funky business happens (like disabling the panel right away) interactionTimer = timeToInteract * 2; } point.isTouched = true; point.panel.SetActive(true); videoController.Pause(); } //NOTE(Kristof): Making a panel inactive //NOTE This only needs to be the done the same frame that the interactiontimer exceeds the timeToInteract, on this frame point.interactionTimer //NOTE will be between timeToInteract and timeToInteract + deltaTime //NOTE(Kristof): This condition will occasionally cause bugs (see HACK above) else if (timeToInteract < interactionTimer && interactionTimer < timeToInteract + Time.deltaTime) { point.panel.SetActive(false); activePoints--; interactionTimer = -1; if (activePoints == 0 && VideoController.autoResume) { videoController.TogglePlay(); } } } } } } //NOTE(Kristof): Gets executed for the point.panels that the user made active before but are not currently being interacted with (no hit) else if (point.panel != null && point.panel.activeSelf) { //NOTE(Kristof): Disable all panels when using the Seekbar play button to resume play if (videoController.playing) { point.panel.SetActive(false); activePoints--; } //NOTE(Kristof): Video can resume if the user is not using VR if (VRDevices.loadedSdk == VRDevices.LoadedSdk.None) { point.panel.SetActive(false); videoController.TogglePlay(); } } } } } if (playerState == PlayerState.Opening) { var panel = indexPanel.GetComponent <IndexPanel>(); if (panel.answered) { var metaFilename = Path.Combine(Application.persistentDataPath, Path.Combine(panel.answerVideoId, SaveFile.metaFilename)); if (OpenFile(metaFilename)) { Destroy(indexPanel); playerState = PlayerState.Watching; Canvass.modalBackground.SetActive(false); if (VRDevices.loadedSdk > VRDevices.LoadedSdk.None) { EventManager.OnSpace(); videoPositions.Clear(); } } else { Debug.Log("Couldn't open savefile"); } } } //NOTE(Kristof): Interaction with UI { RaycastHit hit; Physics.Raycast(ray, out hit, 100, LayerMask.GetMask("UI", "WorldUI")); var controllerList = new List <Controller> { controllerLeft.GetComponent <Controller>(), controllerRight.GetComponent <Controller>() }; //NOTE(Kristof): Looping over hittable UI scripts foreach (var hittable in hittables) { if (hittable == null) { continue; } hittable.hitting = false; hittable.hovering = false; //NOTE(Kristof): Checking for controller hover needs to happen independently of controller interactions foreach (var con in controllerList) { if (con.uiHovering && con.hovered == hittable.gameObject) { hittable.hovering = true; } } if (hit.transform != null && hit.transform.gameObject == hittable.gameObject) { //NOTE(Kristof): Interacting with controller if (VRDevices.loadedControllerSet > VRDevices.LoadedControllerSet.NoControllers) { hittable.hitting = true; } //NOTE(Kristof): Interacting without controllers else { interacting = true; hittable.hovering = true; if (interactionTimer >= timeToInteract) { interactionTimer = -1; hittable.hitting = true; } } } } } //NOTE(Kristof): Interaction interactionTimer and Crosshair behaviour { if (interacting) { interactionTimer += Time.deltaTime; crosshairTimer.fillAmount = interactionTimer / timeToInteract; crosshair.fillAmount = 1 - (interactionTimer / timeToInteract); } else { interactionTimer = 0; crosshairTimer.fillAmount = 0; crosshair.fillAmount = 1; } } //NOTE(Kristof): Turning CameraRig { var controllers = new[] { controllerLeft.GetComponent <SteamVR_TrackedObject>(), controllerRight.GetComponent <SteamVR_TrackedObject>() }; for (var index = 0; index < controllers.Length; index++) { var controller = controllers[index]; if (controller.index > SteamVR_TrackedObject.EIndex.None) { var device = SteamVR_Controller.Input((int)controller.index); switch (VRDevices.loadedControllerSet) { case VRDevices.LoadedControllerSet.Oculus: { var touchpad = device.GetAxis(); if (-0.7f < touchpad.x && touchpad.x < 0.7f) { cameraRigMovable[index] = true; } else if (touchpad.x > 0.7f && cameraRigMovable[index]) { cameraRig.transform.localEulerAngles += new Vector3(0, 30, 0); cameraRigMovable[index] = false; } else if (touchpad.x < -0.7f && cameraRigMovable[index]) { cameraRig.transform.localEulerAngles -= new Vector3(0, 30, 0); cameraRigMovable[index] = false; } break; } case VRDevices.LoadedControllerSet.Vive: { var touchpad = device.GetAxis(); if (device.GetPressDown(SteamVR_Controller.ButtonMask.Touchpad)) { if (touchpad.x > 0.7f && cameraRigMovable[index]) { cameraRig.transform.localEulerAngles += new Vector3(0, 30, 0); cameraRigMovable[index] = false; } else if (touchpad.x < -0.7f && cameraRigMovable[index]) { cameraRig.transform.localEulerAngles -= new Vector3(0, 30, 0); cameraRigMovable[index] = false; } } else { cameraRigMovable[index] = true; } break; } } } } } }
void Update() { //NOTE(Kristof): VR specific behaviour { if (XRSettings.enabled) { videoController.transform.position = Camera.main.transform.position; Canvass.seekbar.gameObject.SetActive(true); //NOTE(Kristof): Rotating the seekbar { //NOTE(Kristof): Seekbar rotation is the same as the seekbar's angle on the circle var seekbarAngle = Vector2.SignedAngle(new Vector2(Canvass.seekbar.transform.position.x, Canvass.seekbar.transform.position.z), Vector2.up); var fov = Camera.main.fieldOfView; //NOTE(Kristof): Camera rotation tells you to which angle on the circle the camera is looking towards var cameraAngle = Camera.main.transform.eulerAngles.y; //NOTE(Kristof): Calculate the absolute degree angle from the camera to the seekbar var distanceLeft = Mathf.Abs((cameraAngle - seekbarAngle + 360) % 360); var distanceRight = Mathf.Abs((cameraAngle - seekbarAngle - 360) % 360); var angle = Mathf.Min(distanceLeft, distanceRight); if (isSeekbarOutOfView) { if (angle < 2.5f) { isSeekbarOutOfView = false; } } else { if (angle > fov) { isSeekbarOutOfView = true; } } if (isSeekbarOutOfView) { float newAngle = Mathf.LerpAngle(seekbarAngle, cameraAngle, 0.025f); //NOTE(Kristof): Angle needs to be reversed, in Unity postive angles go clockwise while they go counterclockwise in the unit circle (cos and sin) //NOTE(Kristof): We also need to add an offset of 90 degrees because in Unity 0 degrees is in front of you, in the unit circle it is (1,0) on the axis float radianAngle = (-newAngle + 90) * Mathf.PI / 180; float x = 1.8f * Mathf.Cos(radianAngle); float y = Camera.main.transform.position.y - 2f; float z = 1.8f * Mathf.Sin(radianAngle); Canvass.seekbar.transform.position = new Vector3(x, y, z); Canvass.seekbar.transform.eulerAngles = new Vector3(30, newAngle, 0); } } } else { Canvass.seekbar.gameObject.SetActive(false); } } //NOTE(Simon): Zoom when not using HMD if (!XRSettings.enabled) { if (Input.mouseScrollDelta.y != 0) { Camera.main.fieldOfView = Mathf.Clamp(Camera.main.fieldOfView - Input.mouseScrollDelta.y * 5, 20, 120); } } Ray interactionpointRay = new Ray(); //NOTE(Kristof): Deciding on which object the Ray will be based on //TODO(Simon): Prefers right over left controller { if (trackedControllerLeft != null && trackedControllerLeft.triggerPressed) { interactionpointRay = trackedControllerLeft.CastRay(); } if (trackedControllerRight != null && trackedControllerRight.triggerPressed) { interactionpointRay = trackedControllerRight.CastRay(); } if (VRDevices.loadedControllerSet == VRDevices.LoadedControllerSet.NoControllers && Input.GetMouseButtonUp(0)) { interactionpointRay = Camera.main.ScreenPointToRay(Input.mousePosition); } } if (playerState == PlayerState.Watching) { if (Input.GetKeyDown(KeyCode.Space) && VRDevices.loadedSdk == VRDevices.LoadedSdk.None) { videoController.TogglePlay(); } Seekbar.instance.RenderBlips(interactionPoints, trackedControllerLeft, trackedControllerRight); //Note(Simon): Interaction with points { var reversedRay = interactionpointRay.ReverseRay(); //Note(Simon): Create a reversed raycast to find positions on the sphere with Physics.Raycast(reversedRay, out var hit, 100, 1 << LayerMask.NameToLayer("interactionPoints")); //NOTE(Simon): Update visible interactionpoints for (int i = 0; i < interactionPoints.Count; i++) { bool pointActive = videoController.currentTime >= interactionPoints[i].startTime && videoController.currentTime <= interactionPoints[i].endTime; interactionPoints[i].point.SetActive(pointActive); interactionPoints[i].point.GetComponentInChildren <MeshRenderer>(includeInactive: true).gameObject.SetActive(!interactionPoints[i].isSeen); } //NOTE(Simon): Interact with inactive interactionpoints if (activeInteractionPoint == null && hit.transform != null) { var pointGO = hit.transform.gameObject; InteractionPointPlayer point = null; for (int i = 0; i < interactionPoints.Count; i++) { if (pointGO == interactionPoints[i].point) { point = interactionPoints[i]; break; } } ActivateInteractionPoint(point); } //NOTE(Simon): Disable active interactionPoint if playback was started through seekbar if (videoController.playing && activeInteractionPoint != null) { DeactivateActiveInteractionPoint(); } } //NOTE(Simon): Handle mandatory interactionPoints { double timeToNextPause = Double.MaxValue; var interactionsInChapter = MandatoryInteractionsForTime(videoController.currentTime); //NOTE(Simon): Find the next unseen mandatory interaction in this chapter for (int i = 0; i < interactionsInChapter.Count; i++) { if (!interactionsInChapter[i].isSeen && interactionsInChapter[i].endTime > videoController.currentTime) { timeToNextPause = interactionsInChapter[i].endTime - videoController.currentTime; break; } } //NOTE(Simon): Set the playbackspeed. Speed will get lower the closer to the next pause we are. if (timeToNextPause < mandatoryPauseFadeTime) { float speed = (float)(timeToNextPause / mandatoryPauseFadeTime); if (timeToNextPause < .05f) { speed = 0; } videoController.SetPlaybackSpeed(speed); if (!mandatoryPauseActive) { ShowMandatoryInteractionMessage(); mandatoryPauseActive = true; } } else { if (mandatoryPauseActive) { mandatoryPauseActive = false; HideMandatoryInteractionMessage(); } videoController.SetPlaybackSpeed(1f); } } } if (playerState == PlayerState.Opening) { var panel = indexPanel.GetComponent <IndexPanel>(); if (panel.answered) { var projectPath = Path.Combine(Application.persistentDataPath, panel.answerVideoId); if (OpenFile(projectPath)) { Destroy(indexPanel); playerState = PlayerState.Watching; Canvass.modalBackground.SetActive(false); SetCanvasesActive(true); chapterSelector = Instantiate(chapterSelectorPrefab, Canvass.main.transform, false).GetComponent <ChapterSelectorPanel>(); if (VRDevices.loadedSdk > VRDevices.LoadedSdk.None) { StartCoroutine(FadevideoCanvasOut(videoCanvas)); EventManager.OnSpace(); videoPositions.Clear(); Seekbar.ReattachCompass(); } } else { Debug.LogError("Couldn't open savefile"); } } } //NOTE(Simon): Interaction with Hittables { Physics.Raycast(interactionpointRay, out var hit, 100, LayerMask.GetMask("UI", "WorldUI")); var controllerList = new List <Controller> { trackedControllerLeft, trackedControllerRight }; //NOTE(Simon): Reset all hittables foreach (var hittable in hittables) { if (hittable == null) { continue; } //NOTE(Jitse): Check if a hittable is being held down if (!(controllerList[0].triggerDown || controllerList[1].triggerDown)) { hittable.hitting = false; } hittable.hovering = false; } //NOTE(Simon): Set hover state when hovered by controllers foreach (var con in controllerList) { if (con.uiHovering && con.hoveredGo != null) { var hittable = con.hoveredGo.GetComponent <Hittable>(); if (hittable != null) { hittable.hovering = true; } } } //NOTE(Simon): Set hitting and hovering in hittables if (hit.transform != null) { hit.transform.GetComponent <Hittable>().hitting = true; } } }