/*******************************************************************************
* 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.");
}
}
}