diff --git a/src/gl/stereo3d/gl_anaglyph.cpp b/src/gl/stereo3d/gl_anaglyph.cpp new file mode 100644 index 000000000..ff3926cfc --- /dev/null +++ b/src/gl/stereo3d/gl_anaglyph.cpp @@ -0,0 +1,29 @@ +#include "gl_anaglyph.h" + +namespace s3d { + +MaskAnaglyph::MaskAnaglyph(const ColorMask& leftColorMask, double ipdMeters) + : leftEye(leftColorMask, ipdMeters), rightEye(leftColorMask.inverse(), ipdMeters) +{ + eye_ptrs.push_back(&leftEye); + eye_ptrs.push_back(&rightEye); +} + + +/* static */ +const GreenMagenta& GreenMagenta::getInstance(float ipd) +{ + static GreenMagenta instance(ipd); + return instance; +} + + +/* static */ +const RedCyan& RedCyan::getInstance(float ipd) +{ + static RedCyan instance(ipd); + return instance; +} + + +} /* namespace s3d */ diff --git a/src/gl/stereo3d/gl_anaglyph.h b/src/gl/stereo3d/gl_anaglyph.h new file mode 100644 index 000000000..c95d04205 --- /dev/null +++ b/src/gl/stereo3d/gl_anaglyph.h @@ -0,0 +1,75 @@ +#ifndef GL_ANAGLYPH_H_ +#define GL_ANAGLYPH_H_ + +#include "gl_stereo3d.h" +#include "gl_stereo_leftright.h" + + +namespace s3d { + + +class ColorMask +{ +public: + ColorMask(bool r, bool g, bool b) : r(r), g(g), b(b) {} + ColorMask inverse() const { return ColorMask(!r, !g, !b); } + + GLboolean r; + GLboolean g; + GLboolean b; +}; + + +class AnaglyphLeftPose : public LeftEyePose +{ +public: + AnaglyphLeftPose(const ColorMask& colorMask, float ipd) : LeftEyePose(ipd), colorMask(colorMask) {} + virtual void SetUp() const { glColorMask(colorMask.r, colorMask.g, colorMask.b, true); } + virtual void TearDown() const { glColorMask(1,1,1,1); } +private: + ColorMask colorMask; +}; + +class AnaglyphRightPose : public RightEyePose +{ +public: + AnaglyphRightPose(const ColorMask& colorMask, float ipd) : RightEyePose(ipd), colorMask(colorMask) {} + virtual void SetUp() const { glColorMask(colorMask.r, colorMask.g, colorMask.b, true); } + virtual void TearDown() const { glColorMask(1,1,1,1); } +private: + ColorMask colorMask; +}; + +class MaskAnaglyph : public Stereo3DMode +{ +public: + MaskAnaglyph(const ColorMask& leftColorMask, double ipdMeters); +private: + AnaglyphLeftPose leftEye; + AnaglyphRightPose rightEye; +}; + + +class RedCyan : public MaskAnaglyph +{ +public: + static const RedCyan& getInstance(float ipd); + + RedCyan(float ipd) : MaskAnaglyph(ColorMask(true, false, false), ipd) {} +}; + +class GreenMagenta : public MaskAnaglyph +{ +public: + static const GreenMagenta& getInstance(float ipd); + + GreenMagenta(float ipd) : MaskAnaglyph(ColorMask(false, true, false), ipd) {} +}; + +// TODO matrix anaglyph + + +} /* namespace st3d */ + + +#endif /* GL_ANAGLYPH_H_ */ diff --git a/src/gl/stereo3d/gl_stereo3d.cpp b/src/gl/stereo3d/gl_stereo3d.cpp new file mode 100644 index 000000000..74b36a0fb --- /dev/null +++ b/src/gl/stereo3d/gl_stereo3d.cpp @@ -0,0 +1,71 @@ +#include "gl/stereo3d/gl_stereo3d.h" +#include "vectors.h" // RAD2DEG +#include "doomtype.h" // M_PI + +namespace s3d { + + +/* virtual */ +void EyePose::GetProjection(float fov, float aspectRatio, float fovRatio, GLdouble m[4][4]) const +{ + // Lifted from gl_scene.cpp FGLRenderer::SetProjection() + float fovy = 2 * RAD2DEG(atan(tan(DEG2RAD(fov) / 2) / fovRatio)); + const double zNear = 5.0; + const double zFar = 65536.0; + + double sine, cotangent, deltaZ; + double radians = fovy / 2 * M_PI / 180; + + deltaZ = zFar - zNear; + sine = sin(radians); + if ((deltaZ == 0) || (sine == 0) || (aspectRatio == 0)) { + return; + } + cotangent = cos(radians) / sine; + + memset(m, 0, 16*sizeof(GLdouble)); + m[0][0] = cotangent / aspectRatio; + m[1][1] = cotangent; + m[2][2] = -(zFar + zNear) / deltaZ; + m[2][3] = -1; + m[3][2] = -2 * zNear * zFar / deltaZ; + m[3][3] = 0; +} + +/* virtual */ +Viewport EyePose::GetViewport(const Viewport& fullViewport) const +{ + return fullViewport; +} + + +/* virtual */ +void EyePose::GetViewShift(float yaw, float outViewShift[3]) const +{ + // pass-through for Mono view + outViewShift[0] = 0; + outViewShift[1] = 0; + outViewShift[2] = 0; +} + + +Stereo3DMode::Stereo3DMode() +{ +} + +Stereo3DMode::~Stereo3DMode() +{ +} + +// Avoid static initialization order fiasco by declaring first Mode type (Mono) here in the +// same source file as Stereo3DMode::getCurrentMode() +// https://isocpp.org/wiki/faq/ctors#static-init-order + +/* static */ +const MonoView& MonoView::getInstance() +{ + static MonoView instance; + return instance; +} + +} /* namespace s3d */ diff --git a/src/gl/stereo3d/gl_stereo3d.h b/src/gl/stereo3d/gl_stereo3d.h new file mode 100644 index 000000000..ddbaafaf2 --- /dev/null +++ b/src/gl/stereo3d/gl_stereo3d.h @@ -0,0 +1,80 @@ +#ifndef GL_STEREO3D_H_ +#define GL_STEREO3D_H_ + +#include +#include "gl/system/gl_system.h" + + +/* stereoscopic 3D API */ +namespace s3d { + + +/* Subregion of current display window */ +class Viewport +{ +public: + int x, y; + int width, height; +}; + + +/* Viewpoint of one eye */ +class EyePose +{ +public: + EyePose() {} + virtual ~EyePose() {} + virtual void GetProjection(float fov, float aspectRatio, float fovRatio, GLdouble outMatrix[4][4]) const; + virtual Viewport GetViewport(const Viewport& fullViewport) const; + virtual void GetViewShift(float yaw, float outViewShift[3]) const; + virtual void SetUp() const {}; + virtual void TearDown() const {}; +}; + + +/* Base class for stereoscopic 3D rendering modes */ +class Stereo3DMode +{ +public: + /* const_iterator cycles through the various eye viewpoints */ + typedef std::vector::const_iterator const_iterator; + + /* static methods for managing the selected stereoscopic view state */ + static const Stereo3DMode& getCurrentMode(); + + Stereo3DMode(); + virtual ~Stereo3DMode(); + /* const_iterator cycles through the various eye viewpoints */ + virtual const_iterator begin() const { return eye_ptrs.begin(); } + virtual const_iterator end() const { return eye_ptrs.end(); } + /* hooks for setup and cleanup operations for each stereo mode */ + virtual void SetUp() const {}; + virtual void TearDown() const {}; + +protected: + std::vector eye_ptrs; + +private: + static Stereo3DMode const * currentStereo3DMode; + static void setCurrentMode(const Stereo3DMode& mode); +}; + + +/** +* Ordinary non-3D rendering +*/ +class MonoView : public Stereo3DMode +{ +public: + static const MonoView& getInstance(); + +protected: + MonoView() { eye_ptrs.push_back(¢ralEye); } + EyePose centralEye; +}; + + +} /* namespace st3d */ + + +#endif /* GL_STEREO3D_H_ */ diff --git a/src/gl/stereo3d/gl_stereo_cvars.cpp b/src/gl/stereo3d/gl_stereo_cvars.cpp new file mode 100644 index 000000000..e9d115080 --- /dev/null +++ b/src/gl/stereo3d/gl_stereo_cvars.cpp @@ -0,0 +1,51 @@ +#include "gl/stereo3d/gl_stereo3d.h" +#include "gl/stereo3d/gl_stereo_leftright.h" +#include "gl/stereo3d/gl_anaglyph.h" +#include "gl/system/gl_cvars.h" + +// Set up 3D-specific console variables: +CVAR(Int, vr_mode, 0, CVAR_GLOBALCONFIG) +// intraocular distance in meters +CVAR(Float, vr_ipd, 0.062f, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) // METERS +CVAR(Float, vr_screendist, 0.80f, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) // METERS +CVAR(Float, vr_hunits_per_meter, 41.0f, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) // METERS + +// Manage changing of 3D modes: +namespace s3d { + +// Initialize static member +Stereo3DMode const * Stereo3DMode::currentStereo3DMode = nullptr; + +/* static */ +void Stereo3DMode::setCurrentMode(const Stereo3DMode& mode) { + Stereo3DMode::currentStereo3DMode = &mode; +} + +/* static */ +const Stereo3DMode& Stereo3DMode::getCurrentMode() +{ + // NOTE: Ensure that these vr_mode values correspond to the ones in wadsrc/static/menudef.z + switch (vr_mode) + { + case 1: + setCurrentMode(GreenMagenta::getInstance(vr_ipd)); + break; + case 2: + setCurrentMode(RedCyan::getInstance(vr_ipd)); + break; + case 3: + setCurrentMode(LeftEyeView::getInstance(vr_ipd)); + break; + case 4: + setCurrentMode(RightEyeView::getInstance(vr_ipd)); + break; + case 0: + default: + setCurrentMode(MonoView::getInstance()); + break; + } + return *currentStereo3DMode; +} + +} /* namespace s3d */ + diff --git a/src/gl/stereo3d/gl_stereo_leftright.cpp b/src/gl/stereo3d/gl_stereo_leftright.cpp new file mode 100644 index 000000000..fd1f97729 --- /dev/null +++ b/src/gl/stereo3d/gl_stereo_leftright.cpp @@ -0,0 +1,79 @@ +#include "gl_stereo_leftright.h" +#include "vectors.h" // RAD2DEG +#include "doomtype.h" // M_PI +#include "gl/system/gl_cvars.h" +#include + +EXTERN_CVAR(Float, vr_screendist) +EXTERN_CVAR(Float, vr_hunits_per_meter) + +namespace s3d { + + +/* virtual */ +void ShiftedEyePose::GetProjection(float fov, float aspectRatio, float fovRatio, GLdouble m[4][4]) const +{ + // Lifted from gl_scene.cpp FGLRenderer::SetProjection() + float fovy = 2 * RAD2DEG(atan(tan(DEG2RAD(fov) / 2) / fovRatio)); + double zNear = 5.0; + double zFar = 65536.0; + + // For stereo 3D, use asymmetric frustum shift in projection matrix + // Q: shouldn't shift vary with roll angle, at least for desktop display? + // A: (lab) roll is not measured on desktop display (yet) + double frustumShift = zNear * shift / vr_screendist; // meters cancel + // double frustumShift = 0; // Turning off shift for debugging + double fH = tan(fovy / 360 * M_PI) * zNear; + double fW = fH * aspectRatio; + // Emulate glFrustum command: + // glFrustum(-fW - frustumShift, fW - frustumShift, -fH, fH, zNear, zFar); + double left = -fW - frustumShift; + double right = fW - frustumShift; + double bottom = -fH; + double top = fH; + double deltaZ = zFar - zNear; + + memset(m, 0, 16 * sizeof(GLdouble)); // set all elements to zero, cleverly + + // https://www.opengl.org/sdk/docs/man2/xhtml/glFrustum.xml + m[0][0] = 2 * zNear / (right - left); + m[1][1] = 2 * zNear / (top - bottom); + m[2][2] = -(zFar + zNear) / deltaZ; + m[2][3] = -1; + m[3][2] = -2 * zNear * zFar / deltaZ; + // m[3][3] = 0; // redundant + // m[2][1] = (top + bottom) / (top - bottom); // zero for the cases I know of... + m[2][0] = (right + left) / (right - left); // asymmetric shift is in this term +} + + +/* virtual */ +void ShiftedEyePose::GetViewShift(float yaw, float outViewShift[3]) const +{ + float dx = cos(DEG2RAD(yaw)) * vr_hunits_per_meter * shift; + float dy = sin(DEG2RAD(yaw)) * vr_hunits_per_meter * shift; + outViewShift[0] = dx; + outViewShift[1] = dy; + outViewShift[2] = 0; +} + + +/* static */ +const LeftEyeView& LeftEyeView::getInstance(float ipd) +{ + static LeftEyeView instance(ipd); + instance.setIpd(ipd); + return instance; +} + + +/* static */ +const RightEyeView& RightEyeView::getInstance(float ipd) +{ + static RightEyeView instance(ipd); + instance.setIpd(ipd); + return instance; +} + + +} /* namespace s3d */ diff --git a/src/gl/stereo3d/gl_stereo_leftright.h b/src/gl/stereo3d/gl_stereo_leftright.h new file mode 100644 index 000000000..78f66e7fe --- /dev/null +++ b/src/gl/stereo3d/gl_stereo_leftright.h @@ -0,0 +1,71 @@ +#ifndef GL_STEREO_LEFTRIGHT_H_ +#define GL_STEREO_LEFTRIGHT_H_ + +#include "gl_stereo3d.h" + +namespace s3d { + + +class ShiftedEyePose : public EyePose +{ +public: + ShiftedEyePose(float shift) : shift(shift) {}; + float getShift() const { return shift; } + void setShift(float shift) { this->shift = shift; } + virtual void GetProjection(float fov, float aspectRatio, float fovRatio, GLdouble outMatrix[4][4]) const; + virtual void GetViewShift(float yaw, float outViewShift[3]) const; +protected: + float shift; +}; + + +class LeftEyePose : public ShiftedEyePose +{ +public: + LeftEyePose(float ipd) : ShiftedEyePose(-0.5*ipd) {} + float getIpd() const { return -2.0*getShift(); } + void setIpd(float ipd) { setShift(-0.5*ipd); } +}; + + +class RightEyePose : public ShiftedEyePose +{ +public: + RightEyePose(float ipd) : ShiftedEyePose(+0.5*ipd) {} + float getIpd() const { return +2.0*shift; } + void setIpd(float ipd) { setShift(+0.5*ipd); } +}; + + +/** + * As if viewed through the left eye only + */ +class LeftEyeView : public Stereo3DMode +{ +public: + static const LeftEyeView& getInstance(float ipd); + + LeftEyeView(float ipd) : eye(ipd) { eye_ptrs.push_back(&eye); } + float getIpd() const { return eye.getIpd(); } + void setIpd(float ipd) { eye.setIpd(ipd); } +protected: + LeftEyePose eye; +}; + + +class RightEyeView : public Stereo3DMode +{ +public: + static const RightEyeView& getInstance(float ipd); + + RightEyeView(float ipd) : eye(ipd) { eye_ptrs.push_back(&eye); } + float getIpd() const { return eye.getIpd(); } + void setIpd(float ipd) { eye.setIpd(ipd); } +protected: + RightEyePose eye; +}; + + +} /* namespace s3d */ + +#endif /* GL_STEREO_LEFTRIGHT_H_ */ diff --git a/src/gl/stereo3d/scoped_color_mask.h b/src/gl/stereo3d/scoped_color_mask.h new file mode 100644 index 000000000..cc3a1df1d --- /dev/null +++ b/src/gl/stereo3d/scoped_color_mask.h @@ -0,0 +1,38 @@ +#ifndef GL_STEREO3D_SCOPED_COLOR_MASK_H_ +#define GL_STEREO3D_SCOPED_COLOR_MASK_H_ + +#include "gl/glew.h" + +/** +* Temporarily change color mask +*/ +class ScopedColorMask +{ +public: + ScopedColorMask(GLboolean r, GLboolean g, GLboolean b, GLboolean a) + : isPushed(false) + { + setColorMask(r, g, b, a); + } + ~ScopedColorMask() { + revert(); + } + void setColorMask(GLboolean r, GLboolean g, GLboolean b, GLboolean a) { + if (!isPushed) { + glPushAttrib(GL_COLOR_BUFFER_BIT); + isPushed = true; + } + glColorMask(r, g, b, a); + } + void revert() { + if (isPushed) { + glPopAttrib(); + isPushed = false; + } + } +private: + bool isPushed; +}; + + +#endif // GL_STEREO3D_SCOPED_COLOR_MASK_H_ diff --git a/src/gl/stereo3d/scoped_view_shifter.cpp b/src/gl/stereo3d/scoped_view_shifter.cpp new file mode 100644 index 000000000..74511f3f5 --- /dev/null +++ b/src/gl/stereo3d/scoped_view_shifter.cpp @@ -0,0 +1,29 @@ +#include "scoped_view_shifter.h" +#include "r_utility.h" + +namespace s3d { + +ScopedViewShifter::ScopedViewShifter(float dxyz[3]) // in meters +{ + // save original values + cachedViewx = viewx; + cachedViewy = viewy; + cachedViewz = viewz; + // modify values + float fViewx = FIXED2FLOAT(viewx) - dxyz[0]; + float fViewy = FIXED2FLOAT(viewy) + dxyz[1]; + float fViewz = FIXED2FLOAT(viewz) + dxyz[2]; + viewx = FLOAT2FIXED(fViewx); + viewy = FLOAT2FIXED(fViewy); + viewz = FLOAT2FIXED(fViewz); +} + +ScopedViewShifter::~ScopedViewShifter() +{ + // restore original values + viewx = cachedViewx; + viewy = cachedViewy; + viewz = cachedViewz; +} + +} \ No newline at end of file diff --git a/src/gl/stereo3d/scoped_view_shifter.h b/src/gl/stereo3d/scoped_view_shifter.h new file mode 100644 index 000000000..0b4f201a4 --- /dev/null +++ b/src/gl/stereo3d/scoped_view_shifter.h @@ -0,0 +1,25 @@ +#ifndef GL_STEREO3D_SCOPED_VIEW_SHIFTER_H_ +#define GL_STEREO3D_SCOPED_VIEW_SHIFTER_H_ + +#include "basictypes.h" + +namespace s3d { + + /** + * Temporarily shift viewx, viewy, viewz + */ + class ScopedViewShifter + { + public: + ScopedViewShifter(float dxyz[3]); // in meters + ~ScopedViewShifter(); + + private: + fixed_t cachedViewx; + fixed_t cachedViewy; + fixed_t cachedViewz; + }; + +} /* namespace s3d */ + +#endif // GL_STEREO3D_SCOPED_VIEW_SHIFTER_H_