/******************************************************************************* * Copyright 2014 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.oculus; import static com.oculusvr.capi.OvrLibrary.ovrDistortionCaps.ovrDistortionCap_Chromatic; import static com.oculusvr.capi.OvrLibrary.ovrDistortionCaps.ovrDistortionCap_TimeWarp; import static com.oculusvr.capi.OvrLibrary.ovrDistortionCaps.ovrDistortionCap_Vignette; import static com.oculusvr.capi.OvrLibrary.ovrEyeType.ovrEye_Count; import static com.oculusvr.capi.OvrLibrary.ovrEyeType.ovrEye_Left; import static com.oculusvr.capi.OvrLibrary.ovrEyeType.ovrEye_Right; import static com.oculusvr.capi.OvrLibrary.ovrHmdCaps.ovrHmdCap_DynamicPrediction; import static com.oculusvr.capi.OvrLibrary.ovrHmdCaps.ovrHmdCap_LowPersistence; import static com.oculusvr.capi.OvrLibrary.ovrHmdType.ovrHmd_DK1; import static com.oculusvr.capi.OvrLibrary.ovrTrackingCaps.ovrTrackingCap_MagYawCorrection; import static com.oculusvr.capi.OvrLibrary.ovrTrackingCaps.ovrTrackingCap_Orientation; import static com.oculusvr.capi.OvrLibrary.ovrTrackingCaps.ovrTrackingCap_Position; import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.geom.Angle; import gov.nasa.worldwind.geom.Matrix; import gov.nasa.worldwind.geom.Position; import gov.nasa.worldwind.geom.Quaternion; import gov.nasa.worldwind.geom.Vec4; import gov.nasa.worldwind.pick.PickedObject; import gov.nasa.worldwind.render.DrawContext; import gov.nasa.worldwind.util.OGLStackHandler; import gov.nasa.worldwind.view.ViewUtil; import gov.nasa.worldwind.view.orbit.OrbitView; import java.awt.Dimension; import java.awt.Point; import java.awt.Rectangle; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.util.Arrays; import java.util.List; import javax.media.opengl.GL2; import au.gov.ga.earthsci.worldwind.common.render.DrawableSceneController; import au.gov.ga.earthsci.worldwind.common.render.FrameBuffer; import au.gov.ga.earthsci.worldwind.common.view.delegate.IDelegateView; import au.gov.ga.earthsci.worldwind.common.view.delegate.IViewDelegate; import com.jogamp.opengl.util.GLBuffers; import com.oculusvr.capi.DistortionMesh; import com.oculusvr.capi.DistortionVertex; import com.oculusvr.capi.EyeRenderDesc; import com.oculusvr.capi.FovPort; import com.oculusvr.capi.Hmd; import com.oculusvr.capi.OvrMatrix4f; import com.oculusvr.capi.OvrRecti; import com.oculusvr.capi.OvrSizei; import com.oculusvr.capi.OvrVector2f; import com.oculusvr.capi.OvrVector2i; import com.oculusvr.capi.OvrVector3f; import com.oculusvr.capi.Posef; /** * {@link IViewDelegate} for the Oculus Rift. * <p/> * Based on example from <a * href="https://github.com/elect86/Joglus/">Joglus</a>. * * @author Michael de Hoog (michael.dehoog@ga.gov.au) */ public class RiftViewDistortionDelegate implements IViewDelegate { private enum DistortionObjects { vbo, ibo, count } protected Hmd hmd; protected final OvrRecti[] eyeRenderViewport = (OvrRecti[]) new OvrRecti().toArray(2); protected final OvrVector2f[][] uvScaleOffset = new OvrVector2f[2][2]; protected final EyeRenderDesc[] eyeRenderDescs = (EyeRenderDesc[]) new EyeRenderDesc().toArray(2); protected final OvrVector3f[] eyeOffsets = (OvrVector3f[]) new OvrVector3f().toArray(2); protected final FovPort[] eyeFov = (FovPort[]) new FovPort().toArray(2); protected final Posef[] eyePoses = (Posef[]) new Posef().toArray(2); protected int[][] distortionObjects; protected int indicesCount; protected final FrameBuffer frameBuffer = new FrameBuffer(); protected final DistortionShader distortionShader = new DistortionShader(); protected int frameCount = -1; protected boolean inited = false; protected boolean renderEyes = false; protected int eye = 0; //0 == left, 1 == right protected Matrix pretransformedModelView = Matrix.IDENTITY; protected boolean disableHeadTransform = false; protected boolean shouldRenderForHMD() { return renderEyes; } protected static Hmd openFirstHmd() { Hmd hmd = Hmd.create(0); if (null == hmd) { hmd = Hmd.createDebug(ovrHmd_DK1); } return hmd; } @Override public void installed(IDelegateView view) { Hmd.initialize(); try { Thread.sleep(500); } catch (InterruptedException e) { } hmd = openFirstHmd(); if (hmd == null) { throw new IllegalStateException("Unable to initialize HMD"); } if (hmd.configureTracking(ovrTrackingCap_Orientation | ovrTrackingCap_Position, 0) == 0) { throw new IllegalStateException("Unable to start the sensor"); } //hmd.enableHswDisplay(false); } @Override public void uninstalled(IDelegateView view) { hmd.destroy(); Hmd.shutdown(); } @Override public void beforeComputeMatrices(IDelegateView view) { if (shouldRenderForHMD()) { //double hfovRadians = Math.atan(eyeFov[eye].LeftTan) + Math.atan(eyeFov[eye].RightTan); //double vfovRadians = Math.atan(eyeFov[eye].UpTan) + Math.atan(eyeFov[eye].DownTan); //view.setFieldOfView(Angle.fromRadians(hfovRadians)); //TODO hmmm, something about the reported Oculus FOV above and the World Wind's View FOV //don't mix very well, so set it manually to a high FOV to ensure all tiles are rendered: view.setFieldOfView(Angle.fromDegrees(130)); } } @Override public Matrix computeModelView(IDelegateView view) { pretransformedModelView = view.computeModelView(); return transformModelView(pretransformedModelView, view); } protected Matrix transformModelView(Matrix modelView, IDelegateView view) { if (disableHeadTransform) { return modelView; } Vec4 translation = RiftUtils.toVec4(eyePoses[eye].Position); double translationScale = getHeadTranslationScale(view); Quaternion rotation = RiftUtils.toQuaternion(eyePoses[eye].Orientation); Matrix translationM = Matrix.fromTranslation(translation.multiply3(-translationScale)); Matrix rotationM = Matrix.fromQuaternion(rotation.getInverse()); return rotationM.multiply(translationM.multiply(modelView)); } protected double getHeadTranslationScale(IDelegateView view) { Position eyePosition = view.getEyePosition(); DrawContext dc = view.getDC(); if (dc == null) { return Math.abs(eyePosition.elevation); } double altitude = ViewUtil.computeElevationAboveSurface(dc, eyePosition); return Math.abs(altitude); } @Override public Matrix getPretransformedModelView(IDelegateView view) { return pretransformedModelView; } @Override public Matrix computeProjection(IDelegateView view, Angle horizontalFieldOfView, double nearDistance, double farDistance) { if (!shouldRenderForHMD()) { return view.computeProjection(horizontalFieldOfView, nearDistance, farDistance); } return RiftUtils.toMatrix(Hmd.getPerspectiveProjection(eyeFov[eye], (float) nearDistance, (float) farDistance, true)); } protected void init(GL2 gl) { if (inited) { return; } inited = true; initOculus(gl); } protected void initOculus(GL2 gl) { eyeFov[0] = hmd.DefaultEyeFov[0]; eyeFov[1] = hmd.DefaultEyeFov[1]; initDistortion(gl); hmd.setEnabledCaps(ovrHmdCap_LowPersistence | ovrHmdCap_DynamicPrediction); hmd.configureTracking(ovrTrackingCap_Orientation | ovrTrackingCap_MagYawCorrection | ovrTrackingCap_Position, 0); hmd.recenterPose(); } protected void initDistortion(GL2 gl) { //Configure Stereo settings. OvrSizei recommendedTex0Size = hmd.getFovTextureSize(ovrEye_Left, hmd.DefaultEyeFov[0], 1f); OvrSizei recommendedTex1Size = hmd.getFovTextureSize(ovrEye_Right, hmd.DefaultEyeFov[1], 1f); int x = recommendedTex0Size.w + recommendedTex1Size.w; int y = Math.max(recommendedTex0Size.h, recommendedTex1Size.h); OvrSizei renderTargetSize = new OvrSizei(x, y); frameBuffer.resize(gl, new Dimension(x, y)); // Initialize eye rendering information. eyeRenderViewport[0].Pos = new OvrVector2i(0, 0); eyeRenderViewport[0].Size = new OvrSizei(renderTargetSize.w / 2, renderTargetSize.h); eyeRenderViewport[1].Pos = new OvrVector2i((renderTargetSize.w + 1) / 2, 0); eyeRenderViewport[1].Size = eyeRenderViewport[0].Size; distortionShader.create(gl); distortionObjects = new int[ovrEye_Count][DistortionObjects.count.ordinal()]; for (int eyeNum = 0; eyeNum < ovrEye_Count; eyeNum++) { int distortionCaps = ovrDistortionCap_Chromatic | ovrDistortionCap_TimeWarp | ovrDistortionCap_Vignette; DistortionMesh meshData = hmd.createDistortionMesh(eyeNum, eyeFov[eyeNum], distortionCaps); DistortionVertex[] distortionVertices = new DistortionVertex[meshData.VertexCount]; meshData.pVertexData.toArray(distortionVertices); { initDistortionVBOs(gl, eyeNum, distortionVertices); } short[] indicesData = meshData.pIndexData.getPointer().getShortArray(0, meshData.IndexCount); indicesCount = indicesData.length; { initDistortionIBO(gl, eyeNum, indicesData); } eyeRenderDescs[eyeNum] = hmd.getRenderDesc(eyeNum, eyeFov[eyeNum]); uvScaleOffset[eyeNum] = Hmd.getRenderScaleAndOffset(eyeFov[eyeNum], renderTargetSize, eyeRenderViewport[eyeNum]); eyeOffsets[eyeNum].x = eyeRenderDescs[eyeNum].HmdToEyeViewOffset.x; eyeOffsets[eyeNum].y = eyeRenderDescs[eyeNum].HmdToEyeViewOffset.y; eyeOffsets[eyeNum].z = eyeRenderDescs[eyeNum].HmdToEyeViewOffset.z; } } protected void initDistortionVBOs(GL2 gl, int eyeNum, DistortionVertex[] structures) { int vertexSize = 2 + 1 + 1 + 2 + 2 + 2; float[] vertexData = new float[vertexSize * structures.length]; for (int v = 0; v < structures.length; v++) { vertexData[v * vertexSize + 0] = structures[v].ScreenPosNDC.x; vertexData[v * vertexSize + 1] = structures[v].ScreenPosNDC.y; vertexData[v * vertexSize + 2] = structures[v].TimeWarpFactor; vertexData[v * vertexSize + 3] = structures[v].VignetteFactor; vertexData[v * vertexSize + 4] = structures[v].TanEyeAnglesR.x; vertexData[v * vertexSize + 5] = structures[v].TanEyeAnglesR.y; vertexData[v * vertexSize + 6] = structures[v].TanEyeAnglesG.x; vertexData[v * vertexSize + 7] = structures[v].TanEyeAnglesG.y; vertexData[v * vertexSize + 8] = structures[v].TanEyeAnglesB.x; vertexData[v * vertexSize + 9] = structures[v].TanEyeAnglesB.y; } gl.glGenBuffers(1, distortionObjects[eyeNum], DistortionObjects.vbo.ordinal()); gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, distortionObjects[eyeNum][DistortionObjects.vbo.ordinal()]); { FloatBuffer fb = GLBuffers.newDirectFloatBuffer(vertexData); gl.glBufferData(GL2.GL_ARRAY_BUFFER, vertexData.length * 4, fb, GL2.GL_STATIC_DRAW); } gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, 0); } protected void initDistortionIBO(GL2 gl, int eyeNum, short[] shortIndicesData) { int[] intIndicesData = new int[shortIndicesData.length]; for (int i = 0; i < shortIndicesData.length; i++) { intIndicesData[i] = shortIndicesData[i]; } gl.glGenBuffers(1, distortionObjects[eyeNum], DistortionObjects.ibo.ordinal()); gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, distortionObjects[eyeNum][DistortionObjects.ibo.ordinal()]); { IntBuffer intBuffer = GLBuffers.newDirectIntBuffer(intIndicesData); gl.glBufferData(GL2.GL_ELEMENT_ARRAY_BUFFER, intIndicesData.length * 4, intBuffer, GL2.GL_STATIC_DRAW); } gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, 0); } @Override public void pick(IDelegateView view, DrawContext dc, DrawableSceneController sc) { view.pick(dc, sc); sc.clearFrame(dc); fixViewportCenterPosition(dc, sc); } @Override public void draw(IDelegateView view, DrawContext dc, DrawableSceneController sc) { GL2 gl = dc.getGL().getGL2(); init(gl); if (distortionShader.isCreationFailed()) { view.draw(dc, sc); return; } Rectangle oldViewport = view.getViewport(); hmd.beginFrameTiming(++frameCount); { Posef[] eyePoses = hmd.getEyePoses(frameCount, eyeOffsets); //RiftLogger.logPose(eyePoses); renderEyes = true; frameBuffer.bind(gl); { sc.clearFrame(dc); for (int i = 0; i < ovrEye_Count; i++) { int eye = hmd.EyeRenderOrder[i]; Posef pose = eyePoses[eye]; this.eyePoses[eye].Orientation = pose.Orientation; this.eyePoses[eye].Position = pose.Position; this.eye = eye; gl.glViewport(eyeRenderViewport[eye].Pos.x, eyeRenderViewport[eye].Pos.y, eyeRenderViewport[eye].Size.w, eyeRenderViewport[eye].Size.h); sc.applyView(dc); sc.draw(dc); } } frameBuffer.unbind(gl); renderEyes = false; OGLStackHandler oglsh = new OGLStackHandler(); oglsh.pushAttrib(gl, GL2.GL_ENABLE_BIT); oglsh.pushClientAttrib(gl, GL2.GL_CLIENT_VERTEX_ARRAY_BIT); try { gl.glViewport(0, 0, hmd.Resolution.w, hmd.Resolution.h); gl.glDisable(GL2.GL_DEPTH_TEST); gl.glEnable(GL2.GL_TEXTURE_2D); gl.glActiveTexture(GL2.GL_TEXTURE0); gl.glBindTexture(GL2.GL_TEXTURE_2D, frameBuffer.getTexture().getId()); for (int eyeNum = 0; eyeNum < ovrEye_Count; eyeNum++) { OvrMatrix4f[] timeWarpMatricesRowMajor = new OvrMatrix4f[2]; hmd.getEyeTimewarpMatrices(eyeNum, eyePoses[eyeNum], timeWarpMatricesRowMajor); distortionShader.use(gl, uvScaleOffset[eyeNum][0].x, -uvScaleOffset[eyeNum][0].y, uvScaleOffset[eyeNum][1].x, 1 - uvScaleOffset[eyeNum][1].y, timeWarpMatricesRowMajor[0].M, timeWarpMatricesRowMajor[1].M); gl.glClientActiveTexture(GL2.GL_TEXTURE0); gl.glEnableClientState(GL2.GL_VERTEX_ARRAY); gl.glEnableClientState(GL2.GL_TEXTURE_COORD_ARRAY); gl.glEnableClientState(GL2.GL_COLOR_ARRAY); gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, distortionObjects[eyeNum][DistortionObjects.vbo.ordinal()]); { int stride = 10 * 4; gl.glVertexPointer(4, GL2.GL_FLOAT, stride, 0); gl.glTexCoordPointer(2, GL2.GL_FLOAT, stride, 4 * 4); gl.glColorPointer(4, GL2.GL_FLOAT, stride, 6 * 4); gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, distortionObjects[eyeNum][DistortionObjects.ibo.ordinal()]); { gl.glDrawElements(GL2.GL_TRIANGLES, indicesCount, GL2.GL_UNSIGNED_INT, 0); } gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, 0); } gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, 0); distortionShader.unuse(gl); } } finally { oglsh.pop(gl); } } hmd.endFrameTiming(); //apply the old viewport, and ensure that the view is updated for the next picking round gl.glViewport(oldViewport.x, oldViewport.y, oldViewport.width, oldViewport.height); sc.applyView(dc); view.firePropertyChange(AVKey.VIEW, null, view); //make the view draw repeatedly for oculus rotation } @Override public boolean isTranslateAbsAllowed() { return false; } /** * Transforming the modelview matrix with the Rift's head rotation causes * the {@link OrbitView#focusOnViewportCenter()} method to focus on the * center of the viewport, which means the view jumps around depending on * the head rotation. This method calls the focus method using an * untransformed modelview matrix, keeping the center rotation point more * consistent. * * @param dc */ protected void fixViewportCenterPosition(DrawContext dc, DrawableSceneController sc) { dc.setViewportCenterPosition(null); Point vpc = dc.getViewportCenterScreenPoint(); if (vpc == null) { return; } try { disableHeadTransform = true; sc.applyView(dc); dc.enablePickingMode(); List<Point> points = Arrays.asList(new Point[] { vpc }); List<PickedObject> pickedObjects = dc.getSurfaceGeometry().pick(dc, points); if (pickedObjects == null || pickedObjects.size() == 0) { return; } dc.setViewportCenterPosition((Position) pickedObjects.get(0).getObject()); } finally { disableHeadTransform = false; sc.applyView(dc); dc.disablePickingMode(); } } }