1,此中有真意,欲辨已忘言。
Google Cardboard中一个脚本
// Copyright 2014 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
/// @ingroup Scripts
/// This class is the main Cardboard SDK object.
///
/// The Cardboard object communicates with the head-mounted display in order to:
/// - Query the device for viewing parameters
/// - Retrieve the latest head tracking data
/// - Provide the rendered scene to the device for distortion correction (optional)
///
/// There should only be one of these in a scene. An instance will be generated automatically
/// by this script at runtime, or you can add one via the Editor if you wish to customize
/// its starting properties.
[AddComponentMenu("Cardboard/Cardboard")]
public class Cardboard : MonoBehaviour {
// Cardboard SDK Version
public const string CARDBOARD_SDK_VERSION = "0.7";
/// The singleton instance of the Cardboard class.
public static Cardboard SDK {
get {
#if UNITY_EDITOR
if (sdk == null && !Application.isPlaying) {
sdk = UnityEngine.Object.FindObjectOfType<Cardboard>();
}
#endif
if (sdk == null) {
Debug.LogError("No Cardboard instance found. Ensure one exists in the scene, or call "
+ "Cardboard.Create() at startup to generate one.\n"
+ "If one does exist but hasn't called Awake() yet, "
+ "then this error is due to order-of-initialization.\n"
+ "In that case, consider moving "
+ "your first reference to Cardboard.SDK to a later point in time.\n"
+ "If exiting the scene, this indicates that the Cardboard object has already "
+ "been destroyed.");
}
return sdk;
}
}
private static Cardboard sdk = null;
/// Generate a Cardboard instance. Takes no action if one already exists.
public static void Create() {
if (sdk == null && UnityEngine.Object.FindObjectOfType<Cardboard>() == null) {
Debug.Log("Creating Cardboard object");
var go = new GameObject("Cardboard", typeof(Cardboard));
go.transform.localPosition = Vector3.zero;
// sdk will be set by Cardboard.Awake().
}
}
/// The StereoController instance attached to the main camera, or null if there is none.
/// @note Cached for performance.
public static StereoController Controller {
get {
Camera camera = Camera.main;
// Cache for performance, if possible.
if (camera != currentMainCamera || currentController == null) {
currentMainCamera = camera;
currentController = camera.GetComponent<StereoController>();
}
return currentController;
}
}
private static Camera currentMainCamera;
private static StereoController currentController;
/// @cond
public bool UILayerEnabled {
get {
return uiLayerEnabled;
}
private set {
if (value != uiLayerEnabled && device != null) {
device.SetUILayerEnabled(value);
}
uiLayerEnabled = value;
}
}
// Not serialized.
private bool uiLayerEnabled = false;
/// @endcond
/// Determine whether the scene renders in stereo or mono.
/// _True_ means to render in stereo, and _false_ means to render in mono.
public bool VRModeEnabled {
get {
return vrModeEnabled;
}
set {
if (value != vrModeEnabled && device != null) {
device.SetVRModeEnabled(value);
}
vrModeEnabled = value;
}
}
[SerializeField]
private bool vrModeEnabled = true;
/// Methods for performing lens distortion correction.
public enum DistortionCorrectionMethod {
None, /// No distortion correction
Native, /// Use the native C++ plugin
Unity, /// Perform distortion correction in Unity (recommended)
}
/// Determines the distortion correction method used by the SDK to render the
/// #StereoScreen texture on the phone. If _Native_ is selected but not supported
/// by the device, the _Unity_ method will be used instead.
public DistortionCorrectionMethod DistortionCorrection {
get {
return distortionCorrection;
}
set {
if (device != null && device.RequiresNativeDistortionCorrection()) {
value = DistortionCorrectionMethod.Native;
}
if (value != distortionCorrection && device != null) {
device.SetDistortionCorrectionEnabled(value == DistortionCorrectionMethod.Native
&& NativeDistortionCorrectionSupported);
device.UpdateScreenData();
}
distortionCorrection = value;
}
}
[SerializeField]
private DistortionCorrectionMethod distortionCorrection = DistortionCorrectionMethod.Unity;
/// Enables or disables the vertical line rendered between the stereo views to
/// help the user align the Cardboard to the phone's screen.
public bool EnableAlignmentMarker {
get {
return enableAlignmentMarker;
}
set {
if (value != enableAlignmentMarker && device != null) {
device.SetAlignmentMarkerEnabled(value);
}
enableAlignmentMarker = value;
}
}
[SerializeField]
private bool enableAlignmentMarker = true;
/// Enables or disables the Cardboard settings button. It appears as a gear icon
/// in the blank space between the stereo views. The settings button opens the
/// Google Cardboard app to allow the user to configure their individual settings
/// and Cardboard headset parameters.
public bool EnableSettingsButton {
get {
return enableSettingsButton;
}
set {
if (value != enableSettingsButton && device != null) {
device.SetSettingsButtonEnabled(value);
}
enableSettingsButton = value;
}
}
[SerializeField]
private bool enableSettingsButton = true;
/// Display modes for the VR "Back Button".
public enum BackButtonModes {
Off, /// Always off
OnlyInVR, /// On in VR Mode, otherwise off
On /// Always on
}
/// Whether to show the onscreen analog of the (Android) Back Button.
public BackButtonModes BackButtonMode {
get {
return backButtonMode;
}
set {
if (value != backButtonMode && device != null) {
device.SetVRBackButtonEnabled(value != BackButtonModes.Off);
device.SetShowVrBackButtonOnlyInVR(value == BackButtonModes.OnlyInVR);
}
backButtonMode = value;
}
}
[SerializeField]
private BackButtonModes backButtonMode = BackButtonModes.OnlyInVR;
/// The native SDK will apply a neck offset to the head tracking, resulting in
/// a more realistic model of a person's head position. This control determines
/// the scale factor of the offset. To turn off the neck model, set it to 0, and
/// to turn it all on, set to 1. Intermediate values can be used to animate from
/// on to off or vice versa.
public float NeckModelScale {
get {
return neckModelScale;
}
set {
value = Mathf.Clamp01(value);
if (!Mathf.Approximately(value, neckModelScale) && device != null) {
device.SetNeckModelScale(value);
}
neckModelScale = value;
}
}
[SerializeField]
private float neckModelScale = 0.0f;
/// When enabled, drift in the gyro readings is estimated and removed.
public bool AutoDriftCorrection {
get {
return autoDriftCorrection;
}
set {
if (value != autoDriftCorrection && device != null) {
device.SetAutoDriftCorrectionEnabled(value);
}
autoDriftCorrection = value;
}
}
[SerializeField]
private bool autoDriftCorrection = true;
/// @cond
public bool ElectronicDisplayStabilization {
get {
return electronicDisplayStabilization;
}
set {
if (value != electronicDisplayStabilization && device != null) {
device.SetElectronicDisplayStabilizationEnabled(value);
}
electronicDisplayStabilization = value;
}
}
[SerializeField]
private bool electronicDisplayStabilization = false;
/// @endcond
#if UNITY_EDITOR
/// Restores level head tilt in when playing in the Unity Editor after you
/// release the Ctrl key.
public bool autoUntiltHead = true;
/// @cond
/// Use unity remote as the input source.
[HideInInspector]
public bool UseUnityRemoteInput = false;
/// @endcond
/// The screen size to emulate when testing in the Unity Editor.
public CardboardProfile.ScreenSizes ScreenSize {
get {
return screenSize;
}
set {
if (value != screenSize) {
screenSize = value;
if (device != null) {
device.UpdateScreenData();
}
}
}
}
[SerializeField]
private CardboardProfile.ScreenSizes screenSize = CardboardProfile.ScreenSizes.Nexus5;
/// The device type to emulate when testing in the Unity Editor.
public CardboardProfile.DeviceTypes DeviceType {
get {
return deviceType;
}
set {
if (value != deviceType) {
deviceType = value;
if (device != null) {
device.UpdateScreenData();
}
}
}
}
[SerializeField]
private CardboardProfile.DeviceTypes deviceType = CardboardProfile.DeviceTypes.CardboardMay2015;
#endif
// The VR device that will be providing input data.
private static BaseVRDevice device;
/// Whether native distortion correction functionality is supported by the VR device.
public bool NativeDistortionCorrectionSupported { get; private set; }
/// Whether the VR device supports showing a native UI layer, for example for settings.
public bool NativeUILayerSupported { get; private set; }
/// Scales the resolution of the #StereoScreen. Set to less than 1.0 to increase
/// rendering speed while decreasing sharpness, or greater than 1.0 to do the
/// opposite.
public float StereoScreenScale {
get {
return stereoScreenScale;
}
set {
value = Mathf.Clamp(value, 0.1f, 10.0f); // Sanity.
if (stereoScreenScale != value) {
stereoScreenScale = value;
StereoScreen = null;
}
}
}
[SerializeField]
private float stereoScreenScale = 1;
/// The texture that Unity renders the scene to. After the frame has been rendered,
/// this texture is drawn to the screen with a lens distortion correction effect.
/// The texture size is based on the size of the screen, the lens distortion
/// parameters, and the #StereoScreenScale factor.
public RenderTexture StereoScreen {
get {
// Don't need it except for distortion correction.
if (distortionCorrection == DistortionCorrectionMethod.None || !vrModeEnabled) {
return null;
}
if (stereoScreen == null) {
// Create on demand.
StereoScreen = device.CreateStereoScreen(); // Note: uses set{}
}
return stereoScreen;
}
set {
if (value == stereoScreen) {
return;
}
if (stereoScreen != null) {
stereoScreen.Release();
}
stereoScreen = value;
if (OnStereoScreenChanged != null) {
OnStereoScreenChanged(stereoScreen);
}
}
}
private static RenderTexture stereoScreen = null;
/// A callback for notifications that the StereoScreen property has changed.
public delegate void StereoScreenChangeDelegate(RenderTexture newStereoScreen);
/// Emitted when the StereoScreen property has changed.
public event StereoScreenChangeDelegate OnStereoScreenChanged;
/// Describes the current device, including phone screen.
public CardboardProfile Profile {
get {
return device.Profile;
}
}
/// Distinguish the stereo eyes.
public enum Eye {
Left, /// The left eye
Right, /// The right eye
Center /// The "center" eye (unused)
}
/// When retrieving the #Projection and #Viewport properties, specifies
/// whether you want the values as seen through the Cardboard lenses (`Distorted`) or
/// as if no lenses were present (`Undistorted`).
public enum Distortion {
Distorted, /// Viewing through the lenses
Undistorted /// No lenses
}
/// The transformation of head from origin in the tracking system.
public Pose3D HeadPose {
get {
return device.GetHeadPose();
}
}
/// The transformation from head to eye.
public Pose3D EyePose(Eye eye) {
return device.GetEyePose(eye);
}
/// The projection matrix for a given eye.
/// This matrix is an off-axis perspective projection with near and far
/// clipping planes of 1m and 1000m, respectively. The CardboardEye script
/// takes care of adjusting the matrix for its particular camera.
public Matrix4x4 Projection(Eye eye, Distortion distortion = Distortion.Distorted) {
return device.GetProjection(eye, distortion);
}
/// The screen space viewport that the camera for the specified eye should render into.
/// In the _Distorted_ case, this will be either the left or right half of the `StereoScreen`
/// render texture. In the _Undistorted_ case, it refers to the actual rectangle on the
/// screen that the eye can see.
public Rect Viewport(Eye eye, Distortion distortion = Distortion.Distorted) {
return device.GetViewport(eye, distortion);
}
/// The distance range from the viewer in user-space meters where objects may be viewed
/// comfortably in stereo. If the center of interest falls outside this range, the stereo
/// eye separation should be adjusted to keep the onscreen disparity within the limits set
/// by this range. StereoController will handle this if the _checkStereoComfort_ is
/// enabled.
public Vector2 ComfortableViewingRange {
get {
return defaultComfortableViewingRange;
}
}
private readonly Vector2 defaultComfortableViewingRange = new Vector2(0.4f, 100000.0f);
/// @cond
// Optional. Set to a URI obtained from the Google Cardboard profile generator at
// https://www.google.com/get/cardboard/viewerprofilegenerator/
// Example: Cardboard I/O 2015 viewer profile
//public Uri DefaultDeviceProfile = new Uri("http://google.com/cardboard/cfg?p=CgZHb29nbGUSEkNhcmRib2FyZCBJL08gMjAxNR0J-SA9JQHegj0qEAAAcEIAAHBCAABwQgAAcEJYADUpXA89OghX8as-YrENP1AAYAM");
public Uri DefaultDeviceProfile = null;
/// @endcond
private void InitDevice() {
if (device != null) {
device.Destroy();
}
device = BaseVRDevice.GetDevice();
device.Init();
List<string> diagnostics = new List<string>();
NativeDistortionCorrectionSupported = device.SupportsNativeDistortionCorrection(diagnostics);
if (diagnostics.Count > 0) {
Debug.LogWarning("Built-in distortion correction disabled. Causes: ["
+ String.Join("; ", diagnostics.ToArray()) + "]");
}
diagnostics.Clear();
NativeUILayerSupported = device.SupportsNativeUILayer(diagnostics);
if (diagnostics.Count > 0) {
Debug.LogWarning("Built-in UI layer disabled. Causes: ["
+ String.Join("; ", diagnostics.ToArray()) + "]");
}
if (DefaultDeviceProfile != null) {
device.SetDefaultDeviceProfile(DefaultDeviceProfile);
}
device.SetAlignmentMarkerEnabled(enableAlignmentMarker);
device.SetSettingsButtonEnabled(enableSettingsButton);
device.SetVRBackButtonEnabled(backButtonMode != BackButtonModes.Off);
device.SetShowVrBackButtonOnlyInVR(backButtonMode == BackButtonModes.OnlyInVR);
device.SetDistortionCorrectionEnabled(distortionCorrection == DistortionCorrectionMethod.Native
&& NativeDistortionCorrectionSupported);
device.SetNeckModelScale(neckModelScale);
device.SetAutoDriftCorrectionEnabled(autoDriftCorrection);
device.SetElectronicDisplayStabilizationEnabled(electronicDisplayStabilization);
device.SetVRModeEnabled(vrModeEnabled);
device.UpdateScreenData();
}
/// @note Each scene load causes an OnDestroy of the current SDK, followed
/// by and Awake of a new one. That should not cause the underlying native
/// code to hiccup. Exception: developer may call Application.DontDestroyOnLoad
/// on the SDK if they want it to survive across scene loads.
void Awake() {
if (sdk == null) {
sdk = this;
}
if (sdk != this) {
Debug.LogError("There must be only one Cardboard object in a scene.");
UnityEngine.Object.DestroyImmediate(this);
return;
}
#if UNITY_IOS
Application.targetFrameRate = 60;
#endif
// Prevent the screen from dimming / sleeping
Screen.sleepTimeout = SleepTimeout.NeverSleep;
InitDevice();
StereoScreen = null;
AddCardboardCamera();
}
void Start() {
UILayerEnabled = true;
}
void AddCardboardCamera() {
var preRender = UnityEngine.Object.FindObjectOfType<CardboardPreRender>();
if (preRender == null) {
var go = new GameObject("PreRender", typeof(CardboardPreRender));
go.SendMessage("Reset");
go.transform.parent = transform;
}
var postRender = UnityEngine.Object.FindObjectOfType<CardboardPostRender>();
if (postRender == null) {
var go = new GameObject("PostRender", typeof(CardboardPostRender));
go.SendMessage("Reset");
go.transform.parent = transform;
}
}
/// Emitted whenever a trigger occurs.
public event Action OnTrigger;
/// Emitted whenever the viewer is tilted on its side.
public event Action OnTilt;
/// Emitted whenever the app should respond to a possible change in the device viewer
/// profile, that is, the QR code scanned by the user.
public event Action OnProfileChange;
/// Emitted whenever the user presses the "VR Back Button".
public event Action OnBackButton;
/// Whether the Cardboard trigger was pulled. True for exactly one complete frame
/// after each pull.
public bool Triggered { get; private set; }
/// Whether the Cardboard viewer was tilted on its side. True for exactly one complete frame
/// after each tilt. Whether and how to respond to this event is up to the app.
public bool Tilted { get; private set; }
/// Whether the Cardboard device profile has possibly changed. This is meant to indicate
/// that a new QR code has been scanned, although currently it is actually set any time the
/// application is unpaused, whether it was due to a profile change or not. True for one
/// frame.
public bool ProfileChanged { get; private set; }
/// Whether the user has pressed the "VR Back Button", which on Android should be treated the
/// same as the normal system Back Button, although you can respond to either however you want
/// in your app.
public bool BackButtonPressed { get; private set; }
// Only call device.UpdateState() once per frame.
private int updatedToFrame = 0;
/// Reads the latest tracking data from the phone. This must be
/// called before accessing any of the poses and matrices above.
///
/// Multiple invocations per frame are OK: Subsequent calls merely yield the
/// cached results of the first call. To minimize latency, it should be first
/// called later in the frame (for example, in `LateUpdate`) if possible.
public void UpdateState() {
if (updatedToFrame != Time.frameCount) {
updatedToFrame = Time.frameCount;
device.UpdateState();
if (device.profileChanged) {
if (distortionCorrection != DistortionCorrectionMethod.Native
&& device.RequiresNativeDistortionCorrection()) {
DistortionCorrection = DistortionCorrectionMethod.Native;
}
if (stereoScreen != null
&& device.ShouldRecreateStereoScreen(stereoScreen.width, stereoScreen.height)) {
StereoScreen = null;
}
}
DispatchEvents();
}
}
private void DispatchEvents() {
// Update flags first by copying from device and other inputs.
Triggered = device.triggered || Input.GetMouseButtonDown(0);
Tilted = device.tilted;
ProfileChanged = device.profileChanged;
BackButtonPressed = device.backButtonPressed || Input.GetKeyDown(KeyCode.Escape);
// Reset device flags.
device.triggered = false;
device.tilted = false;
device.profileChanged = false;
device.backButtonPressed = false;
// All flags updated. Now emit events.
if (Tilted && OnTilt != null) {
OnTilt();
}
if (Triggered && OnTrigger != null) {
OnTrigger();
}
if (ProfileChanged && OnProfileChange != null) {
OnProfileChange();
}
if (BackButtonPressed && OnBackButton != null) {
OnBackButton();
}
}
/// Presents the #StereoScreen to the device for distortion correction and display.
/// @note This function is only used if #DistortionCorrection is set to _Native_,
/// and it only has an effect if the device supports it.
public void PostRender(RenderTexture stereoScreen) {
if (NativeDistortionCorrectionSupported && stereoScreen != null && stereoScreen.IsCreated()) {
device.PostRender(stereoScreen);
}
}
/// Resets the tracker so that the user's current direction becomes forward.
public void Recenter() {
device.Recenter();
}
/// Launch the device pairing and setup dialog.
public void ShowSettingsDialog() {
device.ShowSettingsDialog();
}
void OnEnable() {
#if UNITY_EDITOR
// This can happen if you edit code while the editor is in Play mode.
if (device == null) {
InitDevice();
}
#endif
device.OnPause(false);
}
void OnDisable() {
device.OnPause(true);
}
void OnApplicationPause(bool pause) {
device.OnPause(pause);
}
void OnApplicationFocus(bool focus) {
device.OnFocus(focus);
}
void OnLevelWasLoaded(int level) {
device.OnLevelLoaded(level);
}
void OnApplicationQuit() {
device.OnApplicationQuit();
}
void OnDestroy() {
VRModeEnabled = false;
UILayerEnabled = false;
if (device != null) {
device.Destroy();
}
if (sdk == this) {
sdk = null;
}
}
}
// Copyright 2014 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using UnityEngine;
using System.Collections;
using System.Linq;
/// @ingroup Scripts
/// Controls a pair of CardboardEye objects that will render the stereo view
/// of the camera this script is attached to.
///
/// This script must be added to any camera that should render stereo when the app
/// is in VR Mode. This includes picture-in-picture windows, whether their contents
/// are in stereo or not: the window itself must be twinned for stereo, regardless.
///
/// For each frame, StereoController decides whether to render via the camera it
/// is attached to (the _mono_ camera) or the stereo eyes that it controls (see
/// CardboardEye). You control this decision for all cameras at once by setting
/// the value of Cardboard#VRModeEnabled.
///
/// For technical reasons, the mono camera remains enabled for the initial portion of
/// the frame. It is disabled only when rendering begins in `OnPreCull()`, and is
/// reenabled again at the end of the frame. This allows 3rd party scripts that use
/// `Camera.main`, for example, to refer the the mono camera even when VR Mode is
/// enabled.
///
/// At startup the script ensures it has a full stereo rig, which consists of two
/// child cameras with CardboardEye scripts attached, and a CardboardHead script
/// somewhere in the hierarchy of parents and children for head tracking. The rig
/// is created if necessary, the CardboardHead being attached to the controller
/// itself. The child camera settings are then cloned or updated from the mono
/// camera.
///
/// It is permissible for a StereoController to contain another StereoController
/// as a child. In this case, a CardboardEye is controlled by its closest
/// StereoController parent.
///
/// The Inspector panel for this script includes a button _Update Stereo Cameras_.
/// This performs the same action as described above for startup, but in the Editor.
/// Use this to generate the rig if you intend to customize it. This action is also
/// available via _Component -> Cardboard -> Update Stereo Cameras_ in the Editor’s
/// main menu, and in the context menu for the `Camera` component.
[RequireComponent(typeof(Camera))]
[AddComponentMenu("Cardboard/StereoController")]
public class StereoController : MonoBehaviour {
/// Whether to draw directly to the output window (_true_), or to an offscreen buffer
/// first and then blit (_false_). If you wish to use Deferred Rendering or any
/// Image Effects in stereo, turn this option off. A common symptom that indicates
/// you should do so is when one of the eyes is spread across the entire screen.
[Tooltip("Whether to draw directly to the output window (true), or " +
"to an offscreen buffer first and then blit (false). Image " +
" Effects and Deferred Lighting may only work if set to false.")]
public bool directRender = true;
/// When enabled, UpdateStereoValues() is called every frame to keep the stereo cameras
/// completely synchronized with both the mono camera and the device profile. When
/// disabled, you must call UpdateStereoValues() whenever you make a change to the mono
/// camera that should be mirrored to the stereo cameras. Changes to the device profile
/// are handled automatically. It is better for performance to leave this option disabled
/// whenever possible. Good use cases for enabling it are when animating values on the
/// mono camera (like background color), or during development to debug camera synchronization
/// issues.
[Tooltip("When enabled, UpdateStereoValues() is called every frame to keep the stereo cameras " +
"completely synchronized with both the mono camera and the device profile. It is " +
"better for performance to leave this option disabled whenever possible.")]
public bool keepStereoUpdated = false;
/// Adjusts the level of stereopsis for this stereo rig.
/// @note This parameter is not the virtual size of the head -- use a scale
/// on the head game object for that. Instead, it is a control on eye vergence,
/// or rather, how cross-eyed or not the stereo rig is. Set to 0 to turn
/// off stereo in this rig independently of any others.
[Tooltip("Set the stereo level for this camera.")]
[Range(0,1)]
public float stereoMultiplier = 1.0f;
/// The stereo cameras by default use the actual optical FOV of the Cardboard device,
/// because otherwise the match between head motion and scene motion is broken, which
/// impacts the virtual reality effect. However, in some cases it is desirable to
/// adjust the FOV anyway, for special effects or artistic reasons. But in no case
/// should the FOV be allowed to remain very different from the true optical FOV for
/// very long, or users will experience discomfort.
///
/// This value determines how much to match the mono camera's field of view. This is
/// a fraction: 0 means no matching, 1 means full matching, and values in between are
/// compromises. Reasons for not matching 100% would include preserving some VR-ness,
/// and that due to the lens distortion the edges of the view are not as easily seen as
/// when the phone is not in VR-mode.
///
/// Another use for this variable is to preserve scene composition against differences
/// in the optical FOV of various Cardboard models. In all cases, this value simply
/// lets the mono camera have some control over the scene in VR mode, like it does in
/// non-VR mode.
[Tooltip("How much to adjust the stereo field of view to match this camera.")]
[Range(0,1)]
public float matchMonoFOV = 0;
/// Determines the method by which the stereo cameras' FOVs are matched to the mono
/// camera's FOV (assuming #matchMonoFOV is not 0). The default is to move the stereo
/// cameras (#matchByZoom = 0), with the option to instead do a simple camera zoom
/// (#matchByZoom = 1). In-between values yield a mix of the two behaviors.
///
/// It is not recommended to use simple zooming for typical scene composition, as it
/// conflicts with the VR need to match the user's head motion with the corresponding
/// scene motion. This should be reserved for special effects such as when the player
/// views the scene through a telescope or other magnifier (and thus the player knows
/// that VR is going to be affected), or similar situations.
///
/// @note Matching by moving the eyes requires that the #centerOfInterest object
/// be non-null, or there will be no effect.
[Tooltip("Whether to adjust FOV by moving the eyes (0) or simply zooming (1).")]
[Range(0,1)]
public float matchByZoom = 0;
/// Matching the mono camera's field of view in stereo by moving the eyes requires
/// a designated "center of interest". This is either a point in space (an empty
/// gameobject) you place in the scene as a sort of "3D cursor", or an actual scene
/// entity which the player is likely to be focussed on.
///
/// The FOV adjustment is done by moving the eyes toward or away from the COI
/// so that it appears to have the same size on screen as it would in the mono
/// camera. This is disabled if the COI is null.
[Tooltip("Object or point where field of view matching is done.")]
public Transform centerOfInterest;
/// The #centerOfInterest is generally meant to be just a point in space, like a 3D cursor.
/// Occasionally, you will want it to be an actual object with size. Set this
/// to the approximate radius of the object to help the FOV-matching code
/// compensate for the object's horizon when it is close to the camera.
[Tooltip("If COI is an object, its approximate size.")]
public float radiusOfInterest = 0;
/// If true, check that the #centerOfInterest is between the min and max comfortable
/// viewing distances (see Cardboard.cs), or else adjust the stereo multiplier to
/// compensate. If the COI has a radius, then the near side is checked. COI must
/// be non-null for this setting to have any effect.
[Tooltip("Adjust stereo level when COI gets too close or too far.")]
public bool checkStereoComfort = true;
/// Smoothes the changes to the stereo camera FOV and position based on #centerOfInterest
/// and #checkStereoComfort.
[Tooltip("Smoothing factor to use when adjusting stereo for COI and comfort.")]
[Range(0,1)]
public float stereoAdjustSmoothing = 0.1f;
/// For picture-in-picture cameras that don't fill the entire screen,
/// set the virtual depth of the window itself. A value of 0 means
/// zero parallax, which is fairly close. A value of 1 means "full"
/// parallax, which is equal to the interpupillary distance and equates
/// to an infinitely distant window. This does not affect the actual
/// screen size of the the window (in pixels), only the stereo separation
/// of the left and right images.
[Tooltip("Adjust the virtual depth of this camera's window (picture-in-picture only).")]
[Range(0,1)]
public float screenParallax = 0;
/// For picture-in-picture cameras, move the window away from the edges
/// in VR Mode to make it easier to see. The optics of HMDs make the screen
/// edges hard to see sometimes, so you can use this to keep the PIP visible
/// whether in VR Mode or not. The x value is the fraction of the screen along
/// either side to pad.
[Tooltip("Move the camera window horizontally towards the center of the screen (PIP only).")]
[Range(0,1)]
public float stereoPaddingX = 0;
/// For picture-in-picture cameras, move the window away from the edges
/// in VR Mode to make it easier to see. The optics of HMDs make the screen
/// edges hard to see sometimes, so you can use this to keep the PIP visible
/// whether in VR Mode or not. The y value is for the top and bottom of the screen to pad.
[Tooltip("Move the camera window vertically towards the center of the screen (PIP only).")]
[Range(0,1)]
public float stereoPaddingY = 0;
// Flags whether we rendered in stereo for this frame.
private bool renderedStereo = false;
#if !UNITY_EDITOR
// Cache for speed, except in editor (don't want to get out of sync with the scene).
private CardboardEye[] eyes;
private CardboardHead head;
#endif
/// Returns an array of stereo cameras that are controlled by this instance of
/// the script.
/// @note This array is cached for speedier access. Call
/// InvalidateEyes if it is ever necessary to reset the cache.
public CardboardEye[] Eyes {
get {
#if UNITY_EDITOR
CardboardEye[] eyes = null; // Local variable rather than member, so as not to cache.
#endif
if (eyes == null) {
eyes = GetComponentsInChildren<CardboardEye>(true)
.Where(eye => eye.Controller == this)
.ToArray();
}
return eyes;
}
}
/// Returns the nearest CardboardHead that affects our eyes.
/// @note Cached for speed. Call InvalidateEyes to clear the cache.
public CardboardHead Head {
get {
#if UNITY_EDITOR
CardboardHead head = null; // Local variable rather than member, so as not to cache.
#endif
if (head == null) {
head = Eyes.Select(eye => eye.Head).FirstOrDefault();
}
return head;
}
}
/// Clear the cached array of CardboardEye children, as well as the CardboardHead that controls
/// their gaze.
/// @note Be sure to call this if you programmatically change the set of CardboardEye children
/// managed by this StereoController.
public void InvalidateEyes() {
#if !UNITY_EDITOR
eyes = null;
head = null;
#endif
}
/// Updates the stereo cameras from the mono camera every frame. This includes all Camera
/// component values such as background color, culling mask, viewport rect, and so on. Also,
/// it includes updating the viewport rect and projection matrix for side-by-side stereo, plus
/// applying any adjustments for center of interest and stereo comfort.
public void UpdateStereoValues() {
CardboardEye[] eyes = Eyes;
for (int i = 0, n = eyes.Length; i < n; i++) {
eyes[i].UpdateStereoValues();
}
}
public Camera cam { get; private set; }
void Awake() {
Cardboard.Create();
cam = GetComponent<Camera>();
AddStereoRig();
}
/// Helper routine for creation of a stereo rig. Used by the
/// custom editor for this class, or to build the rig at runtime.
public void AddStereoRig() {
// Simplistic test if rig already exists.
// Note: Do not use Eyes property, because it caches the result before we have created the rig.
var eyes = GetComponentsInChildren<CardboardEye>(true).Where(eye => eye.Controller == this);
if (eyes.Any()) {
return;
}
CreateEye(Cardboard.Eye.Left);
CreateEye(Cardboard.Eye.Right);
if (Head == null) {
var head = gameObject.AddComponent<CardboardHead>();
// Don't track position for dynamically added Head components, or else
// you may unexpectedly find your camera pinned to the origin.
head.trackPosition = false;
}
}
// Helper routine for creation of a stereo eye.
private void CreateEye(Cardboard.Eye eye) {
string nm = name + (eye == Cardboard.Eye.Left ? " Left" : " Right");
GameObject go = new GameObject(nm);
go.transform.SetParent(transform, false);
go.AddComponent<Camera>().enabled = false;
var cardboardEye = go.AddComponent<CardboardEye>();
cardboardEye.eye = eye;
cardboardEye.CopyCameraAndMakeSideBySide(this);
}
/// Compute the position of one of the stereo eye cameras. Accounts for both
/// FOV matching and stereo comfort, if those features are enabled. The input is
/// the [1,1] entry of the eye camera's projection matrix, representing the vertical
/// field of view, and the overall scale being applied to the Z axis. Returns the
/// position of the stereo eye camera in local coordinates.
public Vector3 ComputeStereoEyePosition(Cardboard.Eye eye, float proj11, float zScale) {
if (centerOfInterest == null || !centerOfInterest.gameObject.activeInHierarchy) {
return Cardboard.SDK.EyePose(eye).Position * stereoMultiplier;
}
// Distance of COI relative to head.
float distance = centerOfInterest != null ?
(centerOfInterest.position - transform.position).magnitude : 0;
// Size of the COI, clamped to [0..distance] for mathematical sanity in following equations.
float radius = Mathf.Clamp(radiusOfInterest, 0, distance);
// Move the eye so that COI has about the same size onscreen as in the mono camera FOV.
// The radius affects the horizon location, which is where the screen-size matching has to
// occur.
float scale = proj11 / cam.projectionMatrix[1, 1]; // vertical FOV
float offset =
Mathf.Sqrt(radius * radius + (distance * distance - radius * radius) * scale * scale);
float eyeOffset = (distance - offset) * Mathf.Clamp01(matchMonoFOV) / zScale;
float ipdScale = stereoMultiplier;
if (checkStereoComfort) {
// Manage IPD scale based on the distance to the COI.
float minComfort = Cardboard.SDK.ComfortableViewingRange.x;
float maxComfort = Cardboard.SDK.ComfortableViewingRange.y;
if (minComfort < maxComfort) { // Sanity check.
// If closer than the minimum comfort distance, IPD is scaled down.
// If farther than the maximum comfort distance, IPD is scaled up.
// The result is that parallax is clamped within a reasonable range.
float minDistance = (distance - radius) / zScale - eyeOffset;
ipdScale *= minDistance / Mathf.Clamp(minDistance, minComfort, maxComfort);
}
}
return ipdScale * Cardboard.SDK.EyePose(eye).Position + eyeOffset * Vector3.forward;
}
void OnEnable() {
StartCoroutine("EndOfFrame");
}
void OnDisable() {
StopCoroutine("EndOfFrame");
}
void OnPreCull() {
if (Cardboard.SDK.VRModeEnabled) {
// Activate the eyes under our control.
CardboardEye[] eyes = Eyes;
for (int i = 0, n = eyes.Length; i < n; i++) {
eyes[i].cam.enabled = true;
}
// Turn off the mono camera so it doesn't waste time rendering. Remember to reenable.
// @note The mono camera is left on from beginning of frame till now in order that other game
// logic (e.g. referring to Camera.main) continues to work as expected.
cam.enabled = false;
renderedStereo = true;
} else {
Cardboard.SDK.UpdateState();
}
}
IEnumerator EndOfFrame() {
while (true) {
// If *we* turned off the mono cam, turn it back on for next frame.
if (renderedStereo) {
cam.enabled = true;
renderedStereo = false;
}
yield return new WaitForEndOfFrame();
}
}
}