/******************************************************************************* * Copyright 2011 See AUTHORS file. * * 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 com.badlogic.gdx.vr; import com.badlogic.gdx.graphics.Camera; import com.badlogic.gdx.math.Matrix4; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.math.Vector3; import com.badlogic.gdx.math.collision.Ray; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.viewport.Viewport; /** * This viewport can be used to split up the screen into different regions which * can be rendered each on their own. It actually consists of several other * viewports. It has one "root" viewport which is used to define the area that * can be used by the "sub" viewports. The "sub" viewports will split this area * into several areas. <br /> * To render in a certain "sub" viewport, this viewport needs to be activated * first. This will result in a layouting of this viewport and its * {@link Viewport#update(int, int, boolean)} method being called to setup the * camera and the OpenGL viewport (glViewport) correctly. * * @author Daniel Holderbaum */ public class SplitViewport extends Viewport { /** @author Daniel Holderbaum */ public static class SizeInformation { /** Determines, how the size should be interpreted. */ public SizeType sizeType; /** * The size to be used. Is ignored in case {@link SizeType} REST is * used. */ public float size; public SizeInformation(SizeType sizeType, float size) { this.sizeType = sizeType; this.size = size; } } /** * An enum which determines how a size should be interpreted. * * @author Daniel Holderbaum */ public enum SizeType { /** * The size will be fixed and will have exactly the given size all the * time. */ ABSOLUTE, /** * The given size needs to be in [0, 1]. It is relative to the "root" * viewport. */ RELATIVE, /** * If this type is chosen, the given size will be ignored. Instead all * cells with this type will share the rest amount of the "root" * viewport that is still left after all other parts have been * subtracted. */ REST } /** * A sub view for one cell of the {@link SplitViewport}. * * @author Daniel Holderbaum */ public static class SubView { /** The size information for this sub view. */ public SizeInformation sizeInformation; /** The {@link Viewport} for this sub view. */ public Viewport viewport; public SubView(SizeInformation sizeInformation, Viewport viewport) { this.sizeInformation = sizeInformation; this.viewport = viewport; } } private Viewport rootViewport; private Viewport activeViewport; private Array<SubView> rowSizeInformations = new Array<SubView>(); private Array<Array<SubView>> subViews = new Array<Array<SubView>>(); /** * Initializes the split viewport. * * @param rootViewport * The viewport to be used to determine the area which the sub * viewports can use */ public SplitViewport(Viewport rootViewport) { this.rootViewport = rootViewport; } /** * Adds another row to the split viewport. This has to be called at least * once prior to {@link #add(SubView)}. * * @param sizeInformation * The size information for the row. */ public void row(SizeInformation sizeInformation) { if (sizeInformation.sizeType == SizeType.RELATIVE) { validateRelativeSize(sizeInformation.size); } // for rows we don't need a SubView with a viewport, but to not // duplicate some calculation methods, we just create a new // SubView rowSizeInformations.add(new SubView(sizeInformation, null)); subViews.add(new Array<SubView>()); } /** * Adds another sub view to the last added row. * * @param subView * The {@link SubView} with size and viewport. It can be changed * externally. Those changes will be used as soon as the viewport * is activated next time. */ public void add(SubView subView) { if (subViews.size == 0) { throw new IllegalStateException("A row has to be added first."); } if (subView.sizeInformation.sizeType == SizeType.RELATIVE) { validateRelativeSize(subView.sizeInformation.size); } Array<SubView> rowViewports = subViews.peek(); rowViewports.add(subView); } private final Rectangle subViewportArea = new Rectangle(); /** * Updates the viewport at (row, column) and sets it as the currently active * one. The top left sub viewport is (0, 0). * * @param row * The index of the row with the viewport to be activated. Starts * at 0. * @param column * The index of the column with the viewport to be activated. * Starts at 0. * @param centerCamera * Whether the subView should center the camera or not. */ public void activateSubViewport(int row, int column, boolean centerCamera) { validateCoordinates(row, column); Array<SubView> rowMap = subViews.get(row); Viewport viewport = rowMap.get(column).viewport; // update the viewport simulating a smaller sub view calculateSubViewportArea(row, column, subViewportArea); viewport.update((int) subViewportArea.width, (int) subViewportArea.height, centerCamera); // store the current world size so we can restore it in case it gets // changed now float originalWorldWidth = viewport.getWorldWidth(); float originalWorldHeight = viewport.getWorldHeight(); // some scaling strategies will scale the viewport bigger than the // allowed sub view, so we need to limit it if (viewport.getScreenWidth() > subViewportArea.width) { float offcutWidth = viewport.getScreenWidth() - subViewportArea.width; viewport.setScreenWidth((int) subViewportArea.width); viewport.setWorldWidth(viewport.getWorldWidth() - offcutWidth); viewport.setScreenX((int) (viewport.getScreenX() + offcutWidth / 2)); } if (viewport.getScreenHeight() > subViewportArea.height) { float offcutHeight = viewport.getScreenHeight() - subViewportArea.height; viewport.setScreenHeight((int) subViewportArea.height); viewport.setWorldHeight(viewport.getWorldHeight() - offcutHeight); viewport.setScreenY((int) (viewport.getScreenY() + offcutHeight / 2)); } // now shift it to the correct place viewport.setScreenX((int) (viewport.getScreenX() + subViewportArea.x)); viewport.setScreenY((int) (viewport.getScreenY() + subViewportArea.y)); // we changed the viewport parameters, now we need to update once more // to correct the glViewport viewport.apply(); // restore the original world width after the glViewport has been set viewport.setWorldWidth(originalWorldWidth); viewport.setWorldHeight(originalWorldHeight); activeViewport = viewport; } public Viewport getRootViewport() { return rootViewport; } public void setRootViewport(Viewport rootViewport) { this.rootViewport = rootViewport; } // ############################################################ // The following methods all just delegate to the root viewport // ############################################################ @Override public void update(int screenWidth, int screenHeight, boolean centerCamera) { rootViewport.update(screenWidth, screenHeight, centerCamera); } @Override public Vector2 unproject(Vector2 screenCoords) { return rootViewport.unproject(screenCoords); } @Override public Vector2 project(Vector2 worldCoords) { return rootViewport.project(worldCoords); } @Override public Vector3 unproject(Vector3 screenCoords) { return rootViewport.unproject(screenCoords); } @Override public Vector3 project(Vector3 worldCoords) { return rootViewport.project(worldCoords); } @Override public Ray getPickRay(float screenX, float screenY) { return rootViewport.getPickRay(screenX, screenY); } @Override public void calculateScissors(Matrix4 batchTransform, Rectangle area, Rectangle scissor) { rootViewport.calculateScissors(batchTransform, area, scissor); } @Override public Vector2 toScreenCoordinates(Vector2 worldCoords, Matrix4 transformMatrix) { return rootViewport.toScreenCoordinates(worldCoords, transformMatrix); } @Override public Camera getCamera() { return rootViewport.getCamera(); } @Override public void setCamera(Camera camera) { rootViewport.setCamera(camera); } @Override public void setWorldSize(float worldWidth, float worldHeight) { rootViewport.setWorldSize(worldWidth, worldHeight); } @Override public float getWorldWidth() { return rootViewport.getWorldWidth(); } @Override public void setWorldWidth(float worldWidth) { rootViewport.setWorldWidth(worldWidth); } @Override public float getWorldHeight() { return rootViewport.getWorldHeight(); } @Override public void setWorldHeight(float worldHeight) { rootViewport.setWorldHeight(worldHeight); } @Override public int getScreenX() { return rootViewport.getScreenX(); } @Override public int getScreenY() { return rootViewport.getScreenY(); } @Override public int getScreenWidth() { return rootViewport.getScreenWidth(); } @Override public int getScreenHeight() { return rootViewport.getScreenHeight(); } @Override public int getLeftGutterWidth() { return rootViewport.getLeftGutterWidth(); } @Override public int getRightGutterX() { return rootViewport.getRightGutterX(); } @Override public int getRightGutterWidth() { return rootViewport.getRightGutterWidth(); } @Override public int getBottomGutterHeight() { return rootViewport.getBottomGutterHeight(); } @Override public int getTopGutterY() { return rootViewport.getTopGutterY(); } @Override public int getTopGutterHeight() { return rootViewport.getTopGutterHeight(); } // ################################################################# // Private utility methods to help with calculations and validations // ################################################################# private Rectangle calculateSubViewportArea(int row, int col, Rectangle subViewportArea) { subViewportArea.x = calculateWidthOffset(subViews.get(row), col); subViewportArea.y = calculateHeightOffset(rowSizeInformations, row); subViewportArea.width = calculateSize(subViews.get(row), col, getScreenWidth()); subViewportArea.height = calculateSize(rowSizeInformations, row, getScreenHeight()); return subViewportArea; } private float calculateHeightOffset(Array<SubView> subViews, int index) { // the glViewport offset is y-up, but the first row is the top most one // that's why we start at the top and subtract the row heights float heightOffset = getScreenHeight(); for (int i = 0; i <= index; i++) { heightOffset -= calculateSize(subViews, i, getScreenHeight()); } // add the root offset heightOffset += getScreenY(); return heightOffset; } private float calculateWidthOffset(Array<SubView> sizeInformations, int index) { float widthOffset = 0; for (int i = 0; i < index; i++) { widthOffset += calculateSize(sizeInformations, i, getScreenWidth()); } // add the root offset widthOffset += getScreenX(); return widthOffset; } /** * Used to calculate either the width or height. * * @param subViews * The row informations or column informations of a certain row. * @param index * The index of the element to be calculated. * @param totalSize * The total size, either the viewport width or height. */ private float calculateSize(Array<SubView> subViews, int index, float totalSize) { SubView subView = subViews.get(index); switch (subView.sizeInformation.sizeType) { case ABSOLUTE: return subView.sizeInformation.size; case RELATIVE: return subView.sizeInformation.size * totalSize; case REST: int rests = countRest(subViews); float usedSize = calculateUsedSize(subViews, totalSize); return (totalSize - usedSize) / rests; default: throw new IllegalArgumentException(subView.sizeInformation.sizeType + " could not be handled."); } } private float calculateUsedSize(Array<SubView> subViews, float totalSize) { float usedSize = 0; for (SubView subView : subViews) { switch (subView.sizeInformation.sizeType) { case ABSOLUTE: usedSize += subView.sizeInformation.size; break; case RELATIVE: usedSize += subView.sizeInformation.size * totalSize; break; } } return usedSize; } private int countRest(Array<SubView> subViews) { int rests = 0; for (SubView subView : subViews) { if (subView.sizeInformation.sizeType == SizeType.REST) { rests++; } } return rests; } private void validateCoordinates(int row, int col) { if (row >= subViews.size) { throw new IllegalArgumentException("There is no row with ID " + row); } Array<SubView> rowSubViews = subViews.get(row); if (col >= rowSubViews.size) { throw new IllegalArgumentException("There is no column with ID " + col); } } private void validateRelativeSize(float size) { if (size < 0 || size > 1) { throw new IllegalArgumentException(size + " does not fulfill the constraint of 0 <= size <= 1."); } } }