From 4145c3c9559d94d8ffbdfc585437f398df0b3e18 Mon Sep 17 00:00:00 2001
From: crykn <crykn@users.noreply.github.com>
Date: Thu, 15 Oct 2020 21:29:05 +0200
Subject: [PATCH] Initial commit.

---
 README.md                                     |  61 +++
 build.gradle                                  |  81 ++++
 .../gdx/utils/reflect/ReflectionUtils.java    |  39 ++
 .../commons/core/EskalonApplication.java      | 286 ++++++++++++++
 .../eskalon/commons/misc/EventQueueBus.java   |  60 +++
 .../screens/AbstractAssetLoadingScreen.java   | 118 ++++++
 .../commons/audio/DesktopSoundInstance.java   |  97 +++++
 .../commons/audio/DesktopSoundManager.java    |  75 ++++
 .../commons/graphics/ForwardRenderer.java     |  41 ++
 .../eskalon/commons/graphics/IRenderer.java   |  31 ++
 .../commons/graphics/PBRTextureAttribute.java | 104 +++++
 .../de/eskalon/commons/graphics/Scene.java    |  71 ++++
 .../eskalon/commons/graphics/WorldObject.java | 114 ++++++
 .../deferredrendering/AmbientLightPass.java   |  94 +++++
 .../deferredrendering/DebugLightPass.java     | 362 ++++++++++++++++++
 .../DefaultGeometryPassShader.java            | 158 ++++++++
 .../deferredrendering/DeferredRenderer.java   | 112 ++++++
 .../deferredrendering/GeometryPass.java       | 115 ++++++
 .../graphics/deferredrendering/LightPass.java |  34 ++
 19 files changed, 2053 insertions(+)
 create mode 100644 README.md
 create mode 100644 build.gradle
 create mode 100644 core/src/main/java/com/badlogic/gdx/utils/reflect/ReflectionUtils.java
 create mode 100644 core/src/main/java/de/eskalon/commons/core/EskalonApplication.java
 create mode 100644 core/src/main/java/de/eskalon/commons/misc/EventQueueBus.java
 create mode 100644 core/src/main/java/de/eskalon/commons/screens/AbstractAssetLoadingScreen.java
 create mode 100644 desktop/src/main/java/de/eskalon/commons/audio/DesktopSoundInstance.java
 create mode 100644 desktop/src/main/java/de/eskalon/commons/audio/DesktopSoundManager.java
 create mode 100644 g3d/src/main/java/de/eskalon/commons/graphics/ForwardRenderer.java
 create mode 100644 g3d/src/main/java/de/eskalon/commons/graphics/IRenderer.java
 create mode 100644 g3d/src/main/java/de/eskalon/commons/graphics/PBRTextureAttribute.java
 create mode 100644 g3d/src/main/java/de/eskalon/commons/graphics/Scene.java
 create mode 100644 g3d/src/main/java/de/eskalon/commons/graphics/WorldObject.java
 create mode 100644 g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/AmbientLightPass.java
 create mode 100644 g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/DebugLightPass.java
 create mode 100644 g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/DefaultGeometryPassShader.java
 create mode 100644 g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/DeferredRenderer.java
 create mode 100644 g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/GeometryPass.java
 create mode 100644 g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/LightPass.java

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..da829b2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,61 @@
+# Pancake
+
+Pancake is the basis for all libGDX games developed by eskalon.
+
+## Technical 
+
+Pancake uses the following libraries and frameworks :
+
+- [libGDX](https://github.com/libgdx/libgdx)
+- [libgdx-screenmanager](https://github.com/crykn/libgdx-screenmanager)
+- [guacamole](https://github.com/crykn/guacamole)
+- [guava](https://github.com/google/guava) (for its EventBus) and [reflections](https://github.com/ronmamo/reflections) (for the classpath scanning in AbstractAssetLoadingScreen)
+
+## Content
+A selection of what Pancake offers:
+
+### <u>core:</u>
+- **AnnotationAssetManager**
+	- `@Asset("cool_texture.jpg")`, `@Asset(value = "ui/skin/skin.json", params = "ui/skin/skin.atlas")`
+	- `#loadAnnotatedAssets(Class<T> clazz)`
+	- `#injectAssets(Class<T> clazz, @Nullable T instance)`
+	- `#registerAssetLoaderParametersFactory(Class<T> clazz, AssetLoaderParametersFactory<T> factory)`
+- **DefaultSoundManager & Playlist**
+	- `#playSoundEffect(String name)`
+	- `#playMusic(String playlistName)`
+	- Playlist files:
+```java
+{
+   name: "best_playlist_ever",
+   shuffle: true,
+   repeat: true,
+   music: [
+        [
+           "My favourite song",
+           "my_favourite_song.mp3"
+        ],
+     ]
+ }
+ ```
+- **EskalonApplication:** the core application, which keeps a sprite batch, an asset manager, a sound manager, etc.
+- **PostProcessingPipeline:** A simple post processing pipeline [WIP]
+- **Lang, ILocalizable & ILocalized**
+	- `Lang#get(String key, Object... args)`
+- **DebugInfoRenderer:** renders some debug information, including a fps graph
+- **AbstractAssetLoadingScreen:** a screen loading all assets annotated with `@Asset` in a specified package 
+- **AbstractEskalonUIScreen:** a screen rendering a background image & a stage
+- **AbstractImageScreen:** a simple screen rendering one image
+- **EskalonSplashScreen:** a splash screen showing the eskalon logo as well as loading some internal assets
+- **EskalonSettings & KeyBinding:** provides settings for different sound volumes & makes handling changeable key bindings very simple
+- **RandomUtils:** contains various methods dealing with random values
+- **GL32CMacIssueHandler:** provides updated versions of the default shaders that are compatible with OpenGL 3.2
+
+### desktop
+- Audio implementations that support spatial audio
+
+### g3d:
+- A **deferred renderer** [WIP]
+
+
+## Collaboration
+A list of all of our current contributors and the used external assets can be found in [CONTRIBUTORS.md](https://github.com/eskalon/pancake/blob/master/CONTRIBUTORS.md).
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..cea57b3
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,81 @@
+buildscript {
+	repositories {
+		mavenLocal()
+		mavenCentral()
+		maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
+		jcenter()
+	}
+	dependencies {
+	}
+}
+
+allprojects {
+	apply plugin: "eclipse"
+	apply plugin: "idea"
+
+	version = "0.1.0"
+	ext {
+		appName = "pancake"
+		gdxVersion = "1.9.11"
+		guavaVersion = "29.0-jre" // is used for the EventBus
+		junitVersion = "5.6.0"
+		mockitoVersion = "3.4.4"
+		reflectionsVersion = "0.9.11" //0.9.12 doesn't work, see https://github.com/ronmamo/reflections/issues/273
+        screenmanagerVersion = "0.6.2"
+    }
+
+    repositories {
+		mavenLocal()
+		mavenCentral()
+		maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
+		maven { url "https://oss.sonatype.org/content/repositories/releases/" }
+		maven { url "https://jitpack.io" }
+		jcenter()
+    }
+}
+
+project(":core") {
+	apply plugin: "java-library"
+
+	dependencies {
+		implementation "com.badlogicgames.gdx:gdx:$gdxVersion"
+		implementation "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
+		implementation "com.google.guava:guava:$guavaVersion"
+		implementation "org.reflections:reflections:$reflectionsVersion"
+		api "com.github.crykn:libgdx-screenmanager:$screenmanagerVersion"
+
+		testImplementation "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
+		testImplementation "org.mockito:mockito-core:$mockitoVersion"
+		testImplementation "com.badlogicgames.gdx:gdx-backend-headless:$gdxVersion"
+		testImplementation "com.badlogicgames.gdx:gdx:$gdxVersion"
+		testImplementation "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop"
+		testImplementation "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
+		testImplementation "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-desktop"
+	}
+
+	sourceSets.main {
+		resources.srcDirs = ["src/main/assets"]
+	}
+
+	sourceSets.test {
+		resources.srcDirs = ["src/test/assets"]
+	}
+}
+
+project(":g3d") {
+	apply plugin: "java-library"
+
+	dependencies {
+		api project(":core")
+		implementation "com.badlogicgames.gdx:gdx-backend-lwjgl3:$gdxVersion"
+	}
+}
+
+project(":desktop") {
+	apply plugin: "java-library"
+
+	dependencies {
+		api project(":core")
+		implementation "com.badlogicgames.gdx:gdx-backend-lwjgl3:$gdxVersion"
+	}
+}
diff --git a/core/src/main/java/com/badlogic/gdx/utils/reflect/ReflectionUtils.java b/core/src/main/java/com/badlogic/gdx/utils/reflect/ReflectionUtils.java
new file mode 100644
index 0000000..4c5be88
--- /dev/null
+++ b/core/src/main/java/com/badlogic/gdx/utils/reflect/ReflectionUtils.java
@@ -0,0 +1,39 @@
+package com.badlogic.gdx.utils.reflect;
+
+import javax.annotation.Nullable;
+
+/**
+ * Reflection utils for fields.
+ * 
+ * @author damios
+ */
+public class ReflectionUtils {
+
+	private ReflectionUtils() {
+		throw new UnsupportedOperationException();
+	}
+
+	public static Field convertFieldObject(java.lang.reflect.Field field) {
+		return new Field(field);
+	}
+
+	/**
+	 * Creates a class via libGDX reflection by using its name. Returns null, if the
+	 * reflection or instantiation fails.
+	 * 
+	 * @param <T>
+	 * @param className
+	 * @param clazz
+	 * @return
+	 */
+	@SuppressWarnings("unchecked")
+	@Nullable
+	public static <T> T newInstance(String className, Class<T> clazz) {
+		try {
+			return (T) ClassReflection.newInstance(ClassReflection.forName(className));
+		} catch (ReflectionException e) {
+			return null;
+		}
+	}
+
+}
diff --git a/core/src/main/java/de/eskalon/commons/core/EskalonApplication.java b/core/src/main/java/de/eskalon/commons/core/EskalonApplication.java
new file mode 100644
index 0000000..b571c97
--- /dev/null
+++ b/core/src/main/java/de/eskalon/commons/core/EskalonApplication.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2020 eskalon
+ * 
+ * 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 de.eskalon.commons.core;
+
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.assets.loaders.resolvers.InternalFileHandleResolver;
+import com.badlogic.gdx.graphics.OrthographicCamera;
+import com.badlogic.gdx.graphics.g2d.BitmapFont;
+import com.badlogic.gdx.graphics.g2d.SpriteBatch;
+import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator;
+import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGeneratorLoader;
+import com.badlogic.gdx.graphics.g2d.freetype.FreetypeFontLoader;
+import com.badlogic.gdx.math.Interpolation;
+import com.badlogic.gdx.scenes.scene2d.ui.Skin;
+import com.badlogic.gdx.utils.reflect.ReflectionUtils;
+
+import de.damios.guacamole.annotations.GwtIncompatible;
+import de.damios.guacamole.gdx.Log;
+import de.damios.guacamole.gdx.assets.Text;
+import de.damios.guacamole.gdx.assets.TextLoader;
+import de.eskalon.commons.asset.AnnotationAssetManager;
+import de.eskalon.commons.asset.BitmapFontAssetLoaderParametersFactory;
+import de.eskalon.commons.asset.PlaylistDefinition;
+import de.eskalon.commons.asset.PlaylistDefinitionLoader;
+import de.eskalon.commons.audio.ISoundManager;
+import de.eskalon.commons.misc.DebugInfoRenderer;
+import de.eskalon.commons.misc.EskalonGameInputProcessor;
+import de.eskalon.commons.misc.EskalonLogger;
+import de.eskalon.commons.misc.EventQueueBus;
+import de.eskalon.commons.screen.transition.ScreenTransition;
+import de.eskalon.commons.screen.transition.impl.BlankTimedTransition;
+import de.eskalon.commons.screen.transition.impl.BlendingTransition;
+import de.eskalon.commons.screens.AbstractEskalonScreen;
+import de.eskalon.commons.screens.BlankEskalonScreen;
+import de.eskalon.commons.screens.EskalonSplashScreen;
+import de.eskalon.commons.screens.EskalonSplashScreen.EskalonCommonsAssets;
+import de.eskalon.commons.utils.ScreenshotUtils;
+import de.eskalon.commons.utils.graphics.GL32CMacIssueHandler;
+
+/**
+ * A basic game application. Takes care of setting some convenience variables
+ * and constants. Furthermore, adds:
+ * <ul>
+ * <li>an {@linkplain #getAssetManager() asset manager}</li>
+ * <li>a {@linkplain #getSoundManager() sound manager}</li>
+ * <li>an {@linkplain #getEventBus() event bus}</li>
+ * <li>a {@linkplain #getSpriteBatch() batch} and an {@linkplain #getUICamera()
+ * ui camera}</li>
+ * <li>support for setting an {@linkplain #uiSkin UI skin}</li>
+ * </ul>
+ * When the application is created, a {@link EskalonSplashScreen} is pushed.
+ * 
+ * @author damios
+ * @see BasicScreenManager
+ */
+public abstract class EskalonApplication extends ManagedGame<AbstractEskalonScreen, ScreenTransition> {
+
+	@GwtIncompatible
+	public final boolean IN_DEV_ENV = EskalonApplication.class.getPackage().getImplementationVersion() == null;
+	@GwtIncompatible
+	public final String VERSION = IN_DEV_ENV ? "Development Build"
+			: EskalonApplication.class.getPackage().getImplementationVersion();
+
+	private DebugInfoRenderer debugInfoRenderer;
+
+	protected AnnotationAssetManager assetManager = new AnnotationAssetManager(new InternalFileHandleResolver());
+
+	protected ISoundManager soundManager;
+
+	private EskalonGameInputProcessor applicationInputProcessor = new EskalonGameInputProcessor();
+
+	protected EventQueueBus eventBus = new EventQueueBus();
+
+	protected SpriteBatch batch;
+	protected OrthographicCamera uiCamera;
+
+	protected Skin uiSkin;
+
+	private boolean debugLogging;
+	private boolean hasDepth;
+
+	protected EskalonApplication() {
+		this(false, false);
+	}
+
+	protected EskalonApplication(boolean debugLogging, boolean hasDepth) {
+		this.debugLogging = debugLogging;
+		this.hasDepth = hasDepth;
+	}
+
+	@Override
+	public final void create() {
+		// Log stuff
+		Gdx.app.setApplicationLogger(new EskalonLogger());
+
+		if (debugLogging)
+			Log.showAll();
+		else
+			Log.showInfoAndErrors();
+
+		Log.info("Start ", "Version: '%s' | App Type: '%s' | OS: '%s'", VERSION, Gdx.app.getType(),
+				System.getProperty("os.name"));
+		Log.debug("Start ", "GL30 Available: '%b' | Renderer: '%s'", Gdx.graphics.isGL30Available(),
+				Gdx.graphics.getGLVersion().getRendererString());
+
+		// Initialize managed game
+		super.create();
+
+		// Add input listener
+		getInputMultiplexer().addProcessor(applicationInputProcessor);
+
+		// Configure asset manager
+		this.assetManager.setLoader(FreeTypeFontGenerator.class,
+				new FreeTypeFontGeneratorLoader(this.assetManager.getFileHandleResolver()));
+		this.assetManager.setLoader(BitmapFont.class, ".ttf",
+				new FreetypeFontLoader(this.assetManager.getFileHandleResolver()));
+		this.assetManager.setLoader(Text.class, new TextLoader(this.assetManager.getFileHandleResolver()));
+		this.assetManager.setLoader(PlaylistDefinition.class,
+				new PlaylistDefinitionLoader(this.assetManager.getFileHandleResolver()));
+		this.assetManager.registerAssetLoaderParametersFactory(BitmapFont.class,
+				new BitmapFontAssetLoaderParametersFactory());
+
+		// Sound manager
+		this.soundManager = ReflectionUtils.newInstance("de.eskalon.commons.audio.DesktopSoundManager",
+				ISoundManager.class);
+
+		// Create sprite batch & camera
+		this.batch = new SpriteBatch(1000,
+				GL32CMacIssueHandler.doUse32CShader() ? GL32CMacIssueHandler.createSpriteBatchShader() : null);
+
+		this.uiCamera = new OrthographicCamera(viewportWidth, viewportHeight);
+		this.uiCamera.combined.setToOrtho2D(0, 0, getWidth(), getHeight());
+
+		// Configure screen manager
+		this.getScreenManager().setHasDepth(hasDepth);
+
+		// Debug info renderer
+		debugInfoRenderer = new DebugInfoRenderer(batch, debugLogging, VERSION, soundManager);
+
+		// Splash Screen
+		this.screenManager.addScreen("blank", new BlankEskalonScreen(this));
+		this.screenManager.addScreen("splash", new EskalonSplashScreen(this, (param) -> {
+			// Enable stuff depending on commons assets
+			applicationInputProcessor.enable();
+			debugInfoRenderer.initialize(getWidth(), getHeight(),
+					assetManager.get(EskalonCommonsAssets.DEFAULT_FONT_NAME));
+
+			// Push second screen (usually asset loading)
+			screenManager.pushScreen("blank", "splashOutTransition1");
+			screenManager.pushScreen("blank", "splashOutTransition2");
+			screenManager.pushScreen(initApp(), "splashOutTransition3");
+		}));
+
+		BlendingTransition splashBlendingTransition = new BlendingTransition(batch, 0.25F, Interpolation.exp10In);
+		screenManager.addScreenTransition("splashInTransition", splashBlendingTransition);
+		BlendingTransition splashOutTransition1 = new BlendingTransition(batch, 0.18F, Interpolation.fade);
+		screenManager.addScreenTransition("splashOutTransition1", splashOutTransition1);
+		BlankTimedTransition splashOutTransition2 = new BlankTimedTransition(0.22F);
+		screenManager.addScreenTransition("splashOutTransition2", splashOutTransition2);
+		BlendingTransition splashOutTransition3 = new BlendingTransition(batch, 0.35F, Interpolation.pow2In);
+		screenManager.addScreenTransition("splashOutTransition3", splashOutTransition3);
+
+		// Push the splash screen
+		screenManager.pushScreen("splash", "splashInTransition");
+	}
+
+	/**
+	 * Takes care of initializing the application.
+	 * 
+	 * @return the name of the screen that should be pushed after the splash screen
+	 */
+	protected abstract String initApp();
+
+	@Override
+	public void render() {
+		/*
+		 * Takes care of posting the events in the rendering thread
+		 */
+		eventBus.distributeEvents();
+
+		/*
+		 * Render the screen
+		 */
+		super.render();
+
+		/*
+		 * Update fps counter
+		 */
+		debugInfoRenderer.update(Gdx.graphics.getDeltaTime());
+
+		/*
+		 * Debug overlay
+		 */
+		if (applicationInputProcessor.isDebugOverlayEnabled()) {
+			debugInfoRenderer.render();
+		}
+		/*
+		 * Take a screenshot
+		 */
+		if (applicationInputProcessor.takeScreenshot()) {
+			ScreenshotUtils.takeAndSaveScreenshot();
+			soundManager.playSoundEffect(EskalonCommonsAssets.SHUTTER_SOUND_NAME);
+			applicationInputProcessor.setTakeScreenshot(false);
+		}
+	}
+
+	@Override
+	public void resize(int width, int height) {
+		super.resize(width, height);
+
+		debugInfoRenderer.resize(width, height);
+		uiCamera.combined.setToOrtho2D(0, 0, getWidth(), getHeight());
+	}
+
+	/**
+	 * @return the asset manager used by the game
+	 */
+	public AnnotationAssetManager getAssetManager() {
+		return this.assetManager;
+	}
+
+	/**
+	 * @return the sound manager used to play the game's audio
+	 */
+	public ISoundManager getSoundManager() {
+		return soundManager;
+	}
+
+	/**
+	 * @return the sprite batch to render 2D stuff with
+	 */
+	public SpriteBatch getSpriteBatch() {
+		return batch;
+	}
+
+	/**
+	 * @return the camera used by the UI screens
+	 */
+	public OrthographicCamera getUICamera() {
+		return this.uiCamera;
+	}
+
+	/**
+	 * @return the events bus; events are queued first and then processed in the
+	 *         rendering thread; see {@link EventQueueBus}
+	 */
+	public EventQueueBus getEventBus() {
+		return eventBus;
+	}
+
+	public void setUISkin(Skin skin) {
+		this.uiSkin = skin;
+	}
+
+	/**
+	 * @return the application's UI skin; has to be set via {@link #setUISkin(Skin)}
+	 *         beforehand
+	 */
+	public Skin getUISkin() {
+		return uiSkin;
+	}
+
+	@Override
+	public void dispose() {
+		super.dispose();
+		assetManager.dispose();
+		batch.dispose();
+
+		// if (uiSkin != null)
+		// uiSkin.dispose();
+	}
+
+}
diff --git a/core/src/main/java/de/eskalon/commons/misc/EventQueueBus.java b/core/src/main/java/de/eskalon/commons/misc/EventQueueBus.java
new file mode 100644
index 0000000..3617076
--- /dev/null
+++ b/core/src/main/java/de/eskalon/commons/misc/EventQueueBus.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020 eskalon
+ * 
+ * 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 de.eskalon.commons.misc;
+
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import com.google.common.eventbus.EventBus;
+
+/**
+ * This event bus queues events and only posts them to the subscribers when
+ * {@link #distributeEvents()} is called. This can be useful if events have to
+ * get handled in a certain thread.
+ * 
+ * @author damios
+ */
+public class EventQueueBus extends EventBus {
+
+	/**
+	 * Queue of posted events. Is taken care of when {@link #distributeEvents()} is
+	 * called.
+	 */
+	private Queue<Object> eventQueue = new ConcurrentLinkedQueue<>();
+
+	/**
+	 * After this method is called the {@linkplain #eventQueue queued events} get
+	 * posted to their respective subscribers.
+	 */
+	public void distributeEvents() {
+		Object event = eventQueue.poll();
+		while (event != null) {
+			super.post(event);
+			event = eventQueue.poll();
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * <p>
+	 * The events get queued until {@link #distributeEvents()} is called.
+	 */
+	@Override
+	public void post(Object event) {
+		this.eventQueue.add(event);
+	}
+
+}
diff --git a/core/src/main/java/de/eskalon/commons/screens/AbstractAssetLoadingScreen.java b/core/src/main/java/de/eskalon/commons/screens/AbstractAssetLoadingScreen.java
new file mode 100644
index 0000000..3de3f81
--- /dev/null
+++ b/core/src/main/java/de/eskalon/commons/screens/AbstractAssetLoadingScreen.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2020 eskalon
+ * 
+ * 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 de.eskalon.commons.screens;
+
+import java.lang.reflect.Field;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import org.reflections.Reflections;
+import org.reflections.scanners.FieldAnnotationsScanner;
+
+import com.badlogic.gdx.math.Interpolation;
+import com.badlogic.gdx.utils.reflect.ReflectionUtils;
+
+import de.damios.guacamole.annotations.GwtIncompatible;
+import de.eskalon.commons.asset.AnnotationAssetManager;
+import de.eskalon.commons.asset.AnnotationAssetManager.Asset;
+import de.eskalon.commons.core.EskalonApplication;
+
+/**
+ * This screen takes care of loading all assets for an
+ * {@link EskalonApplication}. Assets have to be in a package below the
+ * specified root package and be annotated with {@link Asset}.
+ * <p>
+ * Afterwards, the loaded assets can be injected in the respective fields via
+ * {@link AnnotationAssetManager#injectAssets(Object)}.
+ * 
+ * @author damios
+ */
+@GwtIncompatible
+public abstract class AbstractAssetLoadingScreen extends AbstractEskalonScreen {
+
+	protected EskalonApplication application;
+	@Nullable
+	private String packageRoot;
+
+	private float progress;
+	private boolean isDone = false;
+
+	public AbstractAssetLoadingScreen(EskalonApplication application,
+			@Nullable String packageRoot) {
+		this.application = application;
+		this.packageRoot = packageRoot;
+	}
+
+	@Override
+	protected void create() {
+		loadOwnAssets();
+
+		if (packageRoot != null) {
+			Reflections reflections = new Reflections(packageRoot,
+					new FieldAnnotationsScanner());
+			Set<Field> assetFields = reflections
+					.getFieldsAnnotatedWith(Asset.class);
+
+			for (Field f : assetFields) {
+				application.getAssetManager()
+						.loadAnnotatedAsset(ReflectionUtils.convertFieldObject(f));
+			}
+		}
+	}
+
+	@Override
+	public void show() {
+		super.show();
+		this.isDone = false;
+	}
+
+	/**
+	 * Loads the assets used in the loading screen itself.
+	 */
+	protected abstract void loadOwnAssets();
+
+	/**
+	 * This method is responsible for finishing up the loaded assets (e.g.
+	 * create the skin) and pushing the next screen.
+	 */
+	protected abstract void onFinishedLoading();
+
+	@Override
+	public void render(float delta) {
+		progress = Interpolation.linear
+				.apply(application.getAssetManager().getProgress());
+		// make sure that the final few progress values are 1, which is more
+		// pleasant to look at
+		progress = progress > 0.98F ? 1 : progress + 0.02F;
+
+		// Check if the asset manager is done
+		if (!isDone && application.getAssetManager().update()) {
+			isDone = true;
+			onFinishedLoading();
+		}
+
+		render(delta, progress);
+	}
+
+	@Override
+	protected EskalonApplication getApplication() {
+		return application;
+	}
+
+	public abstract void render(float delta, float progress);
+
+}
diff --git a/desktop/src/main/java/de/eskalon/commons/audio/DesktopSoundInstance.java b/desktop/src/main/java/de/eskalon/commons/audio/DesktopSoundInstance.java
new file mode 100644
index 0000000..252b195
--- /dev/null
+++ b/desktop/src/main/java/de/eskalon/commons/audio/DesktopSoundInstance.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2020 eskalon
+ * 
+ * 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 de.eskalon.commons.audio;
+
+import org.lwjgl.openal.AL10;
+
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.audio.Sound;
+import com.badlogic.gdx.backends.lwjgl3.audio.OpenALLwjgl3Audio;
+import com.badlogic.gdx.math.Vector3;
+
+/**
+ * @author damios
+ */
+public class DesktopSoundInstance extends DefaultSoundInstance {
+
+	private int sourceId = -1;
+
+	public DesktopSoundInstance(Sound sound, long soundId) {
+		super(sound, soundId);
+		sourceId = ((OpenALLwjgl3Audio) Gdx.audio).getSourceId(soundId);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * <p>
+	 * Only supports mono sounds!
+	 */
+	@Override
+	public void setSoundOrientation(float x, float y, float z) {
+		AL10.alSource3f(sourceId, AL10.AL_ORIENTATION, x, y, z);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * <p>
+	 * Only supports mono sounds!
+	 */
+	@Override
+	public void setSoundOrientation(Vector3 dir) {
+		super.setSoundOrientation(dir);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * <p>
+	 * Only supports mono sounds!
+	 */
+	@Override
+	public void setSoundVelocity(float x, float y, float z) {
+		AL10.alSource3f(sourceId, AL10.AL_VELOCITY, x, y, z);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * <p>
+	 * Only supports mono sounds!
+	 */
+	@Override
+	public void setSoundVelocity(Vector3 dir) {
+		super.setSoundVelocity(dir);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * <p>
+	 * Only supports mono sounds!
+	 */
+	@Override
+	public void setSoundPosition(float x, float y, float z) {
+		AL10.alSource3f(sourceId, AL10.AL_POSITION, x, y, z);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * <p>
+	 * Only supports mono sounds!
+	 */
+	@Override
+	public void setSoundPosition(Vector3 dir) {
+		super.setSoundPosition(dir);
+	}
+
+}
diff --git a/desktop/src/main/java/de/eskalon/commons/audio/DesktopSoundManager.java b/desktop/src/main/java/de/eskalon/commons/audio/DesktopSoundManager.java
new file mode 100644
index 0000000..4a6e5ef
--- /dev/null
+++ b/desktop/src/main/java/de/eskalon/commons/audio/DesktopSoundManager.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2020 eskalon
+ * 
+ * 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 de.eskalon.commons.audio;
+
+import java.nio.FloatBuffer;
+import java.util.NoSuchElementException;
+
+import org.lwjgl.BufferUtils;
+import org.lwjgl.openal.AL10;
+
+import com.badlogic.gdx.audio.Sound;
+import com.badlogic.gdx.utils.GdxRuntimeException;
+
+/**
+ * @author damios
+ */
+public class DesktopSoundManager extends DefaultSoundManager {
+
+	@Override
+	public ISoundInstance playSoundEffect(String name, boolean stopIfPlaying,
+			float pitch) {
+		Sound effect = soundEffects.get(name);
+
+		if (effect == null)
+			throw new NoSuchElementException(
+					"There is no sound effect with the name '" + name
+							+ "' registered");
+
+		if (stopIfPlaying)
+			effect.stop();
+
+		long id = effect.play(getEffectiveVolume(effectVolume));
+
+		if (id == -1)
+			throw new GdxRuntimeException(
+					"Some error occurred playing the sound");
+
+		effect.setPitch(id, pitch);
+
+		return new DesktopSoundInstance(effect, id);
+	}
+
+	@Override
+	public void setListenerOrientation(float lookX, float lookY, float lookZ,
+			float upX, float upY, float upZ) {
+		AL10.alListenerfv(AL10.AL_ORIENTATION,
+				(FloatBuffer) BufferUtils.createFloatBuffer(6)
+						.put(new float[] { lookX, lookY, lookZ, upX, upY, upZ })
+						.flip());
+	}
+
+	@Override
+	public void setListenerVelocity(float x, float y, float z) {
+		AL10.alListener3f(AL10.AL_VELOCITY, x, y, z);
+	}
+
+	@Override
+	public void setListenerPosition(float x, float y, float z) {
+		AL10.alListener3f(AL10.AL_POSITION, x, y, z);
+	}
+
+}
diff --git a/g3d/src/main/java/de/eskalon/commons/graphics/ForwardRenderer.java b/g3d/src/main/java/de/eskalon/commons/graphics/ForwardRenderer.java
new file mode 100644
index 0000000..1f9f446
--- /dev/null
+++ b/g3d/src/main/java/de/eskalon/commons/graphics/ForwardRenderer.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2020 eskalon
+ * 
+ * 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 de.eskalon.commons.graphics;
+
+import com.badlogic.gdx.graphics.Camera;
+import com.badlogic.gdx.graphics.g3d.ModelInstance;
+import com.badlogic.gdx.utils.Array;
+
+/**
+ * This class should implement forward rendering.
+ * 
+ * @author Sarroxxie
+ */
+public class ForwardRenderer implements IRenderer {
+
+	@Override
+	public void render(Array<ModelInstance> objects) {
+		// TODO Auto-generated method stub
+
+	}
+
+	@Override
+	public void setCamera(Camera camera) {
+		// TODO Auto-generated method stub
+
+	}
+
+}
diff --git a/g3d/src/main/java/de/eskalon/commons/graphics/IRenderer.java b/g3d/src/main/java/de/eskalon/commons/graphics/IRenderer.java
new file mode 100644
index 0000000..6bd6229
--- /dev/null
+++ b/g3d/src/main/java/de/eskalon/commons/graphics/IRenderer.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2020 eskalon
+ * 
+ * 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 de.eskalon.commons.graphics;
+
+import com.badlogic.gdx.graphics.Camera;
+import com.badlogic.gdx.graphics.g3d.ModelInstance;
+import com.badlogic.gdx.utils.Array;
+
+/**
+ * @author Sarroxxie
+ */
+public interface IRenderer {
+
+	public void render(Array<ModelInstance> objects);
+
+	public void setCamera(Camera camera);
+
+}
diff --git a/g3d/src/main/java/de/eskalon/commons/graphics/PBRTextureAttribute.java b/g3d/src/main/java/de/eskalon/commons/graphics/PBRTextureAttribute.java
new file mode 100644
index 0000000..f12e327
--- /dev/null
+++ b/g3d/src/main/java/de/eskalon/commons/graphics/PBRTextureAttribute.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2020 eskalon
+ * 
+ * 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 de.eskalon.commons.graphics;
+
+import com.badlogic.gdx.graphics.Texture;
+import com.badlogic.gdx.graphics.g2d.TextureRegion;
+import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute;
+import com.badlogic.gdx.graphics.g3d.utils.TextureDescriptor;
+
+/**
+ * Adds additional identifier for Metallic, AmbientOcclusion and Roughness
+ * Textures to use in
+ * <a href="https://en.wikipedia.org/wiki/Physically_based_rendering">
+ * Physically Based Rendering</a>.
+ * 
+ * @author Sarroxxie
+ */
+public class PBRTextureAttribute extends TextureAttribute {
+
+	public final static String AlbedoAlias = "albedoTexture";
+	public final static long Albedo = register(AlbedoAlias);
+	public final static String MetallicAlias = "metallicTexture";
+	public final static long Metallic = register(MetallicAlias);
+	public final static String RoughnessAlias = "roughnessTexture";
+	public final static long Roughness = register(RoughnessAlias);
+
+	static {
+		Mask |= Albedo | Metallic | Roughness;
+	}
+
+	public static PBRTextureAttribute createAlbedo(final Texture texture) {
+		return new PBRTextureAttribute(Albedo, texture);
+	}
+
+	public static PBRTextureAttribute createAlbedo(final TextureRegion region) {
+		return new PBRTextureAttribute(Albedo, region);
+	}
+
+	public static PBRTextureAttribute createMetallic(final Texture texture) {
+		return new PBRTextureAttribute(Metallic, texture);
+	}
+
+	public static PBRTextureAttribute createMetallic(
+			final TextureRegion region) {
+		return new PBRTextureAttribute(Metallic, region);
+	}
+
+	public static PBRTextureAttribute createRoughness(final Texture texture) {
+		return new PBRTextureAttribute(Roughness, texture);
+	}
+
+	public static PBRTextureAttribute createRoughness(
+			final TextureRegion region) {
+		return new PBRTextureAttribute(Roughness, region);
+	}
+
+	public PBRTextureAttribute(final long type) {
+		super(type);
+	}
+
+	public <T extends Texture> PBRTextureAttribute(final long type,
+			final TextureDescriptor<T> textureDescription) {
+		super(type, textureDescription);
+	}
+
+	public <T extends Texture> PBRTextureAttribute(final long type,
+			final TextureDescriptor<T> textureDescription, float offsetU,
+			float offsetV, float scaleU, float scaleV, int uvIndex) {
+		super(type, textureDescription, offsetU, offsetV, scaleU, scaleV,
+				uvIndex);
+	}
+
+	public <T extends Texture> PBRTextureAttribute(final long type,
+			final TextureDescriptor<T> textureDescription, float offsetU,
+			float offsetV, float scaleU, float scaleV) {
+		super(type, textureDescription, offsetU, offsetV, scaleU, scaleV);
+	}
+
+	public PBRTextureAttribute(final long type, final Texture texture) {
+		super(type, texture);
+	}
+
+	public PBRTextureAttribute(final long type, final TextureRegion region) {
+		super(type, region);
+	}
+
+	public PBRTextureAttribute(final TextureAttribute copyFrom) {
+		super(copyFrom);
+	}
+
+}
diff --git a/g3d/src/main/java/de/eskalon/commons/graphics/Scene.java b/g3d/src/main/java/de/eskalon/commons/graphics/Scene.java
new file mode 100644
index 0000000..a310509
--- /dev/null
+++ b/g3d/src/main/java/de/eskalon/commons/graphics/Scene.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2020 eskalon
+ * 
+ * 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 de.eskalon.commons.graphics;
+
+import com.badlogic.gdx.graphics.Camera;
+import com.badlogic.gdx.graphics.g3d.ModelInstance;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.Disposable;
+
+import de.eskalon.commons.core.EskalonApplication;
+import de.eskalon.commons.graphics.deferredrendering.DeferredRenderer;
+
+/**
+ * This class contains all information needed for rendering a 3D Scene.
+ *
+ * @author Sarroxxie
+ */
+public class Scene implements Disposable {
+
+	private Array<ModelInstance> instances;
+	private IRenderer renderer;
+	private EskalonApplication game;
+
+	private Camera camera;
+
+	/**
+	 * 
+	 * @param width
+	 *            viewport width
+	 * @param height
+	 *            viewport height
+	 */
+	public Scene(EskalonApplication game, int width, int height) {
+		this.game = game;
+		this.instances = new Array<ModelInstance>();
+		this.renderer = new DeferredRenderer(this.game);
+	}
+
+	public void render() {
+		this.renderer.render(this.instances);
+	}
+
+	public void setCamera(Camera camera) {
+		this.camera = camera;
+		this.renderer.setCamera(this.camera);
+	}
+
+	public void addInstance(ModelInstance instance) {
+		this.instances.add(instance);
+	}
+
+	@Override
+	public void dispose() {
+		if (renderer instanceof Disposable)
+			((Disposable) renderer).dispose();
+	}
+
+}
diff --git a/g3d/src/main/java/de/eskalon/commons/graphics/WorldObject.java b/g3d/src/main/java/de/eskalon/commons/graphics/WorldObject.java
new file mode 100644
index 0000000..21d744a
--- /dev/null
+++ b/g3d/src/main/java/de/eskalon/commons/graphics/WorldObject.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2020 eskalon
+ * 
+ * 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 de.eskalon.commons.graphics;
+
+import com.badlogic.gdx.graphics.g3d.Model;
+import com.badlogic.gdx.graphics.g3d.ModelInstance;
+import com.badlogic.gdx.math.Vector3;
+import com.badlogic.gdx.math.collision.BoundingBox;
+
+import de.damios.guacamole.Preconditions;
+
+/**
+ * Adds a bit of functionality to the
+ * {@linkplain com.badlogic.gdx.graphics.g3d.ModelInstance model instance} and
+ * stores a corresponding
+ * {@linkplain com.badlogic.gdx.math.collision.BoundingBox bounding box}.
+ *
+ * @author Sarroxxie
+ */
+public class WorldObject extends ModelInstance {
+
+	// TODO: Add functionality for animations
+
+	// TODO: missing constructors, super from model instance
+
+	/**
+	 * Stored {@linkplain com.badlogic.gdx.math.collision.BoundingBox Bounding
+	 * Box}. This contains necessary information for collision tests and
+	 * <a href=
+	 * "https://en.wikipedia.org/wiki/Hidden-surface_determination#Viewing-frustum_culling">
+	 * View Frustum Culling</a>.
+	 */
+	private final BoundingBox bounds;
+
+	/**
+	 * Constructs a new WorldObject and calculates its bounding box.
+	 * 
+	 * @param model
+	 *            The {@link com.badlogic.gdx.graphics.g3d.Model Model} to
+	 *            create an instance of.
+	 */
+	public WorldObject(Model model) {
+		super(model);
+		this.bounds = new BoundingBox();
+		this.calculateBoundingBox(this.bounds);
+	}
+
+	/**
+	 * Calculates the center of the {@linkplain #bounds Bounding Box}.
+	 * 
+	 * @return Center of the {@linkplain #bounds Bounding Box} in Object-Space.
+	 */
+	public Vector3 getBoundingCenter() {
+		return this.bounds.getCenter(new Vector3()); // TODO cache?
+	}
+
+	/**
+	 * Calculates the minimal radius from the {@linkplain #bounds Bounding Box}
+	 * so a sphere with this radius around the {@linkplain #getBoundingCenter()
+	 * center} of the {@linkplain #bounds Bounding Box} fully contains the
+	 * geometry.
+	 * 
+	 * @return Radius of the {@linkplain #bounds Bounding Box} in Object-Space.
+	 */
+	public float getBoundingRadius() {
+		return this.bounds.getDimensions(new Vector3()).len() / 2f;
+	}
+
+	/**
+	 * Sets the scaling of the World Object by modifying its
+	 * {@linkplain com.badlogic.gdx.graphics.g3d.ModelInstance#transform world
+	 * transform}.
+	 * 
+	 * @param scaleX
+	 *            The x-coordinate of the new scale vector.
+	 * @param scaleY
+	 *            The y-coordinate of the new scale vector.
+	 * @param scaleZ
+	 *            The z-coordinate of the new scale vector.
+	 */
+	public void setScale(float scaleX, float scaleY, float scaleZ) {
+		Preconditions.checkArgument(scaleX != 0 && scaleY != 0 && scaleZ != 0,
+				"Neither coordinate of the scale vector is allowed to be 0.");
+		Vector3 oldScale = new Vector3();
+		this.transform.getScale(oldScale);
+		this.transform.scale(scaleX / oldScale.x, scaleY / oldScale.y,
+				scaleZ / oldScale.z);
+	}
+
+	/**
+	 * Sets the scaling of the World Object by modifying its
+	 * {@linkplain com.badlogic.gdx.graphics.g3d.ModelInstance#transform world
+	 * transform}.
+	 * 
+	 * @param scale
+	 *            The new scale vector.
+	 */
+	public void setScale(Vector3 scale) {
+		this.setScale(scale.x, scale.y, scale.z);
+	}
+}
diff --git a/g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/AmbientLightPass.java b/g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/AmbientLightPass.java
new file mode 100644
index 0000000..c975443
--- /dev/null
+++ b/g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/AmbientLightPass.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2020 eskalon
+ * 
+ * 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 de.eskalon.commons.graphics.deferredrendering;
+
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.files.FileHandle;
+import com.badlogic.gdx.graphics.Camera;
+import com.badlogic.gdx.graphics.GL20;
+import com.badlogic.gdx.graphics.Mesh;
+import com.badlogic.gdx.graphics.Texture.TextureFilter;
+import com.badlogic.gdx.graphics.glutils.ShaderProgram;
+import com.badlogic.gdx.math.Vector3;
+
+import de.damios.guacamole.gdx.graphics.QuadMeshGenerator;
+import de.damios.guacamole.gdx.graphics.ShaderProgramFactory;
+
+/**
+ * @author Sarroxxie
+ */
+public class AmbientLightPass extends LightPass {
+
+	private ShaderProgram program;
+	private Camera orthoCam;
+	public Vector3 ambientColor = new Vector3(0.6f, 0.6f, 0.6f);
+
+	private Mesh screenQuad;
+
+	// TODO: Write missing shader!
+	private final FileHandle vert = Gdx.files
+			.internal("resources/shaders/image.vert");
+	private final FileHandle frag = Gdx.files
+			.internal("resources/shaders/ambient.frag");
+
+	public AmbientLightPass(DeferredRenderer renderer) {
+		super(renderer);
+		this.orthoCam = this.renderer.game.getUICamera();
+		this.program = ShaderProgramFactory.fromFile(vert, frag);
+		this.screenQuad = QuadMeshGenerator.createFullScreenQuad(
+				renderer.game.getWidth(), renderer.game.getHeight(), true);
+	}
+
+	@Override
+	public void render() {
+		this.renderer.context.begin();
+
+		this.renderer.gBuffer.getTextureAttachments()
+				.get(DeferredRenderer.MATERIAL_ATTACHMENT_INDEX)
+				.setFilter(TextureFilter.Nearest, TextureFilter.Nearest);
+
+		this.renderer.gBuffer.getTextureAttachments()
+				.get(DeferredRenderer.ALBEDO_ATTACHMENT_INDEX)
+				.setFilter(TextureFilter.Nearest, TextureFilter.Nearest);
+
+		this.program.bind();
+		this.program.setUniformMatrix("u_projTrans", this.orthoCam.combined);
+		this.program.setUniformf("u_ambient_color", ambientColor.x,
+				ambientColor.y, ambientColor.z);
+		this.program.setUniformi("u_ambient_occlusion",
+				this.renderer.context.textureBinder
+						.bind(this.renderer.gBuffer.getTextureAttachments().get(
+								DeferredRenderer.MATERIAL_ATTACHMENT_INDEX)));
+		this.program.setUniformi("u_albedo",
+				this.renderer.context.textureBinder
+						.bind(this.renderer.gBuffer.getTextureAttachments().get(
+								DeferredRenderer.ALBEDO_ATTACHMENT_INDEX)));
+		this.screenQuad.render(this.program, GL20.GL_TRIANGLE_STRIP);
+
+		this.renderer.context.end();
+	}
+
+	@Override
+	public void dispose() {
+		this.program.dispose();
+	}
+
+	public void resize() {
+		this.screenQuad = QuadMeshGenerator.createFullScreenQuad(
+				renderer.game.getWidth(), renderer.game.getHeight(), true);
+	}
+
+}
diff --git a/g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/DebugLightPass.java b/g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/DebugLightPass.java
new file mode 100644
index 0000000..555600f
--- /dev/null
+++ b/g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/DebugLightPass.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright 2020 eskalon
+ * 
+ * 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 de.eskalon.commons.graphics.deferredrendering;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.files.FileHandle;
+import com.badlogic.gdx.graphics.Camera;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.GL20;
+import com.badlogic.gdx.graphics.Mesh;
+import com.badlogic.gdx.graphics.Pixmap.Format;
+import com.badlogic.gdx.graphics.Texture.TextureFilter;
+import com.badlogic.gdx.graphics.g2d.BitmapFont;
+import com.badlogic.gdx.graphics.g2d.GlyphLayout;
+import com.badlogic.gdx.graphics.g2d.SpriteBatch;
+import com.badlogic.gdx.graphics.glutils.FrameBuffer;
+import com.badlogic.gdx.graphics.glutils.ShaderProgram;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
+
+import de.damios.guacamole.gdx.graphics.NestableFrameBuffer;
+import de.damios.guacamole.gdx.graphics.QuadMeshGenerator;
+import de.damios.guacamole.gdx.graphics.ShaderProgramFactory;
+import de.eskalon.commons.screens.EskalonSplashScreen.EskalonCommonsAssets;
+import de.eskalon.commons.utils.graphics.GL32CMacIssueHandler;
+
+/**
+ * Renders the content of the g-buffer for debug purposes.
+ * 
+ * Layout: \ albedo \ normal \ depth \ ambient occlusion \ shaded result \
+ * (empty) \ metallic \ roughness \ (empty)
+ * 
+ * @author Sarroxxie
+ */
+public class DebugLightPass extends LightPass {
+
+	private Camera orthoCam;
+	private Map<String, Mesh> quads = new ConcurrentHashMap<String, Mesh>();
+	private BitmapFont defaultFont;
+	private ShapeRenderer shapeRenderer;
+	private SpriteBatch sBatch;
+
+	private FrameBuffer finalRenderBuffer;
+	private LightPass ambientLightPass;
+
+	private int viewportWidth;
+	private int viewportHeight;
+
+	/**
+	 * Paths to the needed shader files.
+	 */
+	private final FileHandle vert = Gdx.files
+			.internal("resources/shaders/image.vert");
+	private final FileHandle imageFrag = Gdx.files
+			.internal("resources/shaders/image.frag");
+	private final FileHandle depthFrag = Gdx.files
+			.internal("resources/shaders/imageDepth.frag");
+	private final FileHandle ambientFrag = Gdx.files
+			.internal("resources/shaders/imageBlue.frag");
+	private final FileHandle roughnessFrag = Gdx.files
+			.internal("resources/shaders/imageGreen.frag");
+	private final FileHandle metallicFrag = Gdx.files
+			.internal("resources/shaders/imageRed.frag");
+
+	/**
+	 * Contains the line positions like such: {left, right, bottom, top}
+	 */
+	private float[] linePositions = new float[4];
+
+	/**
+	 * Contains the text positions like such: {left collumn, mid collumn, right
+	 * collumn, bottom row, mid row, top row}
+	 */
+	private float[] textPositions = new float[6];
+
+	/**
+	 * This is the percentage of the screen height that the text will be offset
+	 * on the negative Y-axis (downwards).
+	 */
+	public float yOffsetPct = 1 / 50f;
+
+	/**
+	 * This is the thickness of the lines between the boxes.
+	 */
+	public float lineWidth = 1f;
+
+	private ShaderProgram program, depth, metallic, roughness, ambient;
+
+	public DebugLightPass(DeferredRenderer renderer) {
+		super(renderer);
+		this.orthoCam = this.renderer.game.getUICamera();
+		this.sBatch = this.renderer.game.getSpriteBatch();
+		this.shapeRenderer = new ShapeRenderer(5000, GL32CMacIssueHandler
+				.createImmediateModeRenderer20DefaultShader(false, true, 0));
+
+		this.ambientLightPass = new AmbientLightPass(this.renderer);
+
+		this.viewportWidth = this.renderer.game.getWidth();
+		this.viewportHeight = this.renderer.game.getHeight();
+
+		this.calculatePositions(viewportWidth, viewportHeight);
+
+		this.finalRenderBuffer = new NestableFrameBuffer(Format.RGBA8888,
+				viewportWidth, viewportHeight, false);
+
+		this.addQuads();
+
+		// all necessary shader programs are compiled
+		this.program = ShaderProgramFactory.fromFile(vert, imageFrag);
+		this.depth = ShaderProgramFactory.fromFile(vert, depthFrag);
+		this.ambient = ShaderProgramFactory.fromFile(vert, ambientFrag);
+		this.roughness = ShaderProgramFactory.fromFile(vert, roughnessFrag);
+		this.metallic = ShaderProgramFactory.fromFile(vert, metallicFrag);
+	}
+
+	/**
+	 * Calculates the positions for the lines and the texts using the given
+	 * viewport width and height.
+	 * 
+	 * @param viewportWidth
+	 *            screen width.
+	 * @param viewportHeight
+	 *            screen height.
+	 */
+	private void calculatePositions(int viewportWidth, int viewportHeight) {
+		// calculates the positions of the lines
+		this.linePositions[0] = viewportWidth / 3f;
+		this.linePositions[1] = linePositions[0] * 2f;
+		this.linePositions[2] = viewportHeight / 3f;
+		this.linePositions[3] = linePositions[2] * 2f;
+
+		float yOffset = viewportHeight * this.yOffsetPct + this.lineWidth / 2;
+
+		// calculates the positions of the debug texts
+		this.textPositions[0] = viewportWidth / 6f;
+		this.textPositions[1] = textPositions[0] + linePositions[0];
+		this.textPositions[2] = textPositions[1] + linePositions[0];
+
+		this.textPositions[3] = linePositions[2] - yOffset;
+		this.textPositions[4] = linePositions[3] - yOffset;
+		this.textPositions[5] = viewportHeight - yOffset;
+	}
+
+	/**
+	 * Registers all the used quads to the {@linkplain #quads map}.
+	 */
+	private void addQuads() {
+		this.quads.put("albedo", QuadMeshGenerator.createQuadFromCoordinates(0,
+				linePositions[3], linePositions[0], viewportHeight, true));
+		this.quads.put("normal",
+				QuadMeshGenerator.createQuadFromCoordinates(linePositions[0],
+						linePositions[3], linePositions[1], viewportHeight,
+						true));
+		this.quads.put("depth",
+				QuadMeshGenerator.createQuadFromCoordinates(linePositions[1],
+						linePositions[3], viewportWidth, viewportHeight, true));
+		this.quads.put("final render",
+				QuadMeshGenerator.createQuadFromCoordinates(linePositions[0],
+						linePositions[2], linePositions[1], linePositions[3],
+						true));
+		this.quads.put("ambient occlusion",
+				QuadMeshGenerator.createQuadFromCoordinates(0, 0,
+						linePositions[0], linePositions[2], true));
+		this.quads.put("roughness", QuadMeshGenerator.createQuadFromCoordinates(
+				linePositions[0], 0, linePositions[1], linePositions[2], true));
+		this.quads.put("metallic", QuadMeshGenerator.createQuadFromCoordinates(
+				linePositions[1], 0, viewportWidth, linePositions[2], true));
+	}
+
+	@Override
+	public void render() {
+		this.renderFinalRender();
+		this.renderer.context.begin();
+
+		Gdx.gl.glClearColor(0f, 0f, 0f, 0f);
+		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);
+
+		// renders the content of the g-buffer onto quads
+		this.renderQuad(program, "albedo", this.renderer.gBuffer,
+				DeferredRenderer.ALBEDO_ATTACHMENT_INDEX);
+		this.renderQuad(program, "normal", this.renderer.gBuffer,
+				DeferredRenderer.NORMAL_ATTACHMENT_INDEX);
+		this.renderQuad(depth, "depth", this.renderer.gBuffer,
+				DeferredRenderer.DEPTH_ATTACHMENT_INDEX);
+		this.renderQuad(program, "final render", finalRenderBuffer, 0);
+		this.renderQuad(ambient, "ambient occlusion", this.renderer.gBuffer,
+				DeferredRenderer.MATERIAL_ATTACHMENT_INDEX);
+		this.renderQuad(roughness, "roughness", this.renderer.gBuffer,
+				DeferredRenderer.MATERIAL_ATTACHMENT_INDEX);
+		this.renderQuad(metallic, "metallic", this.renderer.gBuffer,
+				DeferredRenderer.MATERIAL_ATTACHMENT_INDEX);
+
+		this.renderer.context.end();
+
+		this.renderDebugLines();
+		this.renderDebugText();
+	}
+
+	/**
+	 * Renders a texture onto a given quad at a given attachment index using a
+	 * given {@link ShaderProgram}. Note, that this method uses
+	 * {@linkplain de.eskalon.commons.graphics.deferredrendering.DeferredRenderer#context
+	 * this render context}, so in order to work correctly,
+	 * {@linkplain com.badlogic.gdx.graphics.g3d.utils.RenderContext#begin()
+	 * begin()} should be called before calling this method and
+	 * {@linkplain com.badlogic.gdx.graphics.g3d.utils.RenderContext#end()
+	 * end()} should be called after calling this method.
+	 * 
+	 * @param program
+	 *            The {@link ShaderProgram} that renders the quad.
+	 * @param quadID
+	 *            ID of the quad in the {@linkplain #quads map}.
+	 * @param attachmentIndex
+	 *            The index of the texture attachment in the
+	 *            {@linkplain de.eskalon.commons.graphics.deferredrendering.DeferredRenderer#gBuffer
+	 *            g-Buffer}.
+	 */
+	private void renderQuad(ShaderProgram program, String quadID,
+			FrameBuffer buffer, int attachmentIndex) {
+		this.renderer.gBuffer.getTextureAttachments().get(attachmentIndex)
+				.setFilter(TextureFilter.Linear, TextureFilter.Linear);
+		program.bind();
+		program.setUniformMatrix("u_projTrans", this.orthoCam.combined);
+		program.setUniformi("u_texture", this.renderer.context.textureBinder
+				.bind(buffer.getTextureAttachments().get(attachmentIndex)));
+		this.quads.get(quadID).render(program, GL20.GL_TRIANGLE_STRIP);
+	}
+
+	private void renderFinalRender() {
+		this.finalRenderBuffer.begin();
+		this.ambientLightPass.render();
+		this.finalRenderBuffer.end();
+	}
+
+	/**
+	 * Renders a debug text for each of the boxes.
+	 */
+	protected void renderDebugText() {
+		if (this.defaultFont == null) {
+			this.defaultFont = this.renderer.game.getAssetManager()
+					.get(EskalonCommonsAssets.DEFAULT_FONT_NAME);
+		}
+		this.sBatch.begin();
+
+		this.renderCenteredString("Albedo", this.textPositions[0],
+				this.textPositions[5]);
+		this.renderCenteredString("Normal", this.textPositions[1],
+				this.textPositions[5]);
+		this.renderCenteredString("Depth", this.textPositions[2],
+				this.textPositions[5]);
+		this.renderCenteredString("Final Render (ambient lighting only)",
+				this.textPositions[1], this.textPositions[4]);
+		this.renderCenteredString("Ambient Occlusion", this.textPositions[0],
+				this.textPositions[3]);
+		this.renderCenteredString("Roughness", this.textPositions[1],
+				this.textPositions[3]);
+		this.renderCenteredString("Metallic", this.textPositions[2],
+				this.textPositions[3]);
+
+		this.sBatch.end();
+	}
+
+	/**
+	 * Renders a given text at a given position. The text will be x-centered.
+	 * 
+	 * @param text
+	 * @param x
+	 *            x-coordinate where the center of the text will be.
+	 * @param y
+	 *            y-coordinate where the center of the text will be.
+	 */
+	private void renderCenteredString(String text, float x, float y) {
+		this.defaultFont.draw(this.sBatch, text,
+				x - this.getCenteredFontOffset(this.defaultFont, text), y);
+	}
+
+	/**
+	 * Renders lines with a certain {@linkplain #lineWidth thickness} to
+	 * seperate the boxes.
+	 */
+	protected void renderDebugLines() {
+		this.shapeRenderer.begin(ShapeType.Filled);
+		this.shapeRenderer.setColor(Color.LIGHT_GRAY);
+
+		// MIDDLE LINES
+		// left line
+		this.shapeRenderer.rectLine(this.linePositions[0], 0f,
+				this.linePositions[0], this.renderer.game.getHeight(),
+				this.lineWidth);
+		// right line
+		this.shapeRenderer.rectLine(this.linePositions[1], 0f,
+				this.linePositions[1], this.renderer.game.getHeight(),
+				this.lineWidth);
+		// bottom line
+		this.shapeRenderer.rectLine(0f, this.linePositions[2],
+				this.renderer.game.getWidth(), this.linePositions[2],
+				this.lineWidth);
+		// top line
+		this.shapeRenderer.rectLine(0f, this.linePositions[3],
+				this.renderer.game.getWidth(), this.linePositions[3],
+				this.lineWidth);
+
+		// SCREEN BORDER LINES
+		if (this.lineWidth > 1f) {
+			// left line
+			this.shapeRenderer.rectLine(0f, 0f, 0f, this.viewportHeight,
+					this.lineWidth);
+			// right line
+			this.shapeRenderer.rectLine(this.viewportWidth, 0f,
+					this.viewportWidth, this.viewportHeight, this.lineWidth);
+			// bottom line
+			this.shapeRenderer.rectLine(0f, 0f, this.viewportWidth, 0f,
+					this.lineWidth);
+			// top line
+			this.shapeRenderer.rectLine(0f, this.viewportHeight,
+					this.viewportWidth, this.viewportHeight, this.lineWidth);
+		}
+
+		this.shapeRenderer.end();
+	}
+
+	/**
+	 * Calculates the necessary x-offset so the BitmapFont can be rendered
+	 * centered.
+	 * 
+	 * @param bitmapFont
+	 *            Used Font.
+	 * @param text
+	 * @return x-offset
+	 */
+	private float getCenteredFontOffset(BitmapFont bitmapFont, String text) {
+		GlyphLayout glyphLayout = new GlyphLayout();
+		glyphLayout.setText(bitmapFont, text);
+		return glyphLayout.width / 2;
+	}
+
+	@Override
+	public void dispose() {
+		this.program.dispose();
+		this.depth.dispose();
+		this.metallic.dispose();
+		this.roughness.dispose();
+		this.ambient.dispose();
+	}
+
+}
diff --git a/g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/DefaultGeometryPassShader.java b/g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/DefaultGeometryPassShader.java
new file mode 100644
index 0000000..fdd19de
--- /dev/null
+++ b/g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/DefaultGeometryPassShader.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2020 eskalon
+ * 
+ * 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 de.eskalon.commons.graphics.deferredrendering;
+
+import java.lang.reflect.Field;
+
+import org.lwjgl.opengl.GL32C;
+
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.graphics.Camera;
+import com.badlogic.gdx.graphics.GL20;
+import com.badlogic.gdx.graphics.VertexAttributes;
+import com.badlogic.gdx.graphics.g3d.Material;
+import com.badlogic.gdx.graphics.g3d.Renderable;
+import com.badlogic.gdx.graphics.g3d.Shader;
+import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute;
+import com.badlogic.gdx.graphics.g3d.utils.RenderContext;
+import com.badlogic.gdx.graphics.glutils.ShaderProgram;
+import com.badlogic.gdx.math.Matrix3;
+
+import de.damios.guacamole.gdx.graphics.ShaderProgramFactory;
+import de.eskalon.commons.graphics.PBRTextureAttribute;
+
+/**
+ * @author Sarroxxie
+ */
+public class DefaultGeometryPassShader implements Shader {
+
+	private ShaderProgram program;
+	private RenderContext context;
+
+	static final long Mask = PBRTextureAttribute.Albedo
+			| PBRTextureAttribute.Normal | PBRTextureAttribute.Metallic
+			| PBRTextureAttribute.Roughness | PBRTextureAttribute.Ambient;
+
+	private Matrix3 matrix3 = new Matrix3();
+
+	@Override
+	public void dispose() {
+		this.program.dispose();
+	}
+
+	@Override
+	public void init() {
+		this.program = ShaderProgramFactory.fromFile(
+				Gdx.files.internal("resources/shaders/defaultGeometryPassShader.vert"),
+				Gdx.files.internal("resources/shaders/defaultGeometryPassShader.frag"));
+
+		int handle = 0;
+		// TODO use program.getHandle();
+		try {
+			Field field = ShaderProgram.class.getDeclaredField("program");
+			field.setAccessible(true);
+			handle = (int) field.getInt(program);
+		} catch (NoSuchFieldException | SecurityException
+				| IllegalArgumentException | IllegalAccessException e) {
+			e.printStackTrace();
+		}
+		System.out.println(handle);
+		GL32C.glBindFragDataLocation(handle, 0, "albedoOut");
+		GL32C.glBindFragDataLocation(handle, 1, "normalOut");
+		GL32C.glBindFragDataLocation(handle, 2, "materialOut");
+		Gdx.gl20.glLinkProgram(handle);
+	}
+
+	@Override
+	public int compareTo(Shader other) {
+		// TODO: find a suitable comparison
+		return 0;
+	}
+
+	@Override
+	public boolean canRender(Renderable instance) {
+		if (instance.meshPart.mesh.getVertexAttribute(
+				VertexAttributes.Usage.TextureCoordinates) == null) {
+			return false;
+		}
+		if (instance.meshPart.mesh
+				.getVertexAttribute(VertexAttributes.Usage.Normal) == null) {
+			return false;
+		}
+		return instance.material.has(Mask);
+	}
+
+	@Override
+	public void begin(Camera camera, RenderContext context) {
+		this.context = context;
+		this.program.bind();
+		this.program.setUniformMatrix("u_projViewTrans", camera.combined);
+		context.setDepthTest(GL20.GL_LEQUAL);
+		context.setCullFace(GL20.GL_BACK);
+	}
+
+	@Override
+	public void render(Renderable renderable) {
+		Material material = renderable.material;
+
+		TextureAttribute albedoTexture = (TextureAttribute) material
+				.get(PBRTextureAttribute.Albedo);
+		TextureAttribute normalTexture = (TextureAttribute) material
+				.get(PBRTextureAttribute.Normal);
+		TextureAttribute metallicTexture = (TextureAttribute) material
+				.get(PBRTextureAttribute.Metallic);
+		TextureAttribute ambientTexture = (TextureAttribute) material
+				.get(PBRTextureAttribute.Ambient);
+		TextureAttribute roughnessTexture = (TextureAttribute) material
+				.get(PBRTextureAttribute.Roughness);
+
+		if (albedoTexture != null) {
+			this.program.setUniformi("u_albedoTexture", context.textureBinder
+					.bind(albedoTexture.textureDescription.texture));
+		}
+		if (normalTexture != null) {
+			this.program.setUniformi("u_normalTexture", context.textureBinder
+					.bind(normalTexture.textureDescription.texture));
+		}
+		if (metallicTexture != null) {
+			this.program.setUniformi("u_metallicTexture", context.textureBinder
+					.bind(metallicTexture.textureDescription.texture));
+		}
+		if (roughnessTexture != null) {
+			this.program.setUniformi("u_roughnessTexture", context.textureBinder
+					.bind(roughnessTexture.textureDescription.texture));
+		}
+		if (ambientTexture != null) {
+			this.program.setUniformi("u_ambientTexture", context.textureBinder
+					.bind(ambientTexture.textureDescription.texture));
+		}
+
+		this.program.setUniformMatrix("u_worldTrans",
+				renderable.worldTransform);
+
+		// TODO: keep an eye on this thing here, so no transformations are wrong
+		// in the end
+		this.program.setUniformMatrix("u_normalWorldTrans",
+				matrix3.set(renderable.worldTransform).inv().transpose());
+
+		renderable.meshPart.render(this.program);
+	}
+
+	@Override
+	public void end() {
+	}
+
+}
diff --git a/g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/DeferredRenderer.java b/g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/DeferredRenderer.java
new file mode 100644
index 0000000..0a530db
--- /dev/null
+++ b/g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/DeferredRenderer.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2020 eskalon
+ * 
+ * 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 de.eskalon.commons.graphics.deferredrendering;
+
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.graphics.Camera;
+import com.badlogic.gdx.graphics.GL30;
+import com.badlogic.gdx.graphics.g3d.ModelInstance;
+import com.badlogic.gdx.graphics.g3d.utils.DefaultTextureBinder;
+import com.badlogic.gdx.graphics.g3d.utils.RenderContext;
+import com.badlogic.gdx.graphics.glutils.FrameBuffer;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.Disposable;
+
+import de.damios.guacamole.gdx.graphics.NestableFrameBuffer.NestableFrameBufferBuilder;
+import de.eskalon.commons.core.EskalonApplication;
+import de.eskalon.commons.graphics.IRenderer;
+
+/**
+ * @author Sarroxxie
+ */
+public class DeferredRenderer implements IRenderer, Disposable {
+
+	protected EskalonApplication game;
+	private GeometryPass geometryPass;
+	private LightPass lightPass;
+
+	/**
+	 * Stores the base color in 24 bit.
+	 */
+	public static final int ALBEDO_ATTACHMENT_INDEX = 0;
+
+	/**
+	 * Stores the world normal in 24 bit.
+	 */
+	public static final int NORMAL_ATTACHMENT_INDEX = 1;
+
+	/**
+	 * Stores the metalness, roughness and ambient occlusion in 8bit each.
+	 */
+	public static final int MATERIAL_ATTACHMENT_INDEX = 2;
+
+	/**
+	 * Stores the depth in 32 bit.
+	 */
+	// TODO: depth as 24 bit?
+	public static final int DEPTH_ATTACHMENT_INDEX = 3;
+
+	protected RenderContext context;
+	protected FrameBuffer gBuffer;
+	protected Camera camera;
+
+	public DeferredRenderer(EskalonApplication game) {
+		this.game = game;
+		this.context = new RenderContext(
+				new DefaultTextureBinder(DefaultTextureBinder.ROUNDROBIN));
+
+		NestableFrameBufferBuilder builder = new NestableFrameBufferBuilder(
+				Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
+		builder.addColorTextureAttachment(GL30.GL_RGB8, GL30.GL_RGB,
+				GL30.GL_UNSIGNED_BYTE);
+		builder.addColorTextureAttachment(GL30.GL_RGB8, GL30.GL_RGB,
+				GL30.GL_UNSIGNED_BYTE);
+		builder.addColorTextureAttachment(GL30.GL_RGB8, GL30.GL_RGB,
+				GL30.GL_UNSIGNED_BYTE);
+		builder.addDepthTextureAttachment(GL30.GL_DEPTH_COMPONENT,
+				GL30.GL_UNSIGNED_SHORT);
+		this.gBuffer = builder.build();
+
+		// TODO: this bliting will be required inside the light (shading) pass
+		// Gdx.gl30.glReadBuffer(gBuffer.getFramebufferHandle());
+		// Gdx.gl30.glBlitFramebuffer(0, 0, game.getWidth(), game.getHeight(),
+		// 0,
+		// 0, game.getWidth(), game.getHeight(), GL30.GL_DEPTH_BUFFER_BIT,
+		// GL30.GL_NEAREST);
+
+		this.geometryPass = new GeometryPass(this);
+		this.lightPass = new DebugLightPass(this);
+	}
+
+	@Override
+	public void render(Array<ModelInstance> objects) {
+		this.geometryPass.render(objects); // add camera and context and gBuffer
+		this.lightPass.render();
+	}
+
+	@Override
+	public void setCamera(Camera camera) {
+		this.camera = camera;
+	}
+
+	@Override
+	public void dispose() {
+		this.gBuffer.dispose();
+		this.geometryPass.dispose();
+		this.lightPass.dispose();
+	}
+
+}
diff --git a/g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/GeometryPass.java b/g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/GeometryPass.java
new file mode 100644
index 0000000..6c8c584
--- /dev/null
+++ b/g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/GeometryPass.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2020 eskalon
+ * 
+ * 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 de.eskalon.commons.graphics.deferredrendering;
+
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.graphics.GL20;
+import com.badlogic.gdx.graphics.g3d.ModelCache;
+import com.badlogic.gdx.graphics.g3d.ModelInstance;
+import com.badlogic.gdx.graphics.g3d.Renderable;
+import com.badlogic.gdx.graphics.g3d.Shader;
+import com.badlogic.gdx.graphics.g3d.utils.BaseShaderProvider;
+import com.badlogic.gdx.graphics.g3d.utils.DefaultRenderableSorter;
+import com.badlogic.gdx.graphics.g3d.utils.RenderableSorter;
+import com.badlogic.gdx.graphics.g3d.utils.ShaderProvider;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.Disposable;
+
+/**
+ * @author Sarroxxie
+ */
+public class GeometryPass implements Disposable {
+
+	private DeferredRenderer renderer;
+
+	private ModelCache modelCache;
+	private Array<Renderable> renderables = new Array<>();
+
+	private ShaderProvider shaderProvider;
+	private RenderableSorter renderableSorter;
+
+	public GeometryPass(DeferredRenderer renderer) {
+		this.renderer = renderer;
+		this.modelCache = new ModelCache();
+
+		this.shaderProvider = new BaseShaderProvider() {
+			@Override
+			protected Shader createShader(Renderable renderable) {
+				DefaultGeometryPassShader shader = new DefaultGeometryPassShader();
+				shader.init();
+				return shader;
+			}
+		};
+		this.renderableSorter = new DefaultRenderableSorter() {
+			@Override
+			public int compare(Renderable o1, Renderable o2) {
+				return o1.material.compareTo(o2.material);
+			}
+		};
+	}
+
+	public void render(Array<ModelInstance> objects) {
+		/*
+		 * Combine models; set shaders; sort renderables.
+		 */
+		this.renderables.clear();
+
+		this.modelCache.begin(this.renderer.camera);
+		this.modelCache.add(objects);
+		this.modelCache.end();
+		this.modelCache.getRenderables(this.renderables, null);
+
+		for (Renderable renderable : this.renderables) {
+			renderable.shader = shaderProvider.getShader(renderable);
+		}
+
+		this.renderableSorter.sort(this.renderer.camera, this.renderables);
+
+		/*
+		 * Fills all the specified buffers for the #gBuffer.
+		 */
+		this.renderer.context.begin();
+		this.renderer.gBuffer.begin();
+
+		Gdx.gl.glClearColor(0f, 0f, 0f, 0f);
+		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);
+
+		Shader currentShader = null;
+		for (int i = 0; i < this.renderables.size; i++) {
+			final Renderable renderable = this.renderables.get(i);
+			if (currentShader != renderable.shader) {
+				if (currentShader != null)
+					currentShader.end();
+				currentShader = renderable.shader;
+				currentShader.begin(this.renderer.camera,
+						this.renderer.context);
+			}
+			currentShader.render(renderable);
+		}
+		if (currentShader != null)
+			currentShader.end();
+
+		this.renderer.gBuffer.end();
+		this.renderer.context.end();
+	}
+
+	@Override
+	public void dispose() {
+		modelCache.dispose();
+		shaderProvider.dispose();
+	}
+
+}
diff --git a/g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/LightPass.java b/g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/LightPass.java
new file mode 100644
index 0000000..e654126
--- /dev/null
+++ b/g3d/src/main/java/de/eskalon/commons/graphics/deferredrendering/LightPass.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2020 eskalon
+ * 
+ * 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 de.eskalon.commons.graphics.deferredrendering;
+
+import com.badlogic.gdx.utils.Disposable;
+
+/**
+ * @author Sarroxxie
+ */
+public abstract class LightPass implements Disposable {
+
+	protected DeferredRenderer renderer;
+
+	public LightPass(DeferredRenderer renderer) {
+		this.renderer = renderer;
+	}
+
+	public void render() {
+	};
+
+}