/*******************************************************************************
* 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.tests.g3d.shadows.system;
import java.util.EnumSet;
import java.util.Set;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.Cubemap;
import com.badlogic.gdx.graphics.Cubemap.CubemapSide;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.graphics.g3d.RenderableProvider;
import com.badlogic.gdx.graphics.g3d.environment.BaseLight;
import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight;
import com.badlogic.gdx.graphics.g3d.environment.PointLight;
import com.badlogic.gdx.graphics.g3d.environment.SpotLight;
import com.badlogic.gdx.graphics.g3d.utils.ShaderProvider;
import com.badlogic.gdx.graphics.glutils.FrameBuffer;
import com.badlogic.gdx.tests.g3d.shadows.utils.AABBNearFarAnalyzer;
import com.badlogic.gdx.tests.g3d.shadows.utils.BoundingSphereDirectionalAnalyzer;
import com.badlogic.gdx.tests.g3d.shadows.utils.DirectionalAnalyzer;
import com.badlogic.gdx.tests.g3d.shadows.utils.FixedShadowMapAllocator;
import com.badlogic.gdx.tests.g3d.shadows.utils.FrustumLightFilter;
import com.badlogic.gdx.tests.g3d.shadows.utils.LightFilter;
import com.badlogic.gdx.tests.g3d.shadows.utils.NearFarAnalyzer;
import com.badlogic.gdx.tests.g3d.shadows.utils.ShadowMapAllocator;
import com.badlogic.gdx.tests.g3d.shadows.utils.ShadowMapAllocator.ShadowMapRegion;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.ObjectMap.Entries;
/** BaseShadowSystem allows to easily create custom shadow system.
* @author realitix */
public abstract class BaseShadowSystem implements ShadowSystem, Disposable {
/** This class handles camera and texture region.
* @author realitix */
public static class LightProperties {
public Camera camera;
public TextureRegion region = new TextureRegion();
public LightProperties (Camera camera) {
this.camera = camera;
}
}
/** This class handles LightProperties for each side of PointLight.
* @author realitix */
public static class PointLightProperties {
public ObjectMap<CubemapSide, LightProperties> properties = new ObjectMap<CubemapSide, LightProperties>(6);
}
/** Main camera */
protected Camera camera;
/** Renderable providers */
protected Iterable<RenderableProvider> renderableProviders;
/** Cameras linked with spot lights */
protected ObjectMap<SpotLight, LightProperties> spotCameras = new ObjectMap<SpotLight, LightProperties>();
/** Cameras linked with directional lights */
protected ObjectMap<DirectionalLight, LightProperties> dirCameras = new ObjectMap<DirectionalLight, LightProperties>();
/** Cameras linked with point lights */
protected ObjectMap<PointLight, PointLightProperties> pointCameras = new ObjectMap<PointLight, PointLightProperties>();
/** Analyzer of near and far for spot and point lights */
protected NearFarAnalyzer nearFarAnalyzer;
/** Allocator which choose where to render shadow map in texture */
protected ShadowMapAllocator allocator;
/** Analyzer which compute how to create the camera for directional light */
protected DirectionalAnalyzer directionalAnalyzer;
/** Filter that choose if light must be rendered */
protected LightFilter lightFilter;
/** Framebuffer used to render all the depth maps */
protected FrameBuffer[] frameBuffers;
/** Current pass in the depth process */
protected int currentPass = -1;
/** Iterators for cameras */
protected Entries<SpotLight, LightProperties> spotCameraIterator;
protected Entries<DirectionalLight, LightProperties> dirCameraIterator;
protected Entries<PointLight, PointLightProperties> pointCameraIterator;
/** Current side in the point light cubemap */
protected int currentPointSide;
protected PointLightProperties currentPointProperties;
/** Shader providers used by this system */
protected ShaderProvider[] passShaderProviders;
protected ShaderProvider mainShaderProvider;
/** Current light and properties during shadowmap generation */
protected LightProperties currentLightProperties;
protected BaseLight currentLight;
/** Construct the system with the needed params.
* @param nearFarAnalyzer Analyzer of near and far
* @param allocator Allocator of shadow maps
* @param directionalAnalyzer Analyze directional light to create orthographic camera
* @param lightFilter Filter light to render */
public BaseShadowSystem (NearFarAnalyzer nearFarAnalyzer, ShadowMapAllocator allocator,
DirectionalAnalyzer directionalAnalyzer, LightFilter lightFilter) {
this.nearFarAnalyzer = nearFarAnalyzer;
this.allocator = allocator;
this.directionalAnalyzer = directionalAnalyzer;
this.lightFilter = lightFilter;
}
/** Construct the system with default values */
public BaseShadowSystem () {
this(new AABBNearFarAnalyzer(), new FixedShadowMapAllocator(FixedShadowMapAllocator.QUALITY_MED,
FixedShadowMapAllocator.QUANTITY_MAP_MED), new BoundingSphereDirectionalAnalyzer(), new FrustumLightFilter());
}
/** Initialize framebuffers and shader providers. You should call super.init() in subclass. */
@Override
public void init () {
frameBuffers = new FrameBuffer[getPassQuantity()];
passShaderProviders = new ShaderProvider[getPassQuantity()];
for (int i = 0; i < getPassQuantity(); i++) {
init(i);
}
};
/** Initialize pass n */
protected abstract void init (int n);
/** getPassQuantity should return at leat one. */
@Override
public abstract int getPassQuantity ();
@Override
public ShaderProvider getPassShaderProvider (int n) {
return passShaderProviders[n];
}
@Override
public ShaderProvider getShaderProvider () {
return mainShaderProvider;
}
@Override
public void addLight (SpotLight spot) {
PerspectiveCamera camera = new PerspectiveCamera(spot.cutoffAngle, 0, 0);
camera.position.set(spot.position);
camera.direction.set(spot.direction);
camera.near = 1;
camera.far = 100;
camera.up.set(camera.direction.y, camera.direction.z, camera.direction.x);
spotCameras.put(spot, new LightProperties(camera));
}
@Override
public void addLight (DirectionalLight dir) {
OrthographicCamera camera = new OrthographicCamera();
camera.direction.set(dir.direction);
camera.near = 1;
camera.far = 100;
dirCameras.put(dir, new LightProperties(camera));
}
@Override
public void addLight (PointLight point) {
addLight(point, EnumSet.of(CubemapSide.PositiveX, CubemapSide.NegativeX, CubemapSide.PositiveY, CubemapSide.NegativeY,
CubemapSide.PositiveZ, CubemapSide.NegativeZ));
}
@Override
public void addLight (PointLight point, Set<CubemapSide> sides) {
PointLightProperties plProperty = new PointLightProperties();
for (int i = 0; i < 6; i++) {
CubemapSide cubemapSide = Cubemap.CubemapSide.values()[i];
if (sides.contains(cubemapSide)) {
PerspectiveCamera camera = new PerspectiveCamera(90, 0, 0);
camera.position.set(point.position);
camera.direction.set(cubemapSide.direction);
camera.up.set(cubemapSide.up);
camera.near = 1;
camera.far = 100;
LightProperties p = new LightProperties(camera);
plProperty.properties.put(cubemapSide, p);
}
}
pointCameras.put(point, plProperty);
}
@Override
public void removeLight (SpotLight spot) {
spotCameras.remove(spot);
}
@Override
public void removeLight (DirectionalLight dir) {
dirCameras.remove(dir);
}
@Override
public void removeLight (PointLight point) {
pointCameras.remove(point);
}
@Override
public boolean hasLight (SpotLight spot) {
if (spotCameras.containsKey(spot)) return true;
return false;
}
@Override
public boolean hasLight (DirectionalLight dir) {
if (dirCameras.containsKey(dir)) return true;
return false;
}
@Override
public boolean hasLight (PointLight point) {
if (pointCameras.containsKey(point)) return true;
return false;
}
@Override
public void update () {
for (ObjectMap.Entry<SpotLight, LightProperties> e : spotCameras) {
e.value.camera.position.set(e.key.position);
e.value.camera.direction.set(e.key.direction);
nearFarAnalyzer.analyze(e.key, e.value.camera, renderableProviders);
}
for (ObjectMap.Entry<DirectionalLight, LightProperties> e : dirCameras) {
directionalAnalyzer.analyze(e.key, e.value.camera, camera).update();
}
for (ObjectMap.Entry<PointLight, PointLightProperties> e : pointCameras) {
for (ObjectMap.Entry<CubemapSide, LightProperties> c : e.value.properties) {
c.value.camera.position.set(e.key.position);
nearFarAnalyzer.analyze(e.key, c.value.camera, renderableProviders);
}
}
}
@Override
public <T extends RenderableProvider> void begin (Camera camera, final Iterable<T> renderableProviders) {
if (this.renderableProviders != null || this.camera != null) throw new GdxRuntimeException("Call end() first.");
this.camera = camera;
this.renderableProviders = (Iterable<RenderableProvider>)renderableProviders;
}
@Override
public void begin (int n) {
if (n >= passShaderProviders.length)
throw new GdxRuntimeException("Pass " + n + " doesn't exist in " + getClass().getName());
currentPass = n;
spotCameraIterator = spotCameras.iterator();
dirCameraIterator = dirCameras.iterator();
pointCameraIterator = pointCameras.iterator();
currentPointSide = 6;
beginPass(n);
}
/** Begin pass n.
* @param n Pass number */
protected void beginPass (int n) {
frameBuffers[n].begin();
};
@Override
public void end () {
this.camera = null;
this.renderableProviders = null;
currentPass = -1;
}
@Override
public void end (int n) {
if (currentPass != n) throw new GdxRuntimeException("Begin " + n + " must be called before end " + n);
endPass(n);
}
/** End pass n.
* @param n Pass number */
protected void endPass (int n) {
frameBuffers[n].end();
}
@Override
public Camera next () {
LightProperties lp = nextDirectional();
if (lp != null) return interceptCamera(lp);
lp = nextSpot();
if (lp != null) return interceptCamera(lp);
lp = nextPoint();
if (lp != null) return interceptCamera(lp);
return null;
}
/** Allows to return custom camera if needed.
* @param lp Returned LightProperties
* @return Camera */
protected Camera interceptCamera (LightProperties lp) {
return lp.camera;
}
protected LightProperties nextDirectional () {
if (!dirCameraIterator.hasNext()) return null;
ObjectMap.Entry<DirectionalLight, LightProperties> e = dirCameraIterator.next();
currentLight = e.key;
currentLightProperties = e.value;
LightProperties lp = e.value;
processViewport(lp, false);
return lp;
}
protected LightProperties nextSpot () {
if (!spotCameraIterator.hasNext()) return null;
ObjectMap.Entry<SpotLight, LightProperties> e = spotCameraIterator.next();
currentLight = e.key;
currentLightProperties = e.value;
LightProperties lp = e.value;
if (!lightFilter.filter(spotCameras.findKey(lp, true), lp.camera, this.camera)) {
return nextSpot();
}
processViewport(lp, true);
return lp;
}
protected LightProperties nextPoint () {
if (!pointCameraIterator.hasNext() && currentPointSide > 5) return null;
if (currentPointSide > 5) currentPointSide = 0;
if (currentPointSide == 0) {
ObjectMap.Entry<PointLight, PointLightProperties> e = pointCameraIterator.next();
currentLight = e.key;
currentPointProperties = e.value;
}
if (currentPointProperties.properties.containsKey(Cubemap.CubemapSide.values()[currentPointSide])) {
LightProperties lp = currentPointProperties.properties.get(Cubemap.CubemapSide.values()[currentPointSide]);
currentLightProperties = lp;
currentPointSide += 1;
if (!lightFilter.filter(pointCameras.findKey(currentPointProperties, true), lp.camera, this.camera)) {
return nextPoint();
}
processViewport(lp, true);
return lp;
}
currentPointSide += 1;
return nextPoint();
}
/** Set viewport according to allocator.
* @param lp LightProperties to process.
* @param cameraViewport Set camera viewport if true. */
protected void processViewport (LightProperties lp, boolean cameraViewport) {
Camera camera = lp.camera;
ShadowMapRegion r = allocator.nextResult(currentLight);
if (r == null) return;
TextureRegion region = lp.region;
region.setTexture(frameBuffers[currentPass].getColorBufferTexture());
// We don't use HdpiUtils
// gl commands related to shadow map size and not to screen size
Gdx.gl.glViewport(r.x, r.y, r.width, r.height);
Gdx.gl.glScissor(r.x + 1, r.y + 1, r.width - 2, r.height - 2);
region.setRegion(r.x, r.y, r.width, r.height);
if (cameraViewport) {
camera.viewportHeight = r.height;
camera.viewportWidth = r.width;
camera.update();
}
}
public ObjectMap<DirectionalLight, LightProperties> getDirectionalCameras () {
return dirCameras;
}
public ObjectMap<SpotLight, LightProperties> getSpotCameras () {
return spotCameras;
}
public ObjectMap<PointLight, PointLightProperties> getPointCameras () {
return pointCameras;
}
public Texture getTexture (int n) {
if (n >= getPassQuantity()) throw new GdxRuntimeException("Can't get texture " + n);
return frameBuffers[n].getColorBufferTexture();
}
public LightProperties getCurrentLightProperties () {
return currentLightProperties;
}
public BaseLight getCurrentLight () {
return currentLight;
}
public int getCurrentPass () {
return currentPass;
}
@Override
public void dispose () {
for (int i = 0; i < getPassQuantity(); i++) {
frameBuffers[i].dispose();
passShaderProviders[i].dispose();
}
mainShaderProvider.dispose();
}
}