mirror of
https://github.com/donl/brainvr.git
synced 2026-05-26 22:06:24 -06:00
303 lines
12 KiB
C#
303 lines
12 KiB
C#
// 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;
|
|
|
|
// Controls a pair of CardboardEye objects that will render the stereo view
|
|
// of the camera this script is attached to.
|
|
[RequireComponent(typeof(Camera))]
|
|
public class StereoController : MonoBehaviour {
|
|
[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;
|
|
|
|
// Adjusts the level of stereopsis for this stereo rig. Note that 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 that 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 COI 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;
|
|
|
|
// 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, and the y value is for the top and bottom of the screen.
|
|
[Tooltip("Move the camera window horizontally towards the center of the screen (PIP only).")]
|
|
[Range(0,1)]
|
|
public float stereoPaddingX = 0;
|
|
|
|
[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;
|
|
#endif
|
|
|
|
// Returns the CardboardEye components that we control.
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Clear the cached array of CardboardEye children.
|
|
// 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;
|
|
#endif
|
|
}
|
|
|
|
// Returns the nearest CardboardHead that affects our eyes.
|
|
public CardboardHead Head {
|
|
get {
|
|
return Eyes.Select(eye => eye.Head).FirstOrDefault();
|
|
}
|
|
}
|
|
|
|
// Where the stereo eyes will render the scene.
|
|
public RenderTexture StereoScreen {
|
|
get {
|
|
return GetComponent<Camera>().targetTexture ?? Cardboard.SDK.StereoScreen;
|
|
}
|
|
}
|
|
|
|
// In the Unity editor, at the point we need it, the Screen.height oddly includes the tab bar
|
|
// at the top of the Game window. So subtract that out.
|
|
private int ScreenHeight {
|
|
get {
|
|
return Screen.height - (Application.isEditor && StereoScreen == null ? 36 : 0);
|
|
}
|
|
}
|
|
|
|
void Awake() {
|
|
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() {
|
|
if (Eyes.Length > 0) { // Simplistic test if rig already exists.
|
|
return;
|
|
}
|
|
CreateEye(Cardboard.Eye.Left);
|
|
CreateEye(Cardboard.Eye.Right);
|
|
if (Head == null) {
|
|
gameObject.AddComponent<CardboardHead>();
|
|
}
|
|
#if !UNITY_5
|
|
if (GetComponent<Camera>().tag == "MainCamera" && GetComponent<SkyboxMesh>() == null) {
|
|
gameObject.AddComponent<SkyboxMesh>();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// 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.parent = transform;
|
|
go.AddComponent<Camera>().enabled = false;
|
|
#if !UNITY_5
|
|
if (GetComponent<GUILayer>() != null) {
|
|
go.AddComponent<GUILayer>();
|
|
}
|
|
if (GetComponent("FlareLayer") != null) {
|
|
go.AddComponent("FlareLayer");
|
|
}
|
|
#endif
|
|
var cardboardEye = go.AddComponent<CardboardEye>();
|
|
cardboardEye.eye = eye;
|
|
cardboardEye.CopyCameraAndMakeSideBySide(this);
|
|
}
|
|
|
|
// Given information about a specific camera (usually one of the stereo eyes),
|
|
// computes an adjustment to the stereo settings for both FOV matching and
|
|
// stereo comfort. The input is the [1,1] entry of the camera's projection
|
|
// matrix, representing the vertical field of view, and the overall scale
|
|
// being applied to the Z axis. The output is a multiplier of the IPD to
|
|
// use for offseting the eyes laterally, and an offset in the eye's Z direction
|
|
// to account for the FOV difference. The eye offset is in local coordinates.
|
|
public void ComputeStereoAdjustment(float proj11, float zScale,
|
|
out float ipdScale, out float eyeOffset) {
|
|
ipdScale = stereoMultiplier;
|
|
eyeOffset = 0;
|
|
if (centerOfInterest == null || !centerOfInterest.gameObject.activeInHierarchy) {
|
|
return;
|
|
}
|
|
|
|
// Distance of COI relative to head.
|
|
float distance = (centerOfInterest.position - transform.position).magnitude;
|
|
|
|
// 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 / GetComponent<Camera>().projectionMatrix[1, 1]; // vertical FOV
|
|
float offset =
|
|
Mathf.Sqrt(radius * radius + (distance * distance - radius * radius) * scale * scale);
|
|
eyeOffset = (distance - offset) * Mathf.Clamp01(matchMonoFOV) / zScale;
|
|
|
|
// Manage IPD scale based on the distance to the COI.
|
|
if (checkStereoComfort) {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
void OnEnable() {
|
|
StartCoroutine("EndOfFrame");
|
|
}
|
|
|
|
void OnDisable() {
|
|
StopCoroutine("EndOfFrame");
|
|
}
|
|
|
|
void OnPreCull() {
|
|
if (!Cardboard.SDK.VRModeEnabled) {
|
|
// Nothing to do.
|
|
return;
|
|
}
|
|
|
|
Cardboard.SDK.UpdateState();
|
|
|
|
// Turn off the mono camera so it doesn't waste time rendering.
|
|
// Note: mono camera is left on from beginning of frame till now
|
|
// in order that other game logic (e.g. Camera.main) continues
|
|
// to work as expected.
|
|
GetComponent<Camera>().enabled = false;
|
|
|
|
// Render the eyes under our control.
|
|
foreach (var eye in Eyes) {
|
|
eye.Render();
|
|
}
|
|
|
|
// Remember to reenable.
|
|
renderedStereo = true;
|
|
}
|
|
|
|
IEnumerator EndOfFrame() {
|
|
while (true) {
|
|
// If *we* turned off the mono cam, turn it back on for next frame.
|
|
if (renderedStereo) {
|
|
GetComponent<Camera>().enabled = true;
|
|
renderedStereo = false;
|
|
}
|
|
yield return new WaitForEndOfFrame();
|
|
}
|
|
}
|
|
}
|