diff --git a/build.gradle b/build.gradle
index 1eff0a5..f442d6d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -18,10 +18,12 @@ dependencies {
     implementation 'ru.windcorp.fork.io.github.java-graphics:glm:1.0.1'
 
 	// log4j
-	compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.13.3'
-	compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.13.3'
+	implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.13.3'
+	implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.13.3'
 
 	testImplementation 'junit:junit:4.12'
+	
+	// See also LWJGL dependencies below
 }
 
 /*
@@ -77,3 +79,22 @@ dependencies {
 }
 
 // LWJGL END
+
+jar {
+    manifest {
+        attributes(
+        	"Main-Class": "ru.windcorp.progressia.client.ProgressiaClientMain",
+        	"Class-Path": configurations.runtimeClasspath.collect { "lib/" + it.getName() }.join(' ')
+        )
+    }
+}
+
+/*
+ * Copies runtime dependencies to a prespecified location so they can be packaged properly.
+ */
+task copyLibs(type: Copy) {
+    into "${libsDir}/lib"
+    from configurations.runtimeClasspath
+}
+
+build.dependsOn(copyLibs)
diff --git a/src/main/java/ru/windcorp/jputil/chars/StringUtil.java b/src/main/java/ru/windcorp/jputil/chars/StringUtil.java
index 9a8933b..8641ccf 100644
--- a/src/main/java/ru/windcorp/jputil/chars/StringUtil.java
+++ b/src/main/java/ru/windcorp/jputil/chars/StringUtil.java
@@ -26,6 +26,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Iterator;
+import java.util.Objects;
 import java.util.function.IntFunction;
 
 public class StringUtil {
@@ -775,4 +776,35 @@ public class StringUtil {
 		else             return (char) ('A' - 0xA + value);
 	}
 	
+	public static String replaceAll(String source, String substring, String replacement) {
+		Objects.requireNonNull(source, "source");
+		Objects.requireNonNull(substring, "substring");
+		
+		if (substring.isEmpty()) {
+			throw new IllegalArgumentException("substring is empty");
+		}
+
+		if (!source.contains(substring)) { // also passes if source is empty
+			return source;
+		}
+		
+		if (substring.equals(replacement)) { // null-safe
+			return source;
+		}
+		
+		StringBuilder sb = new StringBuilder(2 * source.length());
+		
+		for (int i = 0; i < source.length() - substring.length() + 1; ++i) {
+			if (source.startsWith(substring, i)) {
+				if (replacement != null) {
+					sb.append(replacement);
+				}
+			} else {
+				sb.append(source.charAt(i));
+			}
+		}
+		
+		return sb.toString();
+	}
+	
 }
diff --git a/src/main/java/ru/windcorp/progressia/client/ClientState.java b/src/main/java/ru/windcorp/progressia/client/ClientState.java
index 84c26a2..b812907 100644
--- a/src/main/java/ru/windcorp/progressia/client/ClientState.java
+++ b/src/main/java/ru/windcorp/progressia/client/ClientState.java
@@ -3,10 +3,10 @@ package ru.windcorp.progressia.client;
 import ru.windcorp.progressia.client.comms.localhost.LocalServerCommsChannel;
 import ru.windcorp.progressia.client.graphics.GUI;
 import ru.windcorp.progressia.client.graphics.flat.LayerTestUI;
-import ru.windcorp.progressia.client.graphics.gui.LayerTestGUI;
 import ru.windcorp.progressia.client.graphics.world.LayerWorld;
 import ru.windcorp.progressia.common.world.WorldData;
 import ru.windcorp.progressia.server.ServerState;
+import ru.windcorp.progressia.test.LayerTestGUI;
 
 public class ClientState {
 	
diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/LayerTestGUI.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/LayerTestGUI.java
deleted file mode 100755
index 6a74d96..0000000
--- a/src/main/java/ru/windcorp/progressia/client/graphics/gui/LayerTestGUI.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*******************************************************************************
- * Progressia
- * Copyright (C) 2020  Wind Corporation
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- *******************************************************************************/
-package ru.windcorp.progressia.client.graphics.gui;
-
-import com.google.common.eventbus.Subscribe;
-import glm.vec._2.i.Vec2i;
-import org.lwjgl.glfw.GLFW;
-import ru.windcorp.progressia.client.graphics.Colors;
-import ru.windcorp.progressia.client.graphics.flat.RenderTarget;
-import ru.windcorp.progressia.client.graphics.font.Font;
-import ru.windcorp.progressia.client.graphics.gui.event.HoverEvent;
-import ru.windcorp.progressia.client.graphics.gui.layout.LayoutAlign;
-import ru.windcorp.progressia.client.graphics.gui.layout.LayoutVertical;
-import ru.windcorp.progressia.client.graphics.input.KeyEvent;
-import ru.windcorp.progressia.client.localization.Localizer;
-import ru.windcorp.progressia.client.localization.MutableString;
-import ru.windcorp.progressia.client.localization.MutableStringLocalized;
-
-public class LayerTestGUI extends GUILayer {
-	
-	private static class DebugComponent extends Component {
-		private final int color;
-		
-		public DebugComponent(String name, Vec2i size, int color) {
-			super(name);
-			this.color = color;
-			
-			setPreferredSize(size);
-			
-			addListener(new Object() {
-				@Subscribe
-				public void onHoverChanged(HoverEvent e) {
-					requestReassembly();
-				}
-			});
-			
-			addListener(KeyEvent.class, this::onClicked);
-		}
-		
-		private boolean onClicked(KeyEvent event) {
-			if (!event.isMouse()) {
-				return false;
-			} else if (event.isPress() && event.isLeftMouseButton()) {
-				System.out.println("You pressed a Component!");
-			}
-			return true;
-		}
-		
-		@Override
-		protected void assembleSelf(RenderTarget target) {
-			target.fill(getX(), getY(), getWidth(), getHeight(), Colors.BLACK);
-			
-			target.fill(
-					getX() + 2, getY() + 2,
-					getWidth() - 4, getHeight() - 4,
-					isHovered() ? Colors.DEBUG_YELLOW : color
-			);
-		}
-	}
-
-	public LayerTestGUI() {
-		super("LayerTestGui", new LayoutAlign(1, 0.75, 5));
-		
-		Panel panel = new Panel("Alex", new LayoutVertical(5));
-		
-		panel.addChild(new DebugComponent("Bravo", new Vec2i(200, 100), 0x44FF44));
-		
-		Component charlie = new DebugComponent("Charlie", null, 0x222222);
-		charlie.setLayout(new LayoutVertical(5));
-
-		//Debug
-		Localizer.getInstance().setLanguage("ru-RU");
-		MutableString epsilon = new MutableStringLocalized("Epsilon")
-				.addListener(() -> ((Label)charlie.getChild(0)).update()).format(34, "thirty-four");
-		// These two are swapped in code due to a bug in layouts, fixing ATM
-		charlie.addChild(
-				new Label(
-						"Delta",
-						new Font().withColor(0xCCBB44).deriveShadow().deriveBold(),
-						"Пре-альфа!"
-				)
-		);
-		charlie.addChild(
-				new Label(
-						"Epsilon",
-						new Font().withColor(0x4444BB).deriveItalic(),
-						() -> epsilon.get().concat("\u269b")
-				)
-		);
-		panel.addChild(charlie);
-
-
-		charlie.addListener(KeyEvent.class, e -> {
-			if(e.isPress() && e.getKey() == GLFW.GLFW_KEY_L) {
-				Localizer localizer = Localizer.getInstance();
-				if (localizer.getLanguage().equals("ru-RU")) {
-					localizer.setLanguage("en-US");
-				} else {
-					localizer.setLanguage("ru-RU");
-				}
-				return true;
-			} return false;
-		});
-		charlie.setFocusable(true);
-		charlie.takeFocus();
-
-		getRoot().addChild(panel);
-	}
-
-}
diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/model/LambdaModel.java b/src/main/java/ru/windcorp/progressia/client/graphics/model/LambdaModel.java
index c6799c5..00d47b1 100644
--- a/src/main/java/ru/windcorp/progressia/client/graphics/model/LambdaModel.java
+++ b/src/main/java/ru/windcorp/progressia/client/graphics/model/LambdaModel.java
@@ -121,5 +121,14 @@ public class LambdaModel extends DynamicModel {
 		}
 		
 	}
+	
+	public static LambdaModel animate(Renderable model, TransformGetter transform) {
+		return new LambdaModel(
+				new Renderable[]      { model },
+				new Mat4[]            { new Mat4() },
+				new boolean[]         { true },
+				new TransformGetter[] { transform }
+		);
+	}
 
 }
diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/texture/ComplexTexture.java b/src/main/java/ru/windcorp/progressia/client/graphics/texture/ComplexTexture.java
index d8f4c18..8aaf4d4 100644
--- a/src/main/java/ru/windcorp/progressia/client/graphics/texture/ComplexTexture.java
+++ b/src/main/java/ru/windcorp/progressia/client/graphics/texture/ComplexTexture.java
@@ -19,10 +19,10 @@ public class ComplexTexture {
 		this.primitive = primitive;
 		
 		this.assumedWidth = abstractWidth
-				* primitive.getWidth() / (float) primitive.getBufferWidth();
+				/ (float) primitive.getWidth() * primitive.getBufferWidth();
 		
 		this.assumedHeight = abstractHeight
-				* primitive.getHeight() / (float) primitive.getBufferHeight();
+				/ (float) primitive.getHeight() * primitive.getBufferHeight();
 	}
 	
 	public Texture get(int x, int y, int width, int height) {
diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/world/Camera.java b/src/main/java/ru/windcorp/progressia/client/graphics/world/Camera.java
index ab99777..56748f9 100644
--- a/src/main/java/ru/windcorp/progressia/client/graphics/world/Camera.java
+++ b/src/main/java/ru/windcorp/progressia/client/graphics/world/Camera.java
@@ -271,6 +271,10 @@ public class Camera {
 			currentModeIndex++;
 		}
 	}
+	
+	public int getCurrentModeIndex() {
+		return currentModeIndex;
+	}
 
 	public float getLastAnchorYaw() {
 		return lastAnchorYaw;
diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/world/LayerWorld.java b/src/main/java/ru/windcorp/progressia/client/graphics/world/LayerWorld.java
index 114e1ed..fb7557e 100644
--- a/src/main/java/ru/windcorp/progressia/client/graphics/world/LayerWorld.java
+++ b/src/main/java/ru/windcorp/progressia/client/graphics/world/LayerWorld.java
@@ -20,45 +20,27 @@ package ru.windcorp.progressia.client.graphics.world;
 import java.util.ArrayList;
 import java.util.List;
 
-import org.lwjgl.glfw.GLFW;
-
-import glm.Glm;
-import glm.mat._3.Mat3;
-import glm.vec._2.Vec2;
-import glm.vec._3.Vec3;
 import ru.windcorp.progressia.client.Client;
+import ru.windcorp.progressia.client.ClientState;
 import ru.windcorp.progressia.client.comms.controls.InputBasedControls;
 import ru.windcorp.progressia.client.graphics.Layer;
 import ru.windcorp.progressia.client.graphics.backend.FaceCulling;
-import ru.windcorp.progressia.client.graphics.backend.GraphicsBackend;
 import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface;
-import ru.windcorp.progressia.client.graphics.input.CursorMoveEvent;
-import ru.windcorp.progressia.client.graphics.input.InputEvent;
-import ru.windcorp.progressia.client.graphics.input.KeyEvent;
 import ru.windcorp.progressia.client.graphics.input.bus.Input;
-import ru.windcorp.progressia.common.collision.AABB;
+import ru.windcorp.progressia.common.Units;
 import ru.windcorp.progressia.common.collision.Collideable;
-import ru.windcorp.progressia.common.collision.CollisionClock;
-import ru.windcorp.progressia.common.collision.CollisionModel;
-import ru.windcorp.progressia.common.collision.CompoundCollisionModel;
 import ru.windcorp.progressia.common.collision.colliders.Collider;
-import ru.windcorp.progressia.common.util.FloatMathUtils;
-import ru.windcorp.progressia.common.util.Vectors;
 import ru.windcorp.progressia.common.world.entity.EntityData;
-import ru.windcorp.progressia.test.AABBRenderer;
+import ru.windcorp.progressia.test.CollisionModelRenderer;
+import ru.windcorp.progressia.test.TestPlayerControls;
 
 public class LayerWorld extends Layer {
 	
-	private final Mat3 angMat = new Mat3();
-
-	private int movementForward = 0;
-	private int movementRight = 0;
-	private int movementUp = 0;
-	
 	private final WorldRenderHelper helper = new WorldRenderHelper();
 	
 	private final Client client;
 	private final InputBasedControls inputBasedControls;
+	private final TestPlayerControls tmp_testControls = TestPlayerControls.getInstance();
 
 	public LayerWorld(Client client) {
 		super("World");
@@ -79,40 +61,12 @@ public class LayerWorld extends Layer {
 	
 	@Override
 	protected void doRender() {
-		if (client.getLocalPlayer() != null) {
-			tmp_handleControls();
-		}
-		
 		Camera camera = client.getCamera();
 		if (camera.hasAnchor()) {
 			renderWorld();
 		}
 	}
 
-	private void tmp_handleControls() {
-		EntityData player = client.getLocalPlayer();
-		
-		angMat.identity().rotateZ(player.getYaw());
-		
-		Vec3 movement = Vectors.grab3();
-		
-		// Horizontal and vertical max control speed
-		final float movementSpeed = 0.1f * 60.0f;
-		// (0; 1], 1 is instant change, 0 is no control authority
-		final float controlAuthority = 0.1f;
-		
-		movement.set(movementForward, -movementRight, 0);
-		if (movementForward != 0 && movementRight != 0) movement.normalize();
-		angMat.mul_(movement); // bug in jglm, .mul() and mul_() are swapped
-		movement.z = movementUp;
-		movement.mul(movementSpeed);
-		movement.sub(player.getVelocity());
-		movement.mul(controlAuthority);
-		player.getVelocity().add(movement);
-		
-		Vectors.release(movement);
-	}
-
 	private void renderWorld() {
 		client.getCamera().apply(helper);
 		FaceCulling.push(true);
@@ -128,150 +82,75 @@ public class LayerWorld extends Layer {
 	private final Collider.ColliderWorkspace tmp_colliderWorkspace = new Collider.ColliderWorkspace();
 	private final List tmp_collideableList = new ArrayList<>();
 	
-	private static final boolean RENDER_AABBS = true;
+	private static final boolean RENDER_COLLISION_MODELS = false;
 	
 	private void tmp_doEveryFrame() {
+		float tickLength = (float) GraphicsInterface.getFrameLength();
+		
 		try {
-			if (RENDER_AABBS) {
-				for (EntityData data : this.client.getWorld().getData().getEntities()) {
-					CollisionModel model = data.getCollisionModel();
-					if (model instanceof AABB) {
-						AABBRenderer.renderAABB((AABB) model, helper);
-					} else if (model instanceof CompoundCollisionModel) {
-						AABBRenderer.renderAABBsInCompound((CompoundCollisionModel) model, helper);
-					}
-				}
-			}
+			tmp_performCollisions(tickLength);
 			
-			tmp_collideableList.clear();
-			tmp_collideableList.addAll(this.client.getWorld().getData().getEntities());
-			
-			Collider.performCollisions(
-					tmp_collideableList,
-					new CollisionClock() {
-						private float t = 0;
-						@Override
-						public float getTime() {
-							return t;
-						}
-						
-						@Override
-						public void advanceTime(float change) {
-							t += change;
-						}
-					},
-					(float) GraphicsInterface.getFrameLength(),
-					tmp_colliderWorkspace
-			);
-			
-			final float frictionCoeff = 1 - 1e-2f;
+			tmp_testControls.applyPlayerControls();
 			
 			for (EntityData data : this.client.getWorld().getData().getEntities()) {
-				data.getVelocity().mul(frictionCoeff);
+				tmp_applyFriction(data);
+				tmp_applyGravity(data, tickLength);
+				tmp_renderCollisionModel(data);
 			}
-		} catch (Exception e) {
+		} catch (Throwable e) {
+			e.printStackTrace();
+			System.err.println("OLEGSHA is to blame. Tell him he vry stupiDD!!");
 			System.exit(31337);
 		}
 	}
 
+	private void tmp_renderCollisionModel(EntityData entity) {
+		if (RENDER_COLLISION_MODELS) {
+			CollisionModelRenderer.renderCollisionModel(entity.getCollisionModel(), helper);
+		}
+	}
+
+	private void tmp_performCollisions(float tickLength) {
+		tmp_collideableList.clear();
+		tmp_collideableList.addAll(this.client.getWorld().getData().getEntities());
+		
+		Collider.performCollisions(
+				tmp_collideableList,
+				this.client.getWorld().getData(),
+				tickLength,
+				tmp_colliderWorkspace
+		);
+	}
+
+	private void tmp_applyFriction(EntityData entity) {
+		final float frictionCoeff = 1 - 1e-5f;
+		entity.getVelocity().mul(frictionCoeff);
+	}
+	
+	private void tmp_applyGravity(EntityData entity, float tickLength) {
+		if (ClientState.getInstance().getLocalPlayer() == entity && tmp_testControls.isFlying()) {
+			return;
+		}
+		
+		final float gravitationalAcceleration;
+		
+		if (tmp_testControls.useMinecraftGravity()) {
+			gravitationalAcceleration = 32f * Units.METERS_PER_SECOND_SQUARED; // plz dont sue me M$
+		} else {
+			gravitationalAcceleration = 9.81f * Units.METERS_PER_SECOND_SQUARED;
+		}
+		entity.getVelocity().add(0, 0, -gravitationalAcceleration * tickLength);
+	}
+
 	@Override
 	protected void handleInput(Input input) {
 		if (input.isConsumed()) return;
 		
-		InputEvent event = input.getEvent();
-		
-		if (event instanceof KeyEvent) {
-			if (onKeyEvent((KeyEvent) event)) {
-				input.consume();
-			}
-		} else if (event instanceof CursorMoveEvent) {
-			onMouseMoved((CursorMoveEvent) event);
-			input.consume();
-		}
+		tmp_testControls.handleInput(input);
 		
 		if (!input.isConsumed()) {
 			inputBasedControls.handleInput(input);
 		}
 	}
-	
-	private boolean flag = true;
-	
-	private boolean onKeyEvent(KeyEvent event) {
-		if (event.isRepeat()) return false;
-		
-		int multiplier = event.isPress() ? 1 : -1;
-		
-		switch (event.getKey()) {
-		case GLFW.GLFW_KEY_W:
-			movementForward += +1 * multiplier;
-			break;
-		case GLFW.GLFW_KEY_S:
-			movementForward += -1 * multiplier;
-			break;
-		case GLFW.GLFW_KEY_A:
-			movementRight += -1 * multiplier;
-			break;
-		case GLFW.GLFW_KEY_D:
-			movementRight += +1 * multiplier;
-			break;
-		case GLFW.GLFW_KEY_SPACE:
-			movementUp += +1 * multiplier;
-			break;
-		case GLFW.GLFW_KEY_LEFT_SHIFT:
-			movementUp += -1 * multiplier;
-			break;
-			
-		case GLFW.GLFW_KEY_ESCAPE:
-			if (!event.isPress()) return false;
-			
-			if (flag) {
-				GLFW.glfwSetInputMode(GraphicsBackend.getWindowHandle(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL);
-			} else {
-				GLFW.glfwSetInputMode(GraphicsBackend.getWindowHandle(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_DISABLED);
-			}
-			
-			flag = !flag;
-			break;
-			
-		case GLFW.GLFW_KEY_F5:
-			if (!event.isPress()) return false;
-			
-			if (client.getCamera().hasAnchor()) {
-				client.getCamera().selectNextMode();
-			}
-			break;
-			
-		default:
-			return false;
-		}
-		
-		return true;
-	}
-	
-	private void onMouseMoved(CursorMoveEvent event) {
-		if (!flag) return;
-		
-		final float yawScale = -0.002f;
-		final float pitchScale = yawScale;
-
-		EntityData player = client.getLocalPlayer();
-		
-		if (player != null) {
-			normalizeAngles(player.getDirection().add(
-					(float) (event.getChangeX() * yawScale),
-					(float) (event.getChangeY() * pitchScale)
-			));
-		}
-	}
-
-	private void normalizeAngles(Vec2 dir) {
-		// Normalize yaw
-		dir.x = FloatMathUtils.normalizeAngle(dir.x);
-		
-		// Clamp pitch
-		dir.y = Glm.clamp(
-				dir.y, -FloatMathUtils.PI_F/2, +FloatMathUtils.PI_F/2
-		);
-	}
 
 }
diff --git a/src/main/java/ru/windcorp/progressia/client/world/entity/HumanoidModel.java b/src/main/java/ru/windcorp/progressia/client/world/entity/HumanoidModel.java
new file mode 100644
index 0000000..45817db
--- /dev/null
+++ b/src/main/java/ru/windcorp/progressia/client/world/entity/HumanoidModel.java
@@ -0,0 +1,116 @@
+package ru.windcorp.progressia.client.world.entity;
+
+import static java.lang.Math.*;
+import static ru.windcorp.progressia.common.util.FloatMathUtils.*;
+
+import glm.mat._4.Mat4;
+import glm.vec._3.Vec3;
+import ru.windcorp.progressia.client.graphics.model.Renderable;
+import ru.windcorp.progressia.client.graphics.model.ShapeRenderHelper;
+import ru.windcorp.progressia.common.world.entity.EntityData;
+
+public class HumanoidModel extends NPedModel {
+	
+	protected static abstract class Limb extends BodyPart {
+		private final float animationOffset;
+		
+		public Limb(
+				Renderable renderable, Vec3 joint,
+				float animationOffset
+		) {
+			super(renderable, joint);
+			this.animationOffset = animationOffset;
+		}
+		
+		@Override
+		protected void applyTransform(Mat4 mat, NPedModel model) {
+			float phase = model.getWalkingFrequency() * model.getWalkingParameter() + animationOffset;
+			float value = sin(phase);
+			float amplitude = getSwingAmplitude((HumanoidModel) model) * model.getVelocityParameter();
+			mat.rotateY(value * amplitude);
+		}
+
+		protected abstract float getSwingAmplitude(HumanoidModel model);
+		
+	}
+	
+	public static class Leg extends Limb {
+		public Leg(
+				Renderable renderable, Vec3 joint,
+				float animationOffset
+		) {
+			super(renderable, joint, animationOffset);
+		}
+		
+		@Override
+		protected float getSwingAmplitude(HumanoidModel model) {
+			return model.walkingLegSwing;
+		}
+	}
+	
+	public static class Arm extends Limb {
+		public Arm(
+				Renderable renderable, Vec3 joint,
+				float animationOffset
+		) {
+			super(renderable, joint, animationOffset);
+		}
+		
+		@Override
+		protected float getSwingAmplitude(HumanoidModel model) {
+			return model.walkingArmSwing;
+		}
+	}
+	
+	private final Arm leftArm;
+	private final Arm rightArm;
+	private final Leg leftLeg;
+	private final Leg rightLeg;
+	
+	private float walkingLegSwing;
+	private float walkingArmSwing;
+	
+	public HumanoidModel(
+			EntityData entity,
+			
+			Body body, Head head,
+			Arm leftArm, Arm rightArm,
+			Leg leftLeg, Leg rightLeg,
+			
+			float scale
+	) {
+		super(entity, body, head, scale);
+		this.leftArm = leftArm;
+		this.rightArm = rightArm;
+		this.leftLeg = leftLeg;
+		this.rightLeg = rightLeg;
+	}
+	
+	@Override
+	protected void renderBodyParts(ShapeRenderHelper renderer) {
+		super.renderBodyParts(renderer);
+		leftArm.render(renderer, this);
+		rightArm.render(renderer, this);
+		leftLeg.render(renderer, this);
+		rightLeg.render(renderer, this);
+	}
+	
+	public float getWalkingArmSwing() {
+		return walkingArmSwing;
+	}
+	
+	public float getWalkingLegSwing() {
+		return walkingLegSwing;
+	}
+	
+	public HumanoidModel setWalkingLegSwing(float walkingLegSwing) {
+		this.walkingLegSwing = walkingLegSwing;
+		return this;
+	}
+	
+	public HumanoidModel setWalkingArmSwing(float walkingArmSwing) {
+		this.walkingArmSwing = walkingArmSwing;
+		return this;
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/progressia/client/world/entity/NPedModel.java b/src/main/java/ru/windcorp/progressia/client/world/entity/NPedModel.java
new file mode 100644
index 0000000..441ff6e
--- /dev/null
+++ b/src/main/java/ru/windcorp/progressia/client/world/entity/NPedModel.java
@@ -0,0 +1,284 @@
+package ru.windcorp.progressia.client.world.entity;
+
+import static java.lang.Math.atan2;
+import static java.lang.Math.min;
+import static java.lang.Math.pow;
+import static java.lang.Math.toRadians;
+import static ru.windcorp.progressia.common.util.FloatMathUtils.normalizeAngle;
+
+import glm.Glm;
+import glm.mat._4.Mat4;
+import glm.vec._3.Vec3;
+import glm.vec._4.Vec4;
+import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface;
+import ru.windcorp.progressia.client.graphics.model.Renderable;
+import ru.windcorp.progressia.client.graphics.model.ShapeRenderHelper;
+import ru.windcorp.progressia.common.Units;
+import ru.windcorp.progressia.common.util.Matrices;
+import ru.windcorp.progressia.common.util.Vectors;
+import ru.windcorp.progressia.common.world.entity.EntityData;
+
+public abstract class NPedModel extends EntityRenderable {
+
+	protected static abstract class BodyPart {
+			private final Renderable renderable;
+			private final Vec3 translation = new Vec3();
+	
+			public BodyPart(Renderable renderable, Vec3 joint) {
+				this.renderable = renderable;
+				if (joint != null) {
+					this.translation.set(joint);
+				}
+			}
+			
+			
+			protected void render(
+					ShapeRenderHelper renderer, NPedModel model
+			) {
+				renderer.pushTransform().translate(translation);
+				applyTransform(renderer.pushTransform(), model);
+				renderable.render(renderer);
+				renderer.popTransform();
+				renderer.popTransform();
+			}
+	
+			protected abstract void applyTransform(Mat4 mat, NPedModel model);
+			
+			public Vec3 getTranslation() {
+				return translation;
+			}
+		}
+
+	public static class Body extends BodyPart {
+		public Body(Renderable renderable) {
+			super(renderable, null);
+		}
+		
+		@Override
+		protected void applyTransform(Mat4 mat, NPedModel model) {
+			// Do nothing
+		}
+	}
+
+	public static class Head extends BodyPart {
+		private final float maxYaw;
+		private final float maxPitch;
+		
+		private final Vec3 viewPoint;
+		
+		public Head(
+				Renderable renderable, Vec3 joint,
+				double maxYawDegrees, double maxPitchDegrees,
+				Vec3 viewPoint
+		) {
+			super(renderable, joint);
+			this.maxYaw = (float) toRadians(maxYawDegrees);
+			this.maxPitch = (float) toRadians(maxPitchDegrees);
+			this.viewPoint = viewPoint;
+		}
+		
+		@Override
+		protected void applyTransform(Mat4 mat, NPedModel model) {
+			mat.rotateZ(model.getHeadYaw()).rotateY(model.getHeadPitch());
+		}
+		
+		public Vec3 getViewPoint() {
+			return viewPoint;
+		}
+	}
+
+	protected final Body body;
+	protected final Head head;
+	
+	private float walkingParameter = 0;
+	private float velocityParameter = 0;
+	private float velocity = 0;
+	
+	/**
+	 * If {@link #velocity} is greater than this value, {@link #velocityParameter} is 1.0.
+	 */
+	private float maxEffectiveVelocity = 5 * Units.METERS_PER_SECOND;
+	
+	/**
+	 * If {@link #velocity} is less than {@link #maxEffectiveVelocity}, then
+	 * {@code velocityCoeff = exp(velocity / maxEffectiveVelocity, velocityCoeffPower)}.
+	 */
+	private float velocityCoeffPower = 1;
+	
+	private final float scale;
+	
+	private float walkingFrequency;
+	
+	private float bodyYaw = Float.NaN;
+	private float headYaw;
+	private float headPitch;
+
+	public NPedModel(EntityData data, Body body, Head head, float scale) {
+		super(data);
+		this.body = body;
+		this.head = head;
+		this.scale = scale;
+	}
+
+	@Override
+	public void render(ShapeRenderHelper renderer) {
+		renderer.pushTransform().scale(scale).rotateZ(bodyYaw);
+		renderBodyParts(renderer);
+		renderer.popTransform();
+		
+		accountForVelocity();
+		evaluateAngles();
+	}
+
+	protected void renderBodyParts(ShapeRenderHelper renderer) {
+		body.render(renderer, this);
+		head.render(renderer, this);
+	}
+
+	private void evaluateAngles() {
+		float globalYaw = normalizeAngle(getData().getYaw());
+		
+		if (Float.isNaN(bodyYaw)) {
+			bodyYaw = globalYaw;
+			headYaw = 0;
+		} else {
+			headYaw = normalizeAngle(globalYaw - bodyYaw);
+			
+			if (headYaw > +head.maxYaw) {
+				bodyYaw += headYaw - +head.maxYaw;
+				headYaw = +head.maxYaw;
+			} else if (headYaw < -head.maxYaw) {
+				bodyYaw += headYaw - -head.maxYaw;
+				headYaw = -head.maxYaw;
+			}
+		}
+		
+		bodyYaw = normalizeAngle(bodyYaw);
+		
+		headPitch = Glm.clamp(
+				getData().getPitch(),
+				-head.maxPitch, head.maxPitch
+		);
+	}
+
+	private void accountForVelocity() {
+		Vec3 horizontal = Vectors.grab3();
+		horizontal.set(getData().getVelocity());
+		horizontal.z = 0;
+		
+		velocity = horizontal.length();
+		
+		evaluateVelocityCoeff();
+		
+		// TODO switch to world time
+		walkingParameter += velocity * GraphicsInterface.getFrameLength() * 1000;
+		
+		bodyYaw += velocityParameter * normalizeAngle(
+				(float) (atan2(horizontal.y, horizontal.x) - bodyYaw)
+		) * min(1, GraphicsInterface.getFrameLength() * 10);
+		Vectors.release(horizontal);
+	}
+
+	private void evaluateVelocityCoeff() {
+		if (velocity > maxEffectiveVelocity) {
+			velocityParameter = 1;
+		} else {
+			velocityParameter = (float) pow(velocity / maxEffectiveVelocity, velocityCoeffPower);
+		}
+	}
+
+	@Override
+	public void getViewPoint(Vec3 output) {
+		Mat4 m = Matrices.grab4();
+		Vec4 v = Vectors.grab4();
+		
+		m.identity()
+		.scale(scale)
+		.rotateZ(bodyYaw)
+		.translate(head.getTranslation())
+		.rotateZ(headYaw)
+		.rotateY(headPitch);
+		
+		v.set(head.getViewPoint(), 1);
+		m.mul(v);
+		
+		output.set(v.x, v.y, v.z);
+		
+		Vectors.release(v);
+		Matrices.release(m);
+	}
+	
+	public Body getBody() {
+		return body;
+	}
+	
+	public Head getHead() {
+		return head;
+	}
+	
+	public float getBodyYaw() {
+		return bodyYaw;
+	}
+	
+	public float getHeadYaw() {
+		return headYaw;
+	}
+	
+	public float getHeadPitch() {
+		return headPitch;
+	}
+	
+	/**
+	 * Returns a number in the range [0; 1] that can be used to scale animation effects that depend on speed.
+	 * This parameter is 0 when the entity is not moving and 1 when it's moving "fast".
+	 * @return velocity parameter
+	 */
+	protected float getVelocityParameter() {
+		return velocityParameter;
+	}
+	
+	/**
+	 * Returns a number that can be used to parameterize animation effects that depend on walking.
+	 * This parameter increases when the entity moves (e.g. this can be total traveled distance).
+	 * @return walking parameter
+	 */
+	protected float getWalkingParameter() {
+		return walkingParameter;
+	}
+	
+	protected float getVelocity() {
+		return velocity;
+	}
+	
+	public float getScale() {
+		return scale;
+	}
+	
+	protected float getWalkingFrequency() {
+		return walkingFrequency;
+	}
+	
+	public NPedModel setWalkingFrequency(float walkingFrequency) {
+		this.walkingFrequency = walkingFrequency;
+		return this;
+	}
+	
+	public float getMaxEffectiveVelocity() {
+		return maxEffectiveVelocity;
+	}
+	
+	public float getVelocityCoeffPower() {
+		return velocityCoeffPower;
+	}
+	
+	public NPedModel setMaxEffectiveVelocity(float maxEffectiveVelocity) {
+		this.maxEffectiveVelocity = maxEffectiveVelocity;
+		return this;
+	}
+	
+	public NPedModel setVelocityCoeffPower(float velocityCoeffPower) {
+		this.velocityCoeffPower = velocityCoeffPower;
+		return this;
+	}
+
+}
\ No newline at end of file
diff --git a/src/main/java/ru/windcorp/progressia/client/world/entity/QuadripedModel.java b/src/main/java/ru/windcorp/progressia/client/world/entity/QuadripedModel.java
index c19a3b0..00311d4 100644
--- a/src/main/java/ru/windcorp/progressia/client/world/entity/QuadripedModel.java
+++ b/src/main/java/ru/windcorp/progressia/client/world/entity/QuadripedModel.java
@@ -2,86 +2,15 @@ package ru.windcorp.progressia.client.world.entity;
 
 import static java.lang.Math.*;
 import static ru.windcorp.progressia.common.util.FloatMathUtils.*;
+import static ru.windcorp.progressia.common.util.FloatMathUtils.sin;
 
-import glm.Glm;
 import glm.mat._4.Mat4;
 import glm.vec._3.Vec3;
-import glm.vec._4.Vec4;
-import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface;
 import ru.windcorp.progressia.client.graphics.model.Renderable;
 import ru.windcorp.progressia.client.graphics.model.ShapeRenderHelper;
-import ru.windcorp.progressia.common.util.Matrices;
-import ru.windcorp.progressia.common.util.Vectors;
 import ru.windcorp.progressia.common.world.entity.EntityData;
 
-public class QuadripedModel extends EntityRenderable {
-	
-	private static abstract class BodyPart {
-		private final Renderable renderable;
-		private final Vec3 translation = new Vec3();
-
-		public BodyPart(Renderable renderable, Vec3 joint) {
-			this.renderable = renderable;
-			if (joint != null) {
-				this.translation.set(joint);
-			}
-		}
-		
-		
-		protected void render(
-				ShapeRenderHelper renderer, QuadripedModel model
-		) {
-			renderer.pushTransform().translate(translation);
-			applyTransform(renderer.pushTransform(), model);
-			renderable.render(renderer);
-			renderer.popTransform();
-			renderer.popTransform();
-		}
-
-		protected abstract void applyTransform(Mat4 mat, QuadripedModel model);
-		
-		public Vec3 getTranslation() {
-			return translation;
-		}
-	}
-	
-	public static class Body extends BodyPart {
-		public Body(Renderable renderable) {
-			super(renderable, null);
-		}
-		
-		@Override
-		protected void applyTransform(Mat4 mat, QuadripedModel model) {
-			// Do nothing
-		}
-	}
-	
-	public static class Head extends BodyPart {
-		private final float maxYaw;
-		private final float maxPitch;
-		
-		private final Vec3 viewPoint;
-		
-		public Head(
-				Renderable renderable, Vec3 joint,
-				double maxYawDegrees, double maxPitchDegrees,
-				Vec3 viewPoint
-		) {
-			super(renderable, joint);
-			this.maxYaw = (float) toRadians(maxYawDegrees);
-			this.maxPitch = (float) toRadians(maxPitchDegrees);
-			this.viewPoint = viewPoint;
-		}
-		
-		@Override
-		protected void applyTransform(Mat4 mat, QuadripedModel model) {
-			mat.rotateZ(model.headYaw).rotateY(model.headPitch);
-		}
-		
-		public Vec3 getViewPoint() {
-			return viewPoint;
-		}
-	}
+public class QuadripedModel extends NPedModel {
 	
 	public static class Leg extends BodyPart {
 		private final float animationOffset;
@@ -95,33 +24,19 @@ public class QuadripedModel extends EntityRenderable {
 		}
 		
 		@Override
-		protected void applyTransform(Mat4 mat, QuadripedModel model) {
-			mat.rotateY(sin(model.walkingFrequency * model.walkingAnimationParameter + animationOffset) * model.walkingSwing * model.velocityCoeff);
+		protected void applyTransform(Mat4 mat, NPedModel model) {
+			float phase = model.getWalkingFrequency() * model.getWalkingParameter() + animationOffset;
+			float value = sin(phase);
+			float amplitude = ((QuadripedModel) model).getWalkingSwing() * model.getVelocityParameter();
+			mat.rotateY(value * amplitude);
 		}
+		
 	}
 	
-	private final Body body;
-	private final Head head;
 	private final Leg leftForeLeg, rightForeLeg;
 	private final Leg leftHindLeg, rightHindLeg;
 	
-	private final float scale;
-	
-	private float walkingAnimationParameter = 0;
-	private float velocityCoeff = 0;
-	private float velocity = 0;
-	
-	/**
-	 * Controls how quickly velocityCoeff approaches 1
-	 */
-	private float velocityCutoff = 10;
-	
-	private float walkingFrequency = 0.15f / 60.0f;
 	private float walkingSwing = (float) toRadians(30);
-	
-	private float bodyYaw = Float.NaN;
-	private float headYaw;
-	private float headPitch;
 
 	public QuadripedModel(
 			EntityData entity,
@@ -132,105 +47,30 @@ public class QuadripedModel extends EntityRenderable {
 			
 			float scale
 	) {
-		super(entity);
+		super(entity, body, head, scale);
 		
-		this.body = body;
-		this.head = head;
 		this.leftForeLeg = leftForeLeg;
 		this.rightForeLeg = rightForeLeg;
 		this.leftHindLeg = leftHindLeg;
 		this.rightHindLeg = rightHindLeg;
-		
-		this.scale = scale;
 	}
 	
 	@Override
-	public void render(ShapeRenderHelper renderer) {
-		renderer.pushTransform().scale(scale).rotateZ(bodyYaw);
-		body.render(renderer, this);
-		head.render(renderer, this);
-		leftForeLeg.render(renderer, this);
-		rightForeLeg.render(renderer, this);
-		leftHindLeg.render(renderer, this);
-		rightHindLeg.render(renderer, this);
-		renderer.popTransform();
-		
-		accountForVelocity();
-		evaluateAngles();
-	}
-
-	private void evaluateAngles() {
-		float globalYaw = normalizeAngle(getData().getYaw());
-		
-		if (Float.isNaN(bodyYaw)) {
-			bodyYaw = globalYaw;
-			headYaw = 0;
-		} else {
-			headYaw = normalizeAngle(globalYaw - bodyYaw);
-			
-			if (headYaw > +head.maxYaw) {
-				bodyYaw += headYaw - +head.maxYaw;
-				headYaw = +head.maxYaw;
-			} else if (headYaw < -head.maxYaw) {
-				bodyYaw += headYaw - -head.maxYaw;
-				headYaw = -head.maxYaw;
-			}
-		}
-		
-		bodyYaw = normalizeAngle(bodyYaw);
-		
-		headPitch = Glm.clamp(
-				getData().getPitch(),
-				-head.maxPitch, head.maxPitch
-		);
-	}
-
-	private void accountForVelocity() {
-		Vec3 horizontal = Vectors.grab3();
-		horizontal.set(getData().getVelocity());
-		horizontal.z = 0;
-		
-		velocity = horizontal.length();
-		
-		evaluateVelocityCoeff();
-		
-		// TODO switch to world time
-		walkingAnimationParameter += velocity * GraphicsInterface.getFrameLength() * 1000;
-		
-		bodyYaw += velocityCoeff * normalizeAngle(
-				(float) (atan2(horizontal.y, horizontal.x) - bodyYaw)
-		) * min(1, GraphicsInterface.getFrameLength() * 10);
-		Vectors.release(horizontal);
+	protected void renderBodyParts(ShapeRenderHelper renderer) {
+		super.renderBodyParts(renderer);
+		this.leftForeLeg.render(renderer, this);
+		this.rightForeLeg.render(renderer, this);
+		this.leftHindLeg.render(renderer, this);
+		this.rightHindLeg.render(renderer, this);
 	}
 	
-	private void evaluateVelocityCoeff() {
-		if (velocity * velocityCutoff > 1) {
-			velocityCoeff = 1;
-		} else {
-			velocityCoeff = velocity * velocityCutoff;
-			velocityCoeff *= velocityCoeff;
-		}
+	public float getWalkingSwing() {
+		return walkingSwing;
 	}
 	
-	@Override
-	public void getViewPoint(Vec3 output) {
-		Mat4 m = Matrices.grab4();
-		Vec4 v = Vectors.grab4();
-		
-		m.identity()
-		.scale(scale)
-		.rotateZ(bodyYaw)
-		.translate(head.getTranslation())
-		.rotateZ(headYaw)
-		.rotateY(headPitch);
-		
-		v.set(head.getViewPoint(), 1);
-		m.mul(v);
-		
-		output.set(v.x, v.y, v.z);
-		
-		Vectors.release(v);
-		Matrices.release(m);
+	public QuadripedModel setWalkingSwing(float walkingSwing) {
+		this.walkingSwing = walkingSwing;
+		return this;
 	}
 
 }
diff --git a/src/main/java/ru/windcorp/progressia/common/Units.java b/src/main/java/ru/windcorp/progressia/common/Units.java
new file mode 100644
index 0000000..0c8348a
--- /dev/null
+++ b/src/main/java/ru/windcorp/progressia/common/Units.java
@@ -0,0 +1,56 @@
+package ru.windcorp.progressia.common;
+
+public class Units {
+	
+	// Base units
+	// We're SI.
+	public static final float METERS                     = 1;
+	public static final float KILOGRAMS                  = 1;
+	public static final float SECONDS                    = 1;
+
+	// Length                                            
+	public static final float CENTIMETERS                = METERS / 100; 
+	public static final float MILLIMETERS                = METERS / 1000;
+	public static final float KILOMETERS                 = METERS * 1000;
+	
+	// Surface
+	public static final float SQUARE_CENTIMETERS         = CENTIMETERS * CENTIMETERS;
+	public static final float SQUARE_METERS              = METERS * METERS;
+	public static final float SQUARE_MILLIMETERS         = MILLIMETERS * MILLIMETERS;
+	public static final float SQUARE_KILOMETERS          = KILOMETERS * KILOMETERS;
+	
+	// Volume
+	public static final float CUBIC_CENTIMETERS          = CENTIMETERS * CENTIMETERS * CENTIMETERS;
+	public static final float CUBIC_METERS               = METERS * METERS * METERS;
+	public static final float CUBIC_MILLIMETERS          = MILLIMETERS * MILLIMETERS * MILLIMETERS;
+	public static final float CUBIC_KILOMETERS           = KILOMETERS * KILOMETERS * KILOMETERS;
+
+	// Mass                                              
+	public static final float GRAMS                      = KILOGRAMS / 1000;
+	public static final float TONNES                     = KILOGRAMS * 1000;
+	
+	// Density
+	public static final float KILOGRAMS_PER_CUBIC_METER  = KILOGRAMS / CUBIC_METERS;
+	public static final float GRAMS_PER_CUBIC_CENTIMETER = GRAMS / CUBIC_CENTIMETERS;
+
+	// Time                                              
+	public static final float MILLISECONDS               = SECONDS / 1000;
+	public static final float MINUTES                    = SECONDS * 60;
+	public static final float HOURS                      = MINUTES * 60;
+	public static final float DAYS                       = HOURS * 24;
+	
+	// Frequency
+	public static final float HERTZ                      = 1 / SECONDS;
+	public static final float KILOHERTZ                  = HERTZ * 1000;
+
+	// Velocity                                          
+	public static final float METERS_PER_SECOND          = METERS / SECONDS;
+	public static final float KILOMETERS_PER_HOUR        = KILOMETERS / HOURS;
+
+	// Acceleration                                      
+	public static final float METERS_PER_SECOND_SQUARED  = METERS_PER_SECOND / SECONDS;
+
+	// Force                                             
+	public static final float NEWTONS                    = METERS_PER_SECOND_SQUARED * KILOGRAMS;
+
+}
diff --git a/src/main/java/ru/windcorp/progressia/common/collision/AABB.java b/src/main/java/ru/windcorp/progressia/common/collision/AABB.java
index 7188e33..e141673 100644
--- a/src/main/java/ru/windcorp/progressia/common/collision/AABB.java
+++ b/src/main/java/ru/windcorp/progressia/common/collision/AABB.java
@@ -1,93 +1,113 @@
 package ru.windcorp.progressia.common.collision;
 
-import java.util.Collection;
-import java.util.Map;
-
 import glm.vec._3.Vec3;
-import ru.windcorp.progressia.common.world.block.BlockFace;
 
-public class AABB implements CollisionModel {
+/**
+ * An implementation of an
+ * Axis-Aligned Bounding Box.
+ * @author javapony
+ */
+public class AABB implements AABBoid {
+	
+	private class AABBWallImpl implements Wall {
+		
+		private final Vec3 originOffset = new Vec3();
+		private final Vec3 widthSelector = new Vec3();
+		private final Vec3 heightSelector = new Vec3();
 
-	private final Map faces = BlockFace.mapToFaces(
-			new CollisionWall(-0.5f, -0.5f, -0.5f,   +1,  0,  0,    0,  0, +1),
-			new CollisionWall(+0.5f, -0.5f, -0.5f,    0, +1,  0,    0,  0, +1),
-			new CollisionWall(+0.5f, +0.5f, -0.5f,   -1,  0,  0,    0,  0, +1),
-			new CollisionWall(-0.5f, +0.5f, -0.5f,    0, -1,  0,    0,  0, +1),
-			
-			new CollisionWall(-0.5f, -0.5f, +0.5f,   +1,  0,  0,    0, +1,  0),
-			new CollisionWall(-0.5f, -0.5f, -0.5f,    0, +1,  0,   +1,  0,  0)
-	);
+		public AABBWallImpl(
+			float ox, float oy, float oz,
+			float wx, float wy, float wz,
+			float hx, float hy, float hz
+		) {
+			this.originOffset.set(ox, oy, oz);
+			this.widthSelector.set(wx, wy, wz);
+			this.heightSelector.set(hx, hy, hz);
+		}
+
+		@Override
+		public void getOrigin(Vec3 output) {
+			output.set(originOffset).mul(AABB.this.getSize()).add(AABB.this.getOrigin());
+		}
+	
+		@Override
+		public void getWidth(Vec3 output) {
+			output.set(AABB.this.getSize()).mul(widthSelector);
+		}
+	
+		@Override
+		public void getHeight(Vec3 output) {
+			output.set(AABB.this.getSize()).mul(heightSelector);
+		}
+		
+	}
+	
+	public static final AABB UNIT_CUBE = new AABB(0, 0, 0, 1, 1, 1);
+	
+	private final Wall[] walls = new Wall[] {
+			new AABBWallImpl(-0.5f, -0.5f, +0.5f,   +1,  0,  0,    0, +1,  0), // Top
+			new AABBWallImpl(-0.5f, -0.5f, -0.5f,    0, +1,  0,   +1,  0,  0), // Bottom
+			new AABBWallImpl(+0.5f, -0.5f, -0.5f,    0, +1,  0,    0,  0, +1), // North
+			new AABBWallImpl(-0.5f, +0.5f, -0.5f,    0, -1,  0,    0,  0, +1), // South
+			new AABBWallImpl(+0.5f, +0.5f, -0.5f,   -1,  0,  0,    0,  0, +1), // West
+			new AABBWallImpl(-0.5f, -0.5f, -0.5f,   +1,  0,  0,    0,  0, +1)  // East
+	};
 	
 	private final Vec3 origin = new Vec3();
 	private final Vec3 size = new Vec3();
 	
 	public AABB(Vec3 origin, Vec3 size) {
-		this.origin.set(origin);
-		this.size.set(size);
-		
-		for (CollisionWall wall : getFaces()) {
-			wall.moveOrigin(origin);
-			wall.getWidth().mul(size);
-			wall.getHeight().mul(size);
-		}
+		this(origin.x, origin.y, origin.z, size.x, size.y, size.z);
 	}
 	
 	public AABB(
-			float ox, float oy, float oz,
+			float ox,    float oy,    float oz,
 			float xSize, float ySize, float zSize
 	) {
 		this.origin.set(ox, oy, oz);
 		this.size.set(xSize, ySize, zSize);
-		
-		for (CollisionWall wall : getFaces()) {
-			wall.moveOrigin(ox, oy, oz);
-			wall.getWidth().mul(xSize, ySize, zSize);
-			wall.getHeight().mul(xSize, ySize, zSize);
-		}
-	}
-	
-	public Collection getFaces() {
-		return faces.values();
 	}
 	
 	public Vec3 getOrigin() {
 		return origin;
 	}
+
+	@Override
+	public void getOrigin(Vec3 output) {
+		output.set(origin);
+	}
 	
 	@Override
 	public void setOrigin(Vec3 origin) {
-		for (CollisionWall wall : getFaces()) {
-			wall.getOrigin().sub(this.origin).add(origin);
-		}
-
 		this.origin.set(origin);
 	}
 	
 	@Override
 	public void moveOrigin(Vec3 displacement) {
-		for (CollisionWall wall : getFaces()) {
-			wall.getOrigin().add(displacement);
-		}
-
 		this.origin.add(displacement);
 	}
 	
 	public Vec3 getSize() {
 		return size;
 	}
+
+	@Override
+	public void getSize(Vec3 output) {
+		output.set(size);
+	}
 	
 	public void setSize(Vec3 size) {
 		setSize(size.x, size.y, size.z);
 	}
 	
 	public void setSize(float xSize, float ySize, float zSize) {
-		for (CollisionWall wall : getFaces()) {
-			wall.getWidth().div(this.size).mul(xSize, ySize, zSize);
-			wall.getHeight().div(this.size).mul(xSize, ySize, zSize);
-			wall.getOrigin().sub(getOrigin()).div(this.size).mul(xSize, ySize, zSize).add(getOrigin());
-		}
-		
 		this.size.set(xSize, ySize, zSize);
 	}
+
+	@Override
+	public Wall getWall(int faceId) {
+		// No, we don't support Apple.
+		return walls[faceId];
+	}
 	
 }
diff --git a/src/main/java/ru/windcorp/progressia/common/collision/AABBoid.java b/src/main/java/ru/windcorp/progressia/common/collision/AABBoid.java
new file mode 100644
index 0000000..6e1d384
--- /dev/null
+++ b/src/main/java/ru/windcorp/progressia/common/collision/AABBoid.java
@@ -0,0 +1,17 @@
+package ru.windcorp.progressia.common.collision;
+
+import glm.vec._3.Vec3;
+import ru.windcorp.progressia.common.world.block.BlockFace;
+
+public interface AABBoid extends CollisionModel {
+	
+	void getOrigin(Vec3 output);
+	void getSize(Vec3 output);
+	
+	default Wall getWall(BlockFace face) {
+		return getWall(face.getId());
+	}
+	
+	Wall getWall(int faceId);
+
+}
diff --git a/src/main/java/ru/windcorp/progressia/common/collision/CollisionClock.java b/src/main/java/ru/windcorp/progressia/common/collision/CollisionClock.java
deleted file mode 100644
index cf42614..0000000
--- a/src/main/java/ru/windcorp/progressia/common/collision/CollisionClock.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package ru.windcorp.progressia.common.collision;
-
-public interface CollisionClock {
-	
-	float getTime();
-	void advanceTime(float change);
-
-}
diff --git a/src/main/java/ru/windcorp/progressia/common/collision/CollisionPathComputer.java b/src/main/java/ru/windcorp/progressia/common/collision/CollisionPathComputer.java
new file mode 100644
index 0000000..eda8e47
--- /dev/null
+++ b/src/main/java/ru/windcorp/progressia/common/collision/CollisionPathComputer.java
@@ -0,0 +1,79 @@
+package ru.windcorp.progressia.common.collision;
+
+import java.util.function.Consumer;
+
+import glm.vec._3.Vec3;
+import glm.vec._3.i.Vec3i;
+import ru.windcorp.progressia.common.util.Vectors;
+
+import static java.lang.Math.*;
+
+public class CollisionPathComputer {
+	
+	public static void forEveryBlockInCollisionPath(
+			Collideable coll,
+			float maxTime,
+			Consumer action
+	) {
+		Vec3 displacement = Vectors.grab3();
+		coll.getCollideableVelocity(displacement);
+		displacement.mul(maxTime);
+		
+		handleModel(coll.getCollisionModel(), displacement, action);
+		
+		Vectors.release(displacement);
+	}
+
+	private static void handleModel(
+			CollisionModel model,
+			Vec3 displacement,
+			Consumer action
+	) {
+		if (model instanceof CompoundCollisionModel) {
+			for (CollisionModel subModel : ((CompoundCollisionModel) model).getModels()) {
+				handleModel(subModel, displacement, action);
+			}
+		} else if (model instanceof AABBoid) {
+			handleAABBoid((AABBoid) model, displacement, action);
+		} else {
+			throw new RuntimeException("not supported");
+		}
+	}
+
+	private static void handleAABBoid(AABBoid model, Vec3 displacement, Consumer action) {
+		Vec3 size = Vectors.grab3();
+		Vec3 origin = Vectors.grab3();
+		
+		model.getOrigin(origin);
+		model.getSize(size);
+		
+		origin.mul(2).sub(size).div(2); // Subtract 0.5*size
+		
+		Vec3i pos = Vectors.grab3i();
+		
+		for (
+				pos.x =  (int) floor(origin.x + min(0, size.x) + min(0, displacement.x));
+				pos.x <= (int)  ceil(origin.x + max(0, size.x) + max(0, displacement.x));
+				pos.x += 1
+		) {
+			for (
+					pos.y =  (int) floor(origin.y + min(0, size.y) + min(0, displacement.y));
+					pos.y <= (int)  ceil(origin.y + max(0, size.y) + max(0, displacement.y));
+					pos.y += 1
+			) {
+				for (
+						pos.z =  (int) floor(origin.z + min(0, size.z) + min(0, displacement.z));
+						pos.z <= (int)  ceil(origin.z + max(0, size.z) + max(0, displacement.z));
+						pos.z += 1
+				) {
+					action.accept(pos);
+				}
+			}
+		}
+		
+		Vectors.release(origin);
+		Vectors.release(size);
+		Vectors.release(pos);
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/progressia/common/collision/CollisionWall.java b/src/main/java/ru/windcorp/progressia/common/collision/CollisionWall.java
deleted file mode 100644
index 8963ffd..0000000
--- a/src/main/java/ru/windcorp/progressia/common/collision/CollisionWall.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package ru.windcorp.progressia.common.collision;
-
-import glm.vec._3.Vec3;
-
-public class CollisionWall {
-	
-	private final Vec3 origin = new Vec3();
-	private final Vec3 width = new Vec3();
-	private final Vec3 height = new Vec3();
-	
-	public CollisionWall(Vec3 origin, Vec3 width, Vec3 height) {
-		this.origin.set(origin);
-		this.width.set(width);
-		this.height.set(height);
-	}
-	
-	public CollisionWall(
-		float ox, float oy, float oz,
-		float wx, float wy, float wz,
-		float hx, float hy, float hz
-	) {
-		this.origin.set(ox, oy, oz);
-		this.width.set(wx, wy, wz);
-		this.height.set(hx, hy, hz);
-	}
-	
-	public Vec3 getOrigin() {
-		return origin;
-	}
-	
-	public Vec3 getWidth() {
-		return width;
-	}
-	
-	public Vec3 getHeight() {
-		return height;
-	}
-	
-	public void setOrigin(Vec3 origin) {
-		setOrigin(origin.x, origin.y, origin.z);
-	}
-	
-	public void setOrigin(float x, float y, float z) {
-		this.origin.set(x, y, z);
-	}
-	
-	public void moveOrigin(Vec3 displacement) {
-		moveOrigin(displacement.x, displacement.y, displacement.z);
-	}
-	
-	public void moveOrigin(float dx, float dy, float dz) {
-		this.origin.add(dx, dy, dz);
-	}
-
-}
diff --git a/src/main/java/ru/windcorp/progressia/common/collision/CompoundCollisionModel.java b/src/main/java/ru/windcorp/progressia/common/collision/CompoundCollisionModel.java
index 2cf4572..ab5e766 100644
--- a/src/main/java/ru/windcorp/progressia/common/collision/CompoundCollisionModel.java
+++ b/src/main/java/ru/windcorp/progressia/common/collision/CompoundCollisionModel.java
@@ -8,9 +8,9 @@ import glm.vec._3.Vec3;
 
 public class CompoundCollisionModel implements CollisionModel {
 	
-	private final Collection models;
+	private final Collection extends CollisionModel> models;
 
-	public CompoundCollisionModel(Collection models) {
+	public CompoundCollisionModel(Collection extends CollisionModel> models) {
 		this.models = models;
 	}
 	
@@ -18,7 +18,7 @@ public class CompoundCollisionModel implements CollisionModel {
 		this(ImmutableList.copyOf(models));
 	}
 	
-	public Collection getModels() {
+	public Collection extends CollisionModel> getModels() {
 		return models;
 	}
 
diff --git a/src/main/java/ru/windcorp/progressia/common/collision/TranslatedAABB.java b/src/main/java/ru/windcorp/progressia/common/collision/TranslatedAABB.java
new file mode 100644
index 0000000..a22c330
--- /dev/null
+++ b/src/main/java/ru/windcorp/progressia/common/collision/TranslatedAABB.java
@@ -0,0 +1,105 @@
+package ru.windcorp.progressia.common.collision;
+
+import glm.vec._3.Vec3;
+import ru.windcorp.progressia.common.util.Vectors;
+import ru.windcorp.progressia.common.world.block.BlockFace;
+
+public class TranslatedAABB implements AABBoid {
+	
+	private class TranslatedAABBWall implements Wall {
+		private final int id;
+		
+		public TranslatedAABBWall(int id) {
+			this.id = id;
+		}
+
+		@Override
+		public void getOrigin(Vec3 output) {
+			parent.getWall(id).getOrigin(output);
+			output.add(translation);
+		}
+		
+		@Override
+		public void getWidth(Vec3 output) {
+			parent.getWall(id).getWidth(output);
+		}
+		
+		@Override
+		public void getHeight(Vec3 output) {
+			parent.getWall(id).getHeight(output);
+		}
+	}
+	
+	private AABBoid parent;
+	private final Vec3 translation = new Vec3();
+	
+	private final TranslatedAABBWall[] walls = new TranslatedAABBWall[BlockFace.BLOCK_FACE_COUNT];
+	
+	{
+		for (int id = 0; id < walls.length; ++id) {
+			walls[id] = new TranslatedAABBWall(id);
+		}
+	}
+	
+	public TranslatedAABB(AABBoid parent, float tx, float ty, float tz) {
+		setParent(parent);
+		setTranslation(tx, ty, tz);
+	}
+	
+	public TranslatedAABB(AABBoid parent, Vec3 translation) {
+		this(parent, translation.x, translation.y, translation.z);
+	}
+	
+	public TranslatedAABB() {
+		this(null, 0, 0, 0);
+	}
+
+	@Override
+	public void setOrigin(Vec3 origin) {
+		Vec3 v = Vectors.grab3().set(origin).sub(translation);
+		parent.setOrigin(v);
+		Vectors.release(v);
+	}
+
+	@Override
+	public void moveOrigin(Vec3 displacement) {
+		parent.moveOrigin(displacement);
+	}
+
+	@Override
+	public void getOrigin(Vec3 output) {
+		parent.getOrigin(output);
+		output.add(translation);
+	}
+
+	@Override
+	public void getSize(Vec3 output) {
+		parent.getSize(output);
+	}
+
+	@Override
+	public Wall getWall(int faceId) {
+		return walls[faceId];
+	}
+	
+	public AABBoid getParent() {
+		return parent;
+	}
+	
+	public void setParent(AABBoid parent) {
+		this.parent = parent;
+	}
+	
+	public Vec3 getTranslation() {
+		return translation;
+	}
+	
+	public void setTranslation(Vec3 translation) {
+		setTranslation(translation.x, translation.y, translation.z);
+	}
+	
+	public void setTranslation(float tx, float ty, float tz) {
+		this.translation.set(tx, ty, tz);
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/progressia/common/collision/Wall.java b/src/main/java/ru/windcorp/progressia/common/collision/Wall.java
new file mode 100644
index 0000000..9549f04
--- /dev/null
+++ b/src/main/java/ru/windcorp/progressia/common/collision/Wall.java
@@ -0,0 +1,12 @@
+package ru.windcorp.progressia.common.collision;
+
+import glm.vec._3.Vec3;
+
+public interface Wall {
+	
+	void getOrigin(Vec3 output);
+	
+	void getWidth(Vec3 output);
+	void getHeight(Vec3 output);
+
+}
diff --git a/src/main/java/ru/windcorp/progressia/common/collision/WorldCollisionHelper.java b/src/main/java/ru/windcorp/progressia/common/collision/WorldCollisionHelper.java
new file mode 100644
index 0000000..ebf2c0b
--- /dev/null
+++ b/src/main/java/ru/windcorp/progressia/common/collision/WorldCollisionHelper.java
@@ -0,0 +1,94 @@
+package ru.windcorp.progressia.common.collision;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import glm.vec._3.Vec3;
+import glm.vec._3.i.Vec3i;
+import ru.windcorp.progressia.common.util.LowOverheadCache;
+import ru.windcorp.progressia.common.world.WorldData;
+
+public class WorldCollisionHelper {
+	
+	private final Collideable collideable = new Collideable() {
+		@Override
+		public boolean onCollision(Collideable other) {
+			return false;
+		}
+		
+		@Override
+		public void moveAsCollideable(Vec3 displacement) {
+			// Ignore
+			assert displacement.length() < 1e-3f;
+		}
+		
+		@Override
+		public CollisionModel getCollisionModel() {
+			return WorldCollisionHelper.this.model;
+		}
+		
+		@Override
+		public float getCollisionMass() {
+			return Float.POSITIVE_INFINITY;
+		}
+		
+		@Override
+		public void getCollideableVelocity(Vec3 output) {
+			output.set(0);
+		}
+		
+		@Override
+		public void changeVelocityOnCollision(Vec3 velocityChange) {
+			// Ignore
+			assert velocityChange.length() < 1e-3f;
+		}
+	};
+	
+	private final Collection activeBlockModels = new ArrayList<>();
+	private final CollisionModel model = new CompoundCollisionModel(activeBlockModels);
+	private final LowOverheadCache blockModelCache = new LowOverheadCache<>(TranslatedAABB::new);
+	
+	/**
+	 * Changes the state of this helper's {@link #getCollideable()} so it is ready to adequately handle
+	 * collisions with the {@code collideable} that might happen in the next {@code maxTime} seconds.
+	 * This helper is only valid for checking collisions with the given Collideable and only within
+	 * the given time limit.
+	 * @param collideable the {@link Collideable} that collisions will be checked against
+	 * @param maxTime maximum collision time
+	 */
+	public void tuneToCollideable(WorldData world, Collideable collideable, float maxTime) {
+		activeBlockModels.forEach(blockModelCache::release);
+		activeBlockModels.clear();
+		CollisionPathComputer.forEveryBlockInCollisionPath(
+				collideable,
+				maxTime,
+				v -> addModel(world.getCollisionModelOfBlock(v), v)
+		);
+	}
+	
+	private void addModel(CollisionModel model, Vec3i pos) {
+		if (model == null) {
+			// Ignore
+		} else if (model instanceof AABBoid) {
+			addAABBoidModel((AABBoid) model, pos);
+		} else if (model instanceof CompoundCollisionModel) {
+			for (CollisionModel subModel : ((CompoundCollisionModel) model).getModels()) {
+				addModel(subModel, pos);
+			}
+		} else {
+			throw new RuntimeException("not supported");
+		}
+	}
+	
+	private void addAABBoidModel(AABBoid model, Vec3i pos) {
+		TranslatedAABB translator = blockModelCache.grab();
+		translator.setParent(model);
+		translator.setTranslation(pos.x, pos.y, pos.z);
+		activeBlockModels.add(translator);
+	}
+
+	public Collideable getCollideable() {
+		return collideable;
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/progressia/common/collision/colliders/AABBWithAABBCollider.java b/src/main/java/ru/windcorp/progressia/common/collision/colliders/AABBoidCollider.java
similarity index 78%
rename from src/main/java/ru/windcorp/progressia/common/collision/colliders/AABBWithAABBCollider.java
rename to src/main/java/ru/windcorp/progressia/common/collision/colliders/AABBoidCollider.java
index 7b961e1..f440761 100644
--- a/src/main/java/ru/windcorp/progressia/common/collision/colliders/AABBWithAABBCollider.java
+++ b/src/main/java/ru/windcorp/progressia/common/collision/colliders/AABBoidCollider.java
@@ -2,26 +2,25 @@ package ru.windcorp.progressia.common.collision.colliders;
 
 import glm.mat._3.Mat3;
 import glm.vec._3.Vec3;
-import ru.windcorp.progressia.common.collision.AABB;
-import ru.windcorp.progressia.common.collision.Collideable;
-import ru.windcorp.progressia.common.collision.CollisionWall;
+import ru.windcorp.progressia.common.collision.*;
 import ru.windcorp.progressia.common.collision.colliders.Collider.ColliderWorkspace;
 import ru.windcorp.progressia.common.collision.colliders.Collider.Collision;
 import ru.windcorp.progressia.common.util.Matrices;
 import ru.windcorp.progressia.common.util.Vectors;
+import ru.windcorp.progressia.common.world.block.BlockFace;
 
-class AABBWithAABBCollider {
+class AABBoidCollider {
 	
 	static Collider.Collision computeModelCollision(
 			Collideable aBody, Collideable bBody,
-			AABB aModel, AABB bModel,
+			AABBoid aModel, AABBoid bModel,
 			float tickLength,
 			ColliderWorkspace workspace
 	) {
 		Collideable obstacleBody = bBody;
 		Collideable colliderBody = aBody;
-		AABB obstacleModel = bModel;
-		AABB colliderModel = aModel;
+		AABBoid obstacleModel = bModel;
+		AABBoid colliderModel = aModel;
 		
 		Collision result = null;
 		
@@ -32,7 +31,8 @@ class AABBWithAABBCollider {
 		computeCollisionVelocity(collisionVelocity, obstacleBody, colliderBody);
 		
 		// For every wall of collision space
-		for (CollisionWall wall : originCollisionSpace.getFaces()) {
+		for (int i = 0; i < BlockFace.BLOCK_FACE_COUNT; ++i) {
+			Wall wall = originCollisionSpace.getWall(i);
 				
 			Collision collision = computeWallCollision(
 					wall, colliderModel,
@@ -80,12 +80,21 @@ class AABBWithAABBCollider {
 		Vectors.release(colliderVelocity);
 	}
 
-	private static AABB createOriginCollisionSpace(AABB obstacle, AABB collider, AABB output) {
-		output.setOrigin(obstacle.getOrigin());
+	private static AABB createOriginCollisionSpace(AABBoid obstacle, AABBoid collider, AABB output) {
+		Vec3 obstacleOrigin = Vectors.grab3();
+		Vec3 obstacleSize = Vectors.grab3();
+		Vec3 colliderSize = Vectors.grab3();
 		
-		Vec3 size = Vectors.grab3().set(obstacle.getSize()).add(collider.getSize());
-		output.setSize(size);
-		Vectors.release(size);
+		obstacle.getOrigin(obstacleOrigin);
+		output.setOrigin(obstacleOrigin);
+		
+		obstacle.getSize(obstacleSize);
+		collider.getSize(colliderSize);
+		output.setSize(obstacleSize.add(colliderSize));
+		
+		Vectors.release(obstacleOrigin);
+		Vectors.release(obstacleSize);
+		Vectors.release(colliderSize);
 		
 		return output;
 	}
@@ -134,27 +143,34 @@ class AABBWithAABBCollider {
 	 * If all conditions are satisfied, then the moment of impact is t0 + t.
 	 */
 	private static Collision computeWallCollision(
-			CollisionWall obstacleWall,
-			AABB colliderModel,
+			Wall obstacleWall,
+			AABBoid colliderModel,
 			Vec3 collisionVelocity,
 			float tickLength, ColliderWorkspace workspace,
 			Collideable aBody, Collideable bBody
 	) {
-		Vec3 w = obstacleWall.getWidth();
-		Vec3 h = obstacleWall.getHeight();
+		Vec3 w = Vectors.grab3();
+		Vec3 h = Vectors.grab3();
 		Vec3 v = Vectors.grab3();
 		Mat3 m = Matrices.grab3(); // The matrix [w h -v]
 		Vec3 r = Vectors.grab3();
+		Vec3 r_line = Vectors.grab3();
+		Vec3 r_wall = Vectors.grab3();
 		Vec3 xyt = Vectors.grab3();
 		
 		try {
+			obstacleWall.getWidth(w);
+			obstacleWall.getHeight(h);
+			
 			v.set(collisionVelocity);
 			
 			if (isExiting(v, w, h)) {
 				return null;
 			}
 			
-			r.set(colliderModel.getOrigin()).sub(obstacleWall.getOrigin());
+			obstacleWall.getOrigin(r_wall);
+			colliderModel.getOrigin(r_line);
+			r.set(r_line).sub(r_wall);
 			m.c0(w).c1(h).c2(v.negate());
 			
 			if (Math.abs(m.det()) < 1e-6) {
@@ -179,9 +195,13 @@ class AABBWithAABBCollider {
 			
 			return workspace.grab().set(aBody, bBody, obstacleWall, t);
 		} finally {
+			Vectors.release(w);
+			Vectors.release(h);
 			Vectors.release(v);
-			Vectors.release(r);
 			Matrices.release(m);
+			Vectors.release(r);
+			Vectors.release(r_line);
+			Vectors.release(r_wall);
 			Vectors.release(xyt);
 		}
 	}
@@ -193,6 +213,6 @@ class AABBWithAABBCollider {
 		return result;
 	}
 
-	private AABBWithAABBCollider() {}
+	private AABBoidCollider() {}
 
 }
diff --git a/src/main/java/ru/windcorp/progressia/common/collision/colliders/Collider.java b/src/main/java/ru/windcorp/progressia/common/collision/colliders/Collider.java
index 74bcc2c..8fdbc90 100644
--- a/src/main/java/ru/windcorp/progressia/common/collision/colliders/Collider.java
+++ b/src/main/java/ru/windcorp/progressia/common/collision/colliders/Collider.java
@@ -6,14 +6,10 @@ import java.util.List;
 import org.apache.logging.log4j.LogManager;
 
 import glm.vec._3.Vec3;
-import ru.windcorp.progressia.common.collision.AABB;
-import ru.windcorp.progressia.common.collision.Collideable;
-import ru.windcorp.progressia.common.collision.CollisionClock;
-import ru.windcorp.progressia.common.collision.CollisionModel;
-import ru.windcorp.progressia.common.collision.CollisionWall;
-import ru.windcorp.progressia.common.collision.CompoundCollisionModel;
+import ru.windcorp.progressia.common.collision.*;
 import ru.windcorp.progressia.common.util.LowOverheadCache;
 import ru.windcorp.progressia.common.util.Vectors;
+import ru.windcorp.progressia.common.world.WorldData;
 
 public class Collider {
 	
@@ -21,7 +17,7 @@ public class Collider {
 	
 	public static void performCollisions(
 			List extends Collideable> colls,
-			CollisionClock clock,
+			WorldData world,
 			float tickLength,
 			ColliderWorkspace workspace
 	) {
@@ -37,12 +33,12 @@ public class Collider {
 				return;
 			}
 			
-			Collision firstCollision = getFirstCollision(colls, tickLength, workspace);
+			Collision firstCollision = getFirstCollision(colls, tickLength, world, workspace);
 			
 			if (firstCollision == null) {
 				break;
 			} else {
-				collide(firstCollision, colls, clock, tickLength, workspace);
+				collide(firstCollision, colls, world, tickLength, workspace);
 				workspace.release(firstCollision);
 				collisionCount++;
 				
@@ -50,45 +46,49 @@ public class Collider {
 			}
 		}
 		
-		advanceTime(colls, clock, tickLength);
+		advanceTime(colls, world, tickLength);
 	}
 
 	private static Collision getFirstCollision(
 			List extends Collideable> colls,
 			float tickLength,
+			WorldData world,
 			ColliderWorkspace workspace
 	) {
 		Collision result = null;
+		Collideable worldColl = workspace.worldCollisionHelper.getCollideable();
 		
 		// For every pair of colls
 		for (int i = 0; i < colls.size(); ++i) {
 			Collideable a = colls.get(i);
 			
+			tuneWorldCollisionHelper(a, tickLength, world, workspace);
+			
+			result = workspace.updateLatestCollision(
+					result,
+					getCollision(a, worldColl, tickLength, workspace)
+			);
+			
 			for (int j = i + 1; j < colls.size(); ++j) {
 				Collideable b = colls.get(j);
-				
 				Collision collision = getCollision(a, b, tickLength, workspace);
-				
-				// Update result
-				if (collision != null) {
-					Collision second;
-					
-					if (result == null || collision.time < result.time) {
-						second = result;
-						result = collision;
-					} else {
-						second = collision;
-					}
-					
-					// Release Collision that is no longer used
-					if (second != null) workspace.release(second);
-				}
+				result = workspace.updateLatestCollision(result, collision);
 			}
 		}
 		
 		return result;
 	}
 	
+	private static void tuneWorldCollisionHelper(
+			Collideable coll,
+			float tickLength,
+			WorldData world,
+			ColliderWorkspace workspace
+	) {
+		WorldCollisionHelper wch = workspace.worldCollisionHelper;
+		wch.tuneToCollideable(world, coll, tickLength);
+	}
+
 	static Collision getCollision(
 			Collideable a,
 			Collideable b,
@@ -108,10 +108,10 @@ public class Collider {
 			float tickLength,
 			ColliderWorkspace workspace
 	) {
-		if (aModel instanceof AABB && bModel instanceof AABB) {
-			return AABBWithAABBCollider.computeModelCollision(
+		if (aModel instanceof AABBoid && bModel instanceof AABBoid) {
+			return AABBoidCollider.computeModelCollision(
 					aBody, bBody,
-					(AABB) aModel, (AABB) bModel,
+					(AABBoid) aModel, (AABBoid) bModel,
 					tickLength,
 					workspace
 			);
@@ -144,11 +144,11 @@ public class Collider {
 			Collision collision,
 			
 			Collection extends Collideable> colls,
-			CollisionClock clock,
+			WorldData world,
 			float tickLength,
 			ColliderWorkspace workspace
 	) {
-		advanceTime(colls, clock, collision.time);
+		advanceTime(colls, world, collision.time);
 		
 		boolean doNotHandle = false;
 		
@@ -237,7 +237,7 @@ public class Collider {
 		Vec3 du_a = Vectors.grab3();
 		Vec3 du_b = Vectors.grab3();
 		
-		n.set(collision.wall.getWidth()).cross(collision.wall.getHeight()).normalize();
+		n.set(collision.wallWidth).cross(collision.wallHeight).normalize();
 		collision.a.getCollideableVelocity(v_a);
 		collision.b.getCollideableVelocity(v_b);
 		
@@ -306,10 +306,10 @@ public class Collider {
 
 	private static void advanceTime(
 			Collection extends Collideable> colls,
-			CollisionClock clock,
+			WorldData world,
 			float step
 	) {
-		clock.advanceTime(step);
+		world.advanceTime(step);
 		
 		Vec3 tmp = Vectors.grab3();
 		
@@ -328,6 +328,8 @@ public class Collider {
 				new LowOverheadCache<>(Collision::new);
 		
 		AABB dummyAABB = new AABB(0, 0, 0, 1, 1, 1);
+		
+		WorldCollisionHelper worldCollisionHelper = new WorldCollisionHelper();
 
 		Collision grab() {
 			return collisionCache.grab();
@@ -337,12 +339,35 @@ public class Collider {
 			collisionCache.release(object);
 		}
 		
+		Collision updateLatestCollision(Collision a, Collision b) {
+			if (a == null) {
+				return b; // may be null
+			} else if (b == null) {
+				return a;
+			}
+			
+			Collision first, second;
+			
+			if (a.time > b.time) {
+				first = b;
+				second = a;
+			} else {
+				first = a;
+				second = b;
+			}
+			
+			release(second);
+			return first;
+		}
+		
 	}
 	
 	static class Collision {
 		public Collideable a;
 		public Collideable b;
-		public final CollisionWall wall = new CollisionWall(0, 0, 0, 0, 0, 0, 0, 0, 0);
+		
+		public final Vec3 wallWidth = new Vec3();
+		public final Vec3 wallHeight = new Vec3();
 		
 		/**
 		 * Time offset from the start of the tick.
@@ -350,12 +375,15 @@ public class Collider {
 		 */
 		public float time;
 		
-		public Collision set(Collideable a, Collideable b, CollisionWall wall, float time) {
+		public Collision set(
+				Collideable a, Collideable b,
+				Wall wall,
+				float time
+		) {
 			this.a = a;
 			this.b = b;
-			this.wall.getOrigin().set(wall.getOrigin());
-			this.wall.getWidth().set(wall.getWidth());
-			this.wall.getHeight().set(wall.getHeight());
+			wall.getWidth(wallWidth);
+			wall.getHeight(wallHeight);
 			this.time = time;
 			
 			return this;
diff --git a/src/main/java/ru/windcorp/progressia/common/util/Vectors.java b/src/main/java/ru/windcorp/progressia/common/util/Vectors.java
index 489bf13..7cc3623 100644
--- a/src/main/java/ru/windcorp/progressia/common/util/Vectors.java
+++ b/src/main/java/ru/windcorp/progressia/common/util/Vectors.java
@@ -30,6 +30,19 @@ import glm.vec._4.i.Vec4i;
  */
 public class Vectors {
 	
+	public static final Vec2  ZERO_2  = new Vec2 (0, 0);
+	public static final Vec2i ZERO_2i = new Vec2i(0, 0);
+	public static final Vec3  ZERO_3  = new Vec3 (0, 0, 0);
+	public static final Vec3i ZERO_3i = new Vec3i(0, 0, 0);
+	public static final Vec4  ZERO_4  = new Vec4 (0, 0, 0, 0);
+	public static final Vec4i ZERO_4i = new Vec4i(0, 0, 0, 0);
+	public static final Vec2  UNIT_2  = new Vec2 (1, 1);
+	public static final Vec2i UNIT_2i = new Vec2i(1, 1);
+	public static final Vec3  UNIT_3  = new Vec3 (1, 1, 1);
+	public static final Vec3i UNIT_3i = new Vec3i(1, 1, 1);
+	public static final Vec4  UNIT_4  = new Vec4 (1, 1, 1, 1);
+	public static final Vec4i UNIT_4i = new Vec4i(1, 1, 1, 1);
+	
 	private static final LowOverheadCache VEC3IS =
 			new LowOverheadCache<>(Vec3i::new);
 	
diff --git a/src/main/java/ru/windcorp/progressia/common/world/ChunkData.java b/src/main/java/ru/windcorp/progressia/common/world/ChunkData.java
index 18d44b0..f25ac0a 100644
--- a/src/main/java/ru/windcorp/progressia/common/world/ChunkData.java
+++ b/src/main/java/ru/windcorp/progressia/common/world/ChunkData.java
@@ -80,7 +80,7 @@ public class ChunkData {
 		TileData flowers = TileDataRegistry.getInstance().get("Test:YellowFlowers");
 		TileData sand = TileDataRegistry.getInstance().get("Test:Sand");
 	
-		Vec3i aPoint = new Vec3i(5, 0, BLOCKS_PER_CHUNK + BLOCKS_PER_CHUNK/2);
+		Vec3i aPoint = new Vec3i(5, 0, BLOCKS_PER_CHUNK + BLOCKS_PER_CHUNK/2).sub(getPosition());
 		Vec3i pos = new Vec3i();
 		
 		for (int x = 0; x < BLOCKS_PER_CHUNK; ++x) {
@@ -132,18 +132,28 @@ public class ChunkData {
 			}
 		}
 		
-		EntityData javapony = EntityDataRegistry.getInstance().create("Test:Javapony");
-		javapony.setEntityId(0x42);
-		javapony.setPosition(new Vec3(-6, -6, 20));
-		javapony.setDirection(new Vec2(
-				(float) Math.toRadians(40), (float) Math.toRadians(45)
-		));
-		getEntities().add(javapony);
-		
-		EntityData statie = EntityDataRegistry.getInstance().create("Test:Statie");
-		statie.setEntityId(0xDEADBEEF);
-		statie.setPosition(new Vec3(0, 15, 16));
-		getEntities().add(statie);
+		if (!getPosition().any()) {
+//			EntityData javapony = EntityDataRegistry.getInstance().create("Test:Javapony");
+//			javapony.setEntityId(0x42);
+//			javapony.setPosition(new Vec3(-6, -6, 20));
+//			javapony.setDirection(new Vec2(
+//					(float) Math.toRadians(40), (float) Math.toRadians(45)
+//			));
+//			getEntities().add(javapony);
+			
+			EntityData player = EntityDataRegistry.getInstance().create("Test:Player");
+			player.setEntityId(0x42);
+			player.setPosition(new Vec3(-6, -6, 20));
+			player.setDirection(new Vec2(
+					(float) Math.toRadians(40), (float) Math.toRadians(45)
+			));
+			getEntities().add(player);
+			
+			EntityData statie = EntityDataRegistry.getInstance().create("Test:Statie");
+			statie.setEntityId(0xDEADBEEF);
+			statie.setPosition(new Vec3(0, 15, 16));
+			getEntities().add(statie);
+		}
 	}
 
 	public BlockData getBlock(Vec3i posInChunk) {
diff --git a/src/main/java/ru/windcorp/progressia/common/world/WorldData.java b/src/main/java/ru/windcorp/progressia/common/world/WorldData.java
index 6440709..6584884 100644
--- a/src/main/java/ru/windcorp/progressia/common/world/WorldData.java
+++ b/src/main/java/ru/windcorp/progressia/common/world/WorldData.java
@@ -24,8 +24,10 @@ import glm.vec._3.i.Vec3i;
 import gnu.trove.impl.sync.TSynchronizedLongObjectMap;
 import gnu.trove.map.TLongObjectMap;
 import gnu.trove.map.hash.TLongObjectHashMap;
+import ru.windcorp.progressia.common.collision.CollisionModel;
 import ru.windcorp.progressia.common.util.CoordinatePacker;
 import ru.windcorp.progressia.common.util.Vectors;
+import ru.windcorp.progressia.common.world.block.BlockData;
 import ru.windcorp.progressia.common.world.entity.EntityData;
 
 public class WorldData {
@@ -42,6 +44,8 @@ public class WorldData {
 	private final Collection entities =
 			Collections.unmodifiableCollection(entitiesById.valueCollection());
 	
+	private float time = 0;
+	
 	public WorldData() {
 		final int size = 1;
 		
@@ -96,4 +100,25 @@ public class WorldData {
 		return entities;
 	}
 	
+	public float getTime() {
+		return time;
+	}
+	
+	public void advanceTime(float change) {
+		this.time += change;
+	}
+	
+	public CollisionModel getCollisionModelOfBlock(Vec3i blockInWorld) {
+		ChunkData chunk = getChunkByBlock(blockInWorld);
+		if (chunk == null) return null;
+		
+		Vec3i blockInChunk = Vectors.grab3i();
+		Coordinates.convertInWorldToInChunk(blockInWorld, blockInChunk);
+		BlockData block = chunk.getBlock(blockInChunk);
+		Vectors.release(blockInChunk);
+		
+		if (block == null) return null;
+		return block.getCollisionModel();
+	}
+	
 }
diff --git a/src/main/java/ru/windcorp/progressia/common/world/block/BlockData.java b/src/main/java/ru/windcorp/progressia/common/world/block/BlockData.java
index 5bcbd50..e1ea62d 100644
--- a/src/main/java/ru/windcorp/progressia/common/world/block/BlockData.java
+++ b/src/main/java/ru/windcorp/progressia/common/world/block/BlockData.java
@@ -17,6 +17,8 @@
  *******************************************************************************/
 package ru.windcorp.progressia.common.world.block;
 
+import ru.windcorp.progressia.common.collision.AABB;
+import ru.windcorp.progressia.common.collision.CollisionModel;
 import ru.windcorp.progressia.common.util.Namespaced;
 
 public class BlockData extends Namespaced {
@@ -24,5 +26,9 @@ public class BlockData extends Namespaced {
 	public BlockData(String namespace, String name) {
 		super(namespace, name);
 	}
+	
+	public CollisionModel getCollisionModel() {
+		return AABB.UNIT_CUBE;
+	}
 
 }
diff --git a/src/main/java/ru/windcorp/progressia/test/AABBRenderer.java b/src/main/java/ru/windcorp/progressia/test/AABBRenderer.java
deleted file mode 100644
index e9a6b7b..0000000
--- a/src/main/java/ru/windcorp/progressia/test/AABBRenderer.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package ru.windcorp.progressia.test;
-
-import ru.windcorp.progressia.client.graphics.model.Shape;
-import ru.windcorp.progressia.client.graphics.model.ShapeRenderHelper;
-import ru.windcorp.progressia.client.graphics.model.Shapes;
-import ru.windcorp.progressia.client.graphics.texture.Texture;
-import ru.windcorp.progressia.client.graphics.world.WorldRenderProgram;
-import ru.windcorp.progressia.common.collision.AABB;
-import ru.windcorp.progressia.common.collision.CollisionModel;
-import ru.windcorp.progressia.common.collision.CompoundCollisionModel;
-
-public class AABBRenderer {
-	
-	private static final Shape CUBE = new Shapes.PppBuilder(WorldRenderProgram.getDefault(), (Texture) null).setColorMultiplier(1.0f, 0.7f, 0.2f).create();
-	
-	public static void renderAABB(AABB aabb, ShapeRenderHelper helper) {
-		helper.pushTransform().translate(aabb.getOrigin()).scale(aabb.getSize());
-		CUBE.render(helper);
-		helper.popTransform();
-	}
-	
-	public static void renderAABBsInCompound(
-			CompoundCollisionModel model,
-			ShapeRenderHelper helper
-	) {
-		for (CollisionModel part : model.getModels()) {
-			if (part instanceof CompoundCollisionModel) {
-				renderAABBsInCompound((CompoundCollisionModel) part, helper);
-			} else if (part instanceof AABB) {
-				renderAABB((AABB) part, helper);
-			}
-		}
-	}
-
-}
diff --git a/src/main/java/ru/windcorp/progressia/test/CollisionModelRenderer.java b/src/main/java/ru/windcorp/progressia/test/CollisionModelRenderer.java
new file mode 100644
index 0000000..15463d4
--- /dev/null
+++ b/src/main/java/ru/windcorp/progressia/test/CollisionModelRenderer.java
@@ -0,0 +1,61 @@
+package ru.windcorp.progressia.test;
+
+import glm.mat._4.Mat4;
+import glm.vec._3.Vec3;
+import glm.vec._3.i.Vec3i;
+import ru.windcorp.progressia.client.graphics.model.Shape;
+import ru.windcorp.progressia.client.graphics.model.ShapeRenderHelper;
+import ru.windcorp.progressia.client.graphics.model.Shapes;
+import ru.windcorp.progressia.client.graphics.texture.Texture;
+import ru.windcorp.progressia.client.graphics.world.WorldRenderProgram;
+import ru.windcorp.progressia.common.collision.AABBoid;
+import ru.windcorp.progressia.common.collision.CollisionModel;
+import ru.windcorp.progressia.common.collision.CompoundCollisionModel;
+import ru.windcorp.progressia.common.util.Vectors;
+
+public class CollisionModelRenderer {
+	
+	private static final Shape CUBE = new Shapes.PppBuilder(WorldRenderProgram.getDefault(), (Texture) null).setColorMultiplier(1.0f, 0.7f, 0.2f).create();
+	private static final Shape CUBE_GRAY = new Shapes.PppBuilder(WorldRenderProgram.getDefault(), (Texture) null).setColorMultiplier(0.5f, 0.5f, 0.5f).create();
+	
+	public static void renderCollisionModel(CollisionModel model, ShapeRenderHelper helper) {
+		if (model instanceof AABBoid) {
+			renderAABBoid((AABBoid) model, helper);
+		} else if (model instanceof CompoundCollisionModel) {
+			renderCompound((CompoundCollisionModel) model, helper);
+		} else {
+			// Ignore silently
+		}
+	}
+	
+	private static void renderAABBoid(AABBoid aabb, ShapeRenderHelper helper) {
+		Mat4 mat = helper.pushTransform();
+		Vec3 tmp = Vectors.grab3();
+		
+		aabb.getOrigin(tmp);
+		mat.translate(tmp);
+		aabb.getSize(tmp);
+		mat.scale(tmp);
+		
+		Vectors.release(tmp);
+		
+		CUBE.render(helper);
+		helper.popTransform();
+	}
+	
+	private static void renderCompound(
+			CompoundCollisionModel model,
+			ShapeRenderHelper helper
+	) {
+		for (CollisionModel part : model.getModels()) {
+			renderCollisionModel(part, helper);
+		}
+	}
+	
+	public static void renderBlock(Vec3i coords, ShapeRenderHelper helper) {
+		helper.pushTransform().translate(coords.x, coords.y, coords.z);
+		CUBE_GRAY.render(helper);
+		helper.popTransform();
+	}
+
+}
diff --git a/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java b/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java
new file mode 100755
index 0000000..2ae9828
--- /dev/null
+++ b/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java
@@ -0,0 +1,155 @@
+/*******************************************************************************
+ * Progressia
+ * Copyright (C) 2020  Wind Corporation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ *******************************************************************************/
+package ru.windcorp.progressia.test;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import ru.windcorp.progressia.client.ClientState;
+import ru.windcorp.progressia.client.graphics.font.Font;
+import ru.windcorp.progressia.client.graphics.gui.GUILayer;
+import ru.windcorp.progressia.client.graphics.gui.Label;
+import ru.windcorp.progressia.client.graphics.gui.Panel;
+import ru.windcorp.progressia.client.graphics.gui.layout.LayoutAlign;
+import ru.windcorp.progressia.client.graphics.gui.layout.LayoutVertical;
+
+public class LayerTestGUI extends GUILayer {
+	
+	public LayerTestGUI() {
+		super("LayerTestGui", new LayoutAlign(0, 1, 5));
+		
+		Panel panel = new Panel("ControlDisplays", new LayoutVertical(5));
+		
+		Collection