1 /** @file vrconfig.cpp  Virtual reality configuration.
2  *
3  * @authors Copyright (c) 2014-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4  * @authors Copyright (c) 2013 Christopher Bruns <cmbruns@rotatingpenguin.com>
5  *
6  * @par License
7  * LGPL: http://www.gnu.org/licenses/lgpl.html
8  *
9  * <small>This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU Lesser General Public License as published by
11  * the Free Software Foundation; either version 3 of the License, or (at your
12  * option) any later version. This program is distributed in the hope that it
13  * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
15  * General Public License for more details. You should have received a copy of
16  * the GNU Lesser General Public License along with this program; if not, see:
17  * http://www.gnu.org/licenses</small>
18  */
19 
20 #include "de/VRConfig"
21 #include "de/math.h"
22 
23 namespace de {
24 
DENG2_PIMPL(VRConfig)25 DENG2_PIMPL(VRConfig)
26 {
27     StereoMode mode;
28     Eye currentEye = NeitherEye;
29     de::OculusRift ovr;
30     float screenDistance;
31     float ipd;
32     float eyeHeightInMapUnits;
33     float eyeShift;
34     float playerPhysicalHeight;
35     bool swapEyes;
36     int riftFramebufferSamples; // Multisampling used in unwarped Rift framebuffer
37 
38     /**
39      * Unlike most 3D modes, Oculus Rift typically uses no frustum shift. (or if we did,
40      * it would be different and complicated)
41      */
42     bool frustumShift;
43 
44     float dominantEye; ///< Kludge for aim-down-weapon-sight modes
45 
46     Impl(Public *i)
47         : Base(i)
48         , mode(Mono)
49         , screenDistance(20.f)
50         , ipd(.064f) // average male IPD
51         , eyeHeightInMapUnits(41)
52         , eyeShift(0)
53         , playerPhysicalHeight(1.75f)
54         , swapEyes(false)
55         , riftFramebufferSamples(1)
56         , frustumShift(true)
57         , dominantEye(0.0f)
58     {}
59 
60     float mapUnitsPerMeter() const
61     {
62         // 0.925 because eyes are not at top of head
63         return eyeHeightInMapUnits / (0.925 * playerPhysicalHeight);
64     }
65 };
66 
VRConfig()67 VRConfig::VRConfig() : d(new Impl(this))
68 {}
69 
setMode(StereoMode newMode)70 void VRConfig::setMode(StereoMode newMode)
71 {
72     d->mode = newMode;
73 }
74 
setScreenDistance(float distance)75 void VRConfig::setScreenDistance(float distance)
76 {
77     d->screenDistance = distance;
78 }
79 
setEyeHeightInMapUnits(float eyeHeightInMapUnits)80 void VRConfig::setEyeHeightInMapUnits(float eyeHeightInMapUnits)
81 {
82     d->eyeHeightInMapUnits = eyeHeightInMapUnits;
83 }
84 
setInterpupillaryDistance(float ipd)85 void VRConfig::setInterpupillaryDistance(float ipd)
86 {
87     d->ipd = ipd;
88 }
89 
setPhysicalPlayerHeight(float heightInMeters)90 void VRConfig::setPhysicalPlayerHeight(float heightInMeters)
91 {
92     d->playerPhysicalHeight = heightInMeters;
93 }
94 
setCurrentEye(Eye eye)95 void VRConfig::setCurrentEye(Eye eye)
96 {
97     float eyePos = (eye == NeitherEye? 0 : eye == LeftEye? -1 : 1);
98 
99     d->currentEye = eye;
100     d->eyeShift = d->mapUnitsPerMeter() * (eyePos - d->dominantEye) * 0.5 * d->ipd;
101     if (d->swapEyes)
102     {
103         d->eyeShift *= -1;
104     }
105 }
106 
currentEye() const107 VRConfig::Eye VRConfig::currentEye() const
108 {
109     return d->currentEye;
110 }
111 
enableFrustumShift(bool enable)112 void VRConfig::enableFrustumShift(bool enable)
113 {
114     d->frustumShift = enable;
115 }
116 
setRiftFramebufferSampleCount(int samples)117 void VRConfig::setRiftFramebufferSampleCount(int samples)
118 {
119     d->riftFramebufferSamples = samples;
120 }
121 
setSwapEyes(bool swapped)122 void VRConfig::setSwapEyes(bool swapped)
123 {
124     d->swapEyes = swapped;
125 }
126 
setDominantEye(float value)127 void VRConfig::setDominantEye(float value)
128 {
129     d->dominantEye = value;
130 }
131 
mode() const132 VRConfig::StereoMode VRConfig::mode() const
133 {
134     return d->mode;
135 }
136 
screenDistance() const137 float VRConfig::screenDistance() const
138 {
139     return d->screenDistance;
140 }
141 
needsStereoGLFormat() const142 bool VRConfig::needsStereoGLFormat() const
143 {
144     return modeNeedsStereoGLFormat(mode());
145 }
146 
modeNeedsStereoGLFormat(StereoMode mode)147 bool VRConfig::modeNeedsStereoGLFormat(StereoMode mode)
148 {
149     return mode == QuadBuffered;
150 }
151 
interpupillaryDistance() const152 float VRConfig::interpupillaryDistance() const
153 {
154     return d->ipd;
155 }
156 
eyeHeightInMapUnits() const157 float VRConfig::eyeHeightInMapUnits() const
158 {
159     return d->eyeHeightInMapUnits;
160 }
161 
mapUnitsPerMeter() const162 float VRConfig::mapUnitsPerMeter() const
163 {
164     return d->mapUnitsPerMeter();
165 }
166 
physicalPlayerHeight() const167 float VRConfig::physicalPlayerHeight() const
168 {
169     return d->playerPhysicalHeight;
170 }
171 
eyeShift() const172 float VRConfig::eyeShift() const
173 {
174     return d->eyeShift;
175 }
176 
frustumShift() const177 bool VRConfig::frustumShift() const
178 {
179     return d->frustumShift;
180 }
181 
swapEyes() const182 bool VRConfig::swapEyes() const
183 {
184     return d->swapEyes;
185 }
186 
dominantEye() const187 float VRConfig::dominantEye() const
188 {
189     return d->dominantEye;
190 }
191 
riftFramebufferSampleCount() const192 int VRConfig::riftFramebufferSampleCount() const
193 {
194     return d->riftFramebufferSamples;
195 }
196 
viewAspect(Vector2f const & viewPortSize) const197 float VRConfig::viewAspect(Vector2f const &viewPortSize) const
198 {
199     /*if (mode() == OculusRift)
200     {
201         // Override with the Oculus Rift's aspect ratio.
202         return oculusRift().aspect();
203     }*/
204 
205     // We're assuming pixels are squares.
206     return viewPortSize.x / viewPortSize.y;
207 }
208 
verticalFieldOfView(float horizFovDegrees,Vector2f const & viewPortSize) const209 float VRConfig::verticalFieldOfView(float horizFovDegrees, Vector2f const &viewPortSize) const
210 {
211     // We're assuming pixels are squares.
212     float const aspect = viewAspect(viewPortSize);
213 
214     if (mode() == OculusRift)
215     {
216         // A little trigonometry to apply aspect ratio to angles
217         float x = std::tan(.5f * degreeToRadian(horizFovDegrees));
218         return radianToDegree(2.f * std::atan2(x / aspect, 1.f));
219     }
220 
221     return clamp(1.f, horizFovDegrees / aspect, 179.f);
222 }
223 
projectionMatrix(float fovDegrees,Vector2f const & viewPortSize,float nearClip,float farClip) const224 Matrix4f VRConfig::projectionMatrix(float fovDegrees,
225                                     Vector2f const &viewPortSize,
226                                     float nearClip, float farClip) const
227 {
228     if (mode() == OculusRift && oculusRift().isReady())
229     {
230         // OVR will calculate our projection matrix.
231         float const mapUnits = d->mapUnitsPerMeter();
232         return oculusRift().projection(nearClip, farClip) *
233                Matrix4f::translate(oculusRift().eyeOffset() * mapUnits);
234     }
235     return Matrix4f::perspective(fovDegrees, viewAspect(viewPortSize), nearClip, farClip) *
236            Matrix4f::translate(Vector3f(-eyeShift(), 0, 0));
237 
238 #if 0
239     float const yfov = verticalFieldOfView(fovDegrees, viewPortSize);
240     float const fH   = std::tan(.5f * degreeToRadian(yfov)) * nearClip;
241     float const fW   = fH * viewAspect(viewPortSize);
242 
243     /*
244      * Asymmetric frustum shift is computed to realign screen-depth items after view point has shifted.
245      * Asymmetric frustum shift method is probably superior to competing toe-in stereo 3D method:
246      *  - AFS preserves identical near and far clipping planes in both views
247      *  - AFS shows items at/near infinity better
248      *  - AFS conforms to what stereo 3D photographers call "ortho stereo"
249      * Asymmetric frustum shift is used for all stereo 3D modes except Oculus Rift mode, which only
250      * applies the viewpoint shift.
251      */
252     float shift = 0;
253     if (frustumShift())
254     {
255         shift = eyeShift() * nearClip / screenDistance();
256     }
257 
258     return Matrix4f::frustum(-fW - shift, fW - shift,
259                              -fH, fH,
260                              nearClip, farClip) *
261            Matrix4f::translate(Vector3f(-eyeShift(), 0, 0));
262 #endif
263 }
264 
oculusRift()265 OculusRift &VRConfig::oculusRift()
266 {
267     return d->ovr;
268 }
269 
oculusRift() const270 OculusRift const &VRConfig::oculusRift() const
271 {
272     return d->ovr;
273 }
274 
modeAppliesDisplacement(StereoMode mode)275 bool VRConfig::modeAppliesDisplacement(StereoMode mode)
276 {
277     switch (mode)
278     {
279     case Mono:
280     case GreenMagenta:
281     case RedCyan:
282     case LeftOnly:
283     case RightOnly:
284     case QuadBuffered:
285     case RowInterleaved:
286         return false;
287 
288     default:
289         break;
290     }
291     return true;
292 }
293 
294 } // namespace de
295