/******************************************************************************* * Copyright 2012 Geoscience Australia * * 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. ******************************************************************************/ package au.gov.ga.earthsci.worldwind.common.view.stereo; import gov.nasa.worldwind.View; import gov.nasa.worldwind.geom.Angle; import gov.nasa.worldwind.geom.Matrix; import gov.nasa.worldwind.geom.Vec4; import gov.nasa.worldwind.globes.Globe; import gov.nasa.worldwind.render.DrawContext; import java.awt.Rectangle; import javax.media.opengl.GL2; import au.gov.ga.earthsci.worldwind.common.render.DrawableSceneController; import au.gov.ga.earthsci.worldwind.common.util.Util; import au.gov.ga.earthsci.worldwind.common.view.stereo.IStereoViewDelegate.Eye; /** * Helper with common functionality for all {@link IStereoViewDelegate} * implementations. * * @author Michael de Hoog (michael.dehoog@ga.gov.au) */ public class StereoViewHelper { private Eye eye = Eye.LEFT; private boolean stereo = false; private StereoViewParameters parameters = new BasicStereoViewParameters(); private double lastFocalLength = 1; private double lastEyeSeparation = 0; /** * @see IStereoViewDelegate#setup(boolean, Eye) */ public void setup(boolean stereo, Eye eye) { //don't allow null eye if (eye == null) { eye = Eye.LEFT; } this.stereo = stereo; this.eye = eye; } /** * @see IStereoViewDelegate#getEye() */ public Eye getEye() { return eye; } /** * @see IStereoViewDelegate#isStereo() */ public boolean isStereo() { return stereo; } /** * @see IStereoViewDelegate#getParameters() */ public StereoViewParameters getParameters() { return parameters; } /** * @see IStereoViewDelegate#setParameters(StereoViewParameters) */ public void setParameters(StereoViewParameters parameters) { this.parameters = parameters; } /** * @see IStereoViewDelegate#getCurrentFocalLength() */ public double getCurrentFocalLength() { if (parameters.isDynamicStereo()) { return lastFocalLength; } return parameters.getFocalLength(); } /** * @see IStereoViewDelegate#getCurrentEyeSeparation() */ public double getCurrentEyeSeparation() { if (parameters.isDynamicStereo()) { return lastEyeSeparation; } return parameters.getEyeSeparation(); } /** * @see TransformView#computeProjection(double, double) */ public Matrix computeProjection(View view, Angle horizontalFieldOfView, double nearDistance, double farDistance) { if (stereo) { return calculateStereoProjectionMatrix(view, horizontalFieldOfView, nearDistance, farDistance, eye, lastFocalLength, lastEyeSeparation); } else { return calculateMonoProjectionMatrix(view, horizontalFieldOfView, nearDistance, farDistance); } } /** * Calculate the focal length and eye separation for the current view. These * parameters are calculated on the fly if dynamic stereo is enabled. * * @param view */ protected void calculateStereoParameters(View view) { if (parameters.isDynamicStereo()) { AsymmetricFrutumParameters afp = calculateStereoParameters(view, parameters.getEyeSeparationMultiplier()); this.lastFocalLength = afp.focalLength; this.lastEyeSeparation = afp.eyeSeparation; } else { this.lastFocalLength = parameters.getFocalLength(); this.lastEyeSeparation = parameters.getEyeSeparation(); } } /** * @see TransformView#beforeComputeMatrices() */ public void beforeComputeMatrices(View view) { calculateStereoParameters(view); } /** * If stereo is enabled, translate the given matrix to the left or right * according to which eye is currently being rendered. * * @param matrix * Matrix to transform * @return Transformed matrix */ public Matrix transformModelView(Matrix matrix) { if (matrix != null && stereo) { matrix = Matrix.fromTranslation((eye == Eye.RIGHT ? 1 : -1) * lastEyeSeparation * 0.5, 0, 0) .multiply(matrix); } return matrix; } /** * @see DrawableView#draw(DrawContext, DrawableSceneController) */ public void draw(DrawContext dc, DrawableSceneController sc) { if (!getParameters().isStereoEnabled()) { sc.draw(dc); return; } GL2 gl = dc.getGL().getGL2(); StereoMode mode = getParameters().getStereoMode(); boolean swap = getParameters().isSwapEyes(); setup(true, swap ? Eye.RIGHT : Eye.LEFT); setupBuffer(gl, mode, Eye.LEFT); sc.applyView(dc); sc.draw(dc); gl.glClear(GL2.GL_DEPTH_BUFFER_BIT); setup(true, swap ? Eye.LEFT : Eye.RIGHT); setupBuffer(gl, mode, Eye.RIGHT); sc.applyView(dc); sc.draw(dc); setup(false, Eye.LEFT); restoreBuffer(gl, mode); sc.applyView(dc); } private static void setupBuffer(GL2 gl, StereoMode mode, Eye eye) { boolean left = eye == Eye.LEFT; switch (mode) { case RC_ANAGLYPH: gl.glColorMask(left, !left, !left, true); break; case GM_ANAGLYPH: gl.glColorMask(!left, left, !left, true); break; case BY_ANAGLYPH: gl.glColorMask(!left, !left, left, true); break; case STEREO_BUFFER: gl.glDrawBuffer(left ? GL2.GL_BACK_LEFT : GL2.GL_BACK_RIGHT); break; } } private static void restoreBuffer(GL2 gl, StereoMode mode) { switch (mode) { case BY_ANAGLYPH: case GM_ANAGLYPH: case RC_ANAGLYPH: gl.glColorMask(true, true, true, true); break; case STEREO_BUFFER: gl.glDrawBuffer(GL2.GL_BACK); break; } } /** * Calculate the projection matrix for the given view, taking into account * the current stereo eye being rendered. Off-axis frustums are used; see * http://paulbourke.net/miscellaneous/stereographics/stereorender/. * * @param view * @param horizontalFieldOfView * @param nearDistance * @param farDistance * @param eye * @param focalLength * @param eyeSeparation * @return Stereo projection matrix */ public static Matrix calculateStereoProjectionMatrix(View view, Angle horizontalFieldOfView, double nearDistance, double farDistance, Eye eye, double focalLength, double eyeSeparation) { Rectangle viewport = view.getViewport(); double viewportWidth = viewport.getWidth() <= 0d ? 1d : viewport.getWidth(); double viewportHeight = viewport.getHeight() <= 0d ? 1d : viewport.getHeight(); double aspectratio = viewportWidth / viewportHeight; double widthdiv2 = nearDistance * horizontalFieldOfView.tanHalfAngle(); double multiplier = eye == Eye.RIGHT ? 0.5 : -0.5; double distance = multiplier * eyeSeparation * nearDistance / focalLength; //hfov: double top = widthdiv2 / aspectratio; double bottom = -widthdiv2 / aspectratio; double left = -widthdiv2 + distance; double right = widthdiv2 + distance; //vfov: //double top = widthdiv2; //double bottom = -widthdiv2; //double left = -widthdiv2 * aspectratio + distance; //double right = widthdiv2 * aspectratio + distance; return fromFrustum(left, right, bottom, top, nearDistance, farDistance); } /** * Calculate a standard non-stereo projection matrix. * * @param view * @param horizontalFieldOfView * @param nearDistance * @param farDistance * @return Perspective projection matrix */ public static Matrix calculateMonoProjectionMatrix(View view, Angle horizontalFieldOfView, double nearDistance, double farDistance) { Rectangle viewport = view.getViewport(); double viewportWidth = viewport.getWidth() <= 0d ? 1d : viewport.getWidth(); double viewportHeight = viewport.getHeight() <= 0d ? 1d : viewport.getHeight(); return Matrix.fromPerspective(horizontalFieldOfView, viewportWidth, viewportHeight, nearDistance, farDistance); } /** * Calculate a projection matrix from the given frustum parameters. * * @param left * @param right * @param bottom * @param top * @param near * @param far * @return Frustum projection matrix */ public static Matrix fromFrustum(double left, double right, double bottom, double top, double near, double far) { double A = (right + left) / (right - left); double B = (top + bottom) / (top - bottom); double C = -(far + near) / (far - near); double D = -(2 * far * near) / (far - near); double E = (2 * near) / (right - left); double F = (2 * near) / (top - bottom); return new Matrix(E, 0, A, 0, 0, F, B, 0, 0, 0, C, D, 0, 0, -1, 0); } /** * Calculate a good eye separation and focal length for the current view. * Used when dynamic stereo is enabled. * * @param view * @param separationExaggeration * Eye separation multiplier * @return {@link AsymmetricFrutumParameters} containing a good eye * separation and focal length to use for the current view. */ public static AsymmetricFrutumParameters calculateStereoParameters(View view, double separationExaggeration) { Vec4 eyePoint = view.getCurrentEyePoint(); double distanceFromOrigin = eyePoint.getLength3(); Globe globe = view.getGlobe(); double radius = globe.getRadiusAt(globe.computePositionFromPoint(eyePoint)); double amount = Util.percentDouble(distanceFromOrigin, radius, radius * 5d); Vec4 centerPoint = view.getCenterPoint(); double distanceToCenter; if (centerPoint != null) { distanceToCenter = eyePoint.distanceTo3(centerPoint); } else { distanceToCenter = view.getHorizonDistance(); } //limit the distance to center relative to the eye distance from the ellipsoid surface double maxDistanceToCenter = (distanceFromOrigin - radius) * 10; distanceToCenter = Math.min(maxDistanceToCenter, distanceToCenter); //calculate focal length as distance to center when zoomed in, and distance from origin when zoomed out double focalLength = Util.mixDouble(amount, distanceToCenter, distanceFromOrigin); //exaggerate separation more when zoomed out separationExaggeration = Util.mixDouble(amount, separationExaggeration, separationExaggeration * 4); //move focal length closer as view is pitched amount = Util.percentDouble(view.getPitch().degrees, 0d, 90d); focalLength = Util.mixDouble(amount, focalLength, focalLength / 3d); //calculate eye separation linearly relative to focal length double eyeSeparation = separationExaggeration * focalLength / 100d; return new AsymmetricFrutumParameters(focalLength, eyeSeparation); } /** * Container class for passing eye separation and focal length parameters. */ public static class AsymmetricFrutumParameters { public final double focalLength; public final double eyeSeparation; public AsymmetricFrutumParameters(double focalLength, double eyeSeparation) { this.focalLength = focalLength; this.eyeSeparation = eyeSeparation; } } }