diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
index 956cbfeb..544e206b 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
@@ -6,6 +6,8 @@ import org.apache.logging.log4j.LogManager
 import org.lwjgl.Version
 import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose
 import ru.dbotthepony.kstarbound.client.StarboundClient
+import ru.dbotthepony.kstarbound.client.render.Animator
+import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
 import ru.dbotthepony.kstarbound.defs.item.DynamicItemDefinition
 import ru.dbotthepony.kstarbound.io.BTreeDB
 import ru.dbotthepony.kstarbound.world.ChunkPos
@@ -189,6 +191,16 @@ fun main() {
 		}
 
 		// println(Starbound.statusEffects["firecharge"])
+
+		starbound.pathStack.push("/animations/dust4")
+		val def = starbound.gson.fromJson(starbound.locate("/animations/dust4/dust4.animation").reader(), AnimationDefinition::class.java)
+		starbound.pathStack.pop()
+
+		val animator = Animator(client.world!!, def)
+
+		client.onPostDrawWorld {
+			animator.render(client.gl.matrixStack)
+		}
 	}
 
 	//ent.position += Vector2d(y = 14.0, x = -10.0)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt
index fb46f871..c4b2aa55 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt
@@ -424,7 +424,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
 	private val entityRenderers = HashMap<Entity, EntityRenderer>()
 
 	override fun onEntityAdded(entity: Entity) {
-		entityRenderers[entity] = EntityRenderer.getRender(state, entity, this)
+		entityRenderers[entity] = EntityRenderer.getRender(world.client, entity, this)
 	}
 
 	override fun onEntityTransferedToThis(entity: Entity, otherChunk: ClientChunk) {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt
index cd28007e..9cab5d68 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt
@@ -18,15 +18,12 @@ import ru.dbotthepony.kstarbound.client.render.Box2DRenderer
 import ru.dbotthepony.kstarbound.client.render.Font
 import ru.dbotthepony.kvector.api.IStruct4f
 import ru.dbotthepony.kvector.matrix.Matrix4fStack
-import ru.dbotthepony.kvector.matrix.nfloat.Matrix4f
 import ru.dbotthepony.kvector.util2d.AABB
 import ru.dbotthepony.kvector.vector.Color
 import java.io.File
-import java.io.FileNotFoundException
 import java.lang.ref.Cleaner
 import java.util.*
 import java.util.concurrent.ThreadFactory
-import java.util.concurrent.atomic.AtomicLong
 import kotlin.collections.ArrayList
 import kotlin.collections.HashMap
 import kotlin.math.roundToInt
@@ -437,61 +434,30 @@ class GLStateTracker(val locator: ISBFileLocator) {
 
 	private val named2DTextures = HashMap<String, GLTexture2D>()
 
-	fun loadNamedTexture(path: String, memoryFormat: Int, fileFormat: Int): GLTexture2D {
-		return named2DTextures.computeIfAbsent(path) {
-			if (!locator.exists(path)) {
-				throw FileNotFoundException("Unable to locate $path")
-			}
-
-			return@computeIfAbsent newTexture(path).upload(locator.readDirect(path), memoryFormat, fileFormat).generateMips()
-		}
-	}
-
-	fun loadNamedTexture(path: String): GLTexture2D {
-		return named2DTextures.computeIfAbsent(path) {
-			if (!locator.exists(path)) {
-				throw FileNotFoundException("Unable to locate $path")
-			}
-
-			return@computeIfAbsent newTexture(path).upload(locator.readDirect(path)).generateMips()
-		}
-	}
-
-	private var loadedEmptyTexture = false
+	private var missingTexture: GLTexture2D? = null
 	private val missingTexturePath = "/assetmissing.png"
 
-	fun loadNamedTextureSafe(path: String, memoryFormat: Int, fileFormat: Int): GLTexture2D {
-		if (!loadedEmptyTexture) {
-			loadedEmptyTexture = true
-			named2DTextures[missingTexturePath] = newTexture(missingTexturePath).upload(locator.readDirect(missingTexturePath), memoryFormat, fileFormat).generateMips()
-		}
+	fun loadTexture(path: String): GLTexture2D {
+		ensureSameThread()
 
-		return named2DTextures.computeIfAbsent(path) {
-			if (!locator.exists(path)) {
-				LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath)
-				return@computeIfAbsent named2DTextures[missingTexturePath]!!
-			}
-
-			return@computeIfAbsent newTexture(path).upload(locator.readDirect(path), memoryFormat, fileFormat).generateMips()
-		}
-	}
-
-	fun loadNamedTextureSafe(path: String): GLTexture2D {
-		if (!loadedEmptyTexture) {
-			loadedEmptyTexture = true
-			named2DTextures[missingTexturePath] = newTexture(missingTexturePath).upload(locator.readDirect(missingTexturePath), GL_RGBA, GL_RGBA).generateMips()
-		}
-
-		return named2DTextures.computeIfAbsent(path) {
-			if (!locator.exists(path)) {
-				LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath)
-				return@computeIfAbsent named2DTextures[missingTexturePath]!!
-			}
-
-			return@computeIfAbsent newTexture(path).upload(locator.readDirect(path)).generateMips().also {
+		if (missingTexture == null) {
+			missingTexture = newTexture(missingTexturePath).upload(locator.readDirect(missingTexturePath), GL_RGBA, GL_RGBA).generateMips().also {
+				it.textureMinFilter = GL_NEAREST
 				it.textureMagFilter = GL_NEAREST
 			}
 		}
+
+		return named2DTextures.computeIfAbsent(path) {
+			if (!locator.exists(path)) {
+				LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath)
+				missingTexture!!
+			} else {
+				newTexture(path).upload(locator.readDirect(path)).generateMips().also {
+					it.textureMinFilter = GL_NEAREST
+					it.textureMagFilter = GL_NEAREST
+				}
+			}
+		}
 	}
 
 	fun bind(obj: VertexBufferObject): VertexBufferObject {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt
index feeba689..4d61acb4 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt
@@ -13,7 +13,7 @@ class TextureLoadingException(message: String) : Throwable(message)
 
 data class UVCoord(val u: Float, val v: Float)
 
-class GLTexturePropertyTracker(private val flag: Int, var value: Int) {
+private class GLTexturePropertyTracker(private val flag: Int, private var value: Int) {
 	operator fun getValue(thisRef: GLTexture2D, property: KProperty<*>): Int {
 		return value
 	}
@@ -62,8 +62,6 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : A
 		return height.toFloat() / width.toFloat()
 	}
 
-	private var mipsWarning = 2
-
 	var textureMinFilter by GLTexturePropertyTracker(GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR)
 	var textureMagFilter by GLTexturePropertyTracker(GL_TEXTURE_MAG_FILTER, GL_LINEAR)
 
@@ -71,15 +69,6 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : A
 	var textureWrapT by GLTexturePropertyTracker(GL_TEXTURE_WRAP_T, GL_REPEAT)
 
 	fun bind(): GLTexture2D {
-		if (textureMinFilter != GL_LINEAR && textureMinFilter != GL_NEAREST) {
-			if (mipsWarning == 1) {
-				LOGGER.warn("(Likely) Trying to use texture {} before generated it's mips, this probably won't work!", this)
-				mipsWarning = 0
-			} else if (mipsWarning == 2) {
-				mipsWarning = 1
-			}
-		}
-
 		state.texture2D = this
 		return this
 	}
@@ -87,8 +76,7 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : A
 	fun generateMips(): GLTexture2D {
 		state.ensureSameThread()
 		glGenerateTextureMipmap(pointer)
-		checkForGLError()
-		mipsWarning = 0
+		checkForGLError("Generating texture mipmaps")
 		return this
 	}
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Animator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Animator.kt
index e4566f84..d13a14dd 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Animator.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Animator.kt
@@ -1,24 +1,51 @@
 package ru.dbotthepony.kstarbound.client.render
 
+import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
 import ru.dbotthepony.kstarbound.client.ClientWorld
 import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
 import ru.dbotthepony.kvector.matrix.Matrix4fStack
+import ru.dbotthepony.kvector.vector.nfloat.Vector3f
 
 class Animator(
 	val world: ClientWorld,
-	val def: AnimationDefinition
+	val def: AnimationDefinition,
+	val renderParams: ((String) -> String?)? = null
 ) {
+	inline val state get() = world.client.gl
 	val frameAnimator: FrameAnimator?
+	val mainSprite: RebindableSprite?
 
 	init {
 		if (def.frames != null && def.animationCycle != null && def.frameNumber != null) {
 			frameAnimator = FrameAnimator(lastFrame = def.frameNumber - 1, time = world.client.time, animationCycle = def.animationCycle)
+			mainSprite = RebindableSprite(world.client, def.frames, ::getRenderParam)
 		} else {
 			frameAnimator = null
+			mainSprite = null
 		}
 	}
 
-	fun render(stack: Matrix4fStack) {
+	private fun getRenderParam(name: String): String? {
+		if (name == "frame") {
+			return frameAnimator?.frameString
+		}
 
+		return renderParams?.invoke(name)
+	}
+
+	fun render(stack: Matrix4fStack) {
+		frameAnimator?.advance()
+		val sprite = mainSprite?.update() ?: return
+		sprite.texture.bind()
+
+		stack.push().translateWithMultiplication(Vector3f(world.client.camera.pos))
+
+		state.programs.textured.use()
+		state.programs.textured.transform = stack.last
+		state.activeTexture = 0
+		state.programs.textured.texture = 0
+
+		state.flat2DTexturedQuads.singleSprite(sprite.width / PIXELS_IN_STARBOUND_UNITf, sprite.height / PIXELS_IN_STARBOUND_UNITf, 0.0, sprite.transformer)
+		stack.pop()
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/BoundSprite.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/BoundSprite.kt
index 37eb0a37..a846eb2a 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/BoundSprite.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/BoundSprite.kt
@@ -17,12 +17,12 @@ class BoundSprite(
 	/**
 	 * Настоящая ширина спрайта, в пикселях
 	 */
-	inline val width get() = sprite.width(texture.width)
+	val width = sprite.width(texture.width)
 
 	/**
 	 * Настоящая высота спрайта, в пикселях
 	 */
-	inline val height get() = sprite.height(texture.height)
+	val height = sprite.height(texture.height)
 
 	override val u0: Float
 	override val v0: Float
@@ -41,24 +41,3 @@ class BoundSprite(
 	fun bind() = texture.bind()
 	val transformer = QuadTransformers.uv(u0, v0, u1, v1)
 }
-
-/**
- * Создаёт связку текстуры-атласа + координгат спрайта на ней
- */
-fun ImageReference.bind(texture: GLTexture2D): BoundSprite {
-	return BoundSprite(sprite!!, texture)
-}
-
-/**
- * Создаёт связку текстуры-атласа, которая загружается через [GLStateTracker.loadNamedTextureSafe] + координгат спрайта на ней
- */
-fun ImageReference.bind(state: GLStateTracker): BoundSprite {
-	return BoundSprite(sprite!!, state.loadNamedTextureSafe(imagePath.value!!))
-}
-
-/**
- * Создаёт связку текстуры-атласа + координгат спрайта на ней
- */
-fun AtlasConfiguration.Sprite.bind(texture: GLTexture2D): BoundSprite {
-	return BoundSprite(this, texture)
-}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/FrameAnimator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/FrameAnimator.kt
index d73c9e3e..6598d422 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/FrameAnimator.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/FrameAnimator.kt
@@ -32,6 +32,13 @@ class FrameAnimator(
 	var frame = 0
 		private set
 
+	/**
+	 * Эффективное преобразование [frame] в строку
+	 */
+	val frameString: String get() {
+		return framenames.getOrNull(frame) ?: frame.toString()
+	}
+
 	private var counter = 0.0
 	private var lastRender = time.seconds
 
@@ -73,4 +80,14 @@ class FrameAnimator(
 			lastRender = time.seconds
 		}
 	}
+
+	companion object {
+		private val framenames = ArrayList<String>()
+
+		init {
+			for (i in 0 .. 500) {
+				framenames.add(i.toString())
+			}
+		}
+	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RebindableSprite.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RebindableSprite.kt
new file mode 100644
index 00000000..41b63aac
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RebindableSprite.kt
@@ -0,0 +1,50 @@
+package ru.dbotthepony.kstarbound.client.render
+
+import ru.dbotthepony.kstarbound.client.StarboundClient
+import ru.dbotthepony.kstarbound.defs.image.ImageReference
+
+class RebindableSprite(
+	val client: StarboundClient,
+	ref: ImageReference,
+	val renderParams: ((String) -> String?)? = null
+) {
+	var sprite: BoundSprite? = null
+		private set
+
+	var ref: ImageReference = ref
+		private set
+
+	init {
+		val unbound = ref.sprite
+
+		if (unbound != null) {
+			sprite = BoundSprite(unbound, client.gl.loadTexture(ref.imagePath.value!!))
+		}
+	}
+
+	/**
+	 * Обновляет [ref] и [sprite] по значениям, которые выдаст [renderParams]
+	 *
+	 * Возвращает новое значение [sprite]
+	 */
+	fun update(): BoundSprite? {
+		client.gl.ensureSameThread()
+
+		if (renderParams == null)
+			return sprite
+
+		val newRef = ref.with(renderParams)
+
+		if (newRef !== ref) {
+			ref = newRef
+			sprite = null
+			val unbound = newRef.sprite
+
+			if (unbound != null) {
+				sprite = BoundSprite(unbound, client.gl.loadTexture(newRef.imagePath.value!!))
+			}
+		}
+
+		return sprite
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt
index 34045438..984fade4 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt
@@ -186,7 +186,7 @@ private class ModifierEqualityTester(val definition: MaterialModifier) : Equalit
 
 class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) {
 	val state get() = renderers.state
-	val texture = state.loadNamedTexture(def.renderParameters.texture.imagePath.value!!).also {
+	val texture = state.loadTexture(def.renderParameters.texture.imagePath.value!!).also {
 		it.textureMagFilter = GL_NEAREST
 	}
 
@@ -255,9 +255,9 @@ class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) {
 			for (renderPiece in matchPiece.pieces) {
 				if (renderPiece.piece.texture != null) {
 					val program = if (background) {
-						renderers.background(state.loadNamedTexture(renderPiece.piece.texture!!))
+						renderers.background(state.loadTexture(renderPiece.piece.texture!!))
 					} else {
-						renderers.foreground(state.loadNamedTexture(renderPiece.piece.texture!!))
+						renderers.foreground(state.loadTexture(renderPiece.piece.texture!!))
 					}
 
 					tesselateAt(self, renderPiece.piece, getter, layers.computeIfAbsent(program, def.renderParameters.zLevel, ::vertexTextureBuilder), pos, renderPiece.offset, isModifier)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/EntityRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/EntityRenderer.kt
index 313a2c92..843ed8b9 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/EntityRenderer.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/EntityRenderer.kt
@@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.client.render.entity
 
 import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap
 import ru.dbotthepony.kstarbound.client.ClientChunk
+import ru.dbotthepony.kstarbound.client.StarboundClient
 import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
 import ru.dbotthepony.kstarbound.world.entities.Entity
 import ru.dbotthepony.kvector.matrix.Matrix4fStack
@@ -13,7 +14,8 @@ import java.io.Closeable
  *
  * Считается, что процесс отрисовки ограничен лишь одним слоем (т.е. отрисовка происходит в один проход)
  */
-open class EntityRenderer(val state: GLStateTracker, val entity: Entity, open var chunk: ClientChunk?) : Closeable {
+open class EntityRenderer(val client: StarboundClient, val entity: Entity, open var chunk: ClientChunk?) : Closeable {
+	inline val state: GLStateTracker get() = client.gl
 	open val renderPos: Vector2d get() = entity.position
 
 	open fun render(stack: Matrix4fStack) {
@@ -39,20 +41,20 @@ open class EntityRenderer(val state: GLStateTracker, val entity: Entity, open va
 		 */
 		const val Z_LEVEL_ENTITIES = 30000
 
-		private val renderers = Reference2ObjectOpenHashMap<Class<*>, (state: GLStateTracker, entity: Entity, chunk: ClientChunk?) -> EntityRenderer>()
+		private val renderers = Reference2ObjectOpenHashMap<Class<*>, (client: StarboundClient, entity: Entity, chunk: ClientChunk?) -> EntityRenderer>()
 
 		@Suppress("unchecked_cast")
-		fun <T : Entity> registerRenderer(clazz: Class<T>, renderer: (state: GLStateTracker, entity: T, chunk: ClientChunk?) -> EntityRenderer) {
-			check(renderers.put(clazz, renderer as (state: GLStateTracker, entity: Entity, chunk: ClientChunk?) -> EntityRenderer) == null) { "Already has renderer for ${clazz.canonicalName}!" }
+		fun <T : Entity> registerRenderer(clazz: Class<T>, renderer: (client: StarboundClient, entity: T, chunk: ClientChunk?) -> EntityRenderer) {
+			check(renderers.put(clazz, renderer as (client: StarboundClient, entity: Entity, chunk: ClientChunk?) -> EntityRenderer) == null) { "Already has renderer for ${clazz.canonicalName}!" }
 		}
 
-		inline fun <reified T : Entity> registerRenderer(noinline renderer: (state: GLStateTracker, entity: T, chunk: ClientChunk?) -> EntityRenderer) {
+		inline fun <reified T : Entity> registerRenderer(noinline renderer: (client: StarboundClient, entity: T, chunk: ClientChunk?) -> EntityRenderer) {
 			registerRenderer(T::class.java, renderer)
 		}
 
-		fun getRender(state: GLStateTracker, entity: Entity, chunk: ClientChunk? = null): EntityRenderer {
-			val factory = renderers[entity::class.java] ?: return EntityRenderer(state, entity, chunk)
-			return factory.invoke(state, entity, chunk)
+		fun getRender(client: StarboundClient, entity: Entity, chunk: ClientChunk? = null): EntityRenderer {
+			val factory = renderers[entity::class.java] ?: return EntityRenderer(client, entity, chunk)
+			return factory.invoke(client, entity, chunk)
 		}
 
 		init {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt
index fd844ca4..bcff3f91 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/entity/ItemRenderer.kt
@@ -2,14 +2,14 @@ package ru.dbotthepony.kstarbound.client.render.entity
 
 import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
 import ru.dbotthepony.kstarbound.client.ClientChunk
-import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
-import ru.dbotthepony.kstarbound.client.render.bind
+import ru.dbotthepony.kstarbound.client.StarboundClient
+import ru.dbotthepony.kstarbound.client.render.RebindableSprite
 import ru.dbotthepony.kstarbound.world.entities.ItemEntity
 import ru.dbotthepony.kvector.matrix.Matrix4fStack
 
-class ItemRenderer(state: GLStateTracker, entity: ItemEntity, chunk: ClientChunk?) : EntityRenderer(state, entity, chunk) {
+class ItemRenderer(client: StarboundClient, entity: ItemEntity, chunk: ClientChunk?) : EntityRenderer(client, entity, chunk) {
 	private val def = entity.def
-	private val textures = def.inventoryIcon?.stream()?.map { it.image.bind(state) }?.toList() ?: listOf()
+	private val textures = def.inventoryIcon?.stream()?.map { RebindableSprite(client, it.image) }?.toList() ?: listOf()
 
 	override fun render(stack: Matrix4fStack) {
 		if (textures.isEmpty())
@@ -20,10 +20,10 @@ class ItemRenderer(state: GLStateTracker, entity: ItemEntity, chunk: ClientChunk
 		state.activeTexture = 0
 		state.programs.textured.texture = 0
 
-		for (texture in textures) {
-			texture.bind()
-
-			state.flat2DTexturedQuads.singleSprite(texture.width / PIXELS_IN_STARBOUND_UNITf, texture.height / PIXELS_IN_STARBOUND_UNITf, entity.movement.angle, texture.transformer)
+		for (unbound in textures) {
+			val sprite = unbound.update() ?: continue
+			sprite.texture.bind()
+			state.flat2DTexturedQuads.singleSprite(sprite.width / PIXELS_IN_STARBOUND_UNITf, sprite.height / PIXELS_IN_STARBOUND_UNITf, entity.movement.angle, sprite.transformer)
 		}
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/AtlasConfiguration.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/AtlasConfiguration.kt
index 2bc161ab..bbe43be5 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/AtlasConfiguration.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/AtlasConfiguration.kt
@@ -58,16 +58,25 @@ class AtlasConfiguration private constructor(
 		}
 	}
 
+	/**
+	 * @return [Sprite] если он существует с данным [name], или `null`
+	 */
 	operator fun get(name: String): Sprite? {
 		return sprites[name]
 	}
 
+	/**
+	 * @return [Sprite] если он существует с данным [name], или `null`
+	 */
 	operator fun get(name: Int): Sprite? {
 		return intIndexed[name]
 	}
 
+	/**
+	 * @return [Sprite] если он существует с данным [name], или первый спрайт в атласе
+	 */
 	fun any(name: String): Sprite {
-		return get(name) ?: first
+		return get(name) ?: any()
 	}
 
 	private val any by lazy { get("root") ?: get("0") ?: get("default") ?: first }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/ImageReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/ImageReference.kt
index 193e1851..110ea88a 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/ImageReference.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/ImageReference.kt
@@ -13,7 +13,7 @@ import ru.dbotthepony.kstarbound.util.PathStack
 import ru.dbotthepony.kstarbound.util.SBPattern
 
 /**
- * @see [AtlasConfiguration.Companion.get]
+ * @see [AtlasConfiguration.Registry.get]
  */
 class ImageReference private constructor(
 	val raw: DirectAssetReference,
@@ -26,8 +26,9 @@ class ImageReference private constructor(
 	 * Спрайт, на которое ссылается данный референс, или `null` если:
 	 * * [atlas] равен `null`
 	 * * [spritePath] является шаблоном и определены не все значения
+	 * * [spritePath] не является правильным именем спрайта внутри [atlas] (смотрим [AtlasConfiguration.get])
 	 */
-	val sprite by lazy {
+	val sprite by lazy(LazyThreadSafetyMode.NONE) {
 		if (atlas == null)
 			null
 		else if (spritePath == null)
@@ -38,7 +39,15 @@ class ImageReference private constructor(
 
 	fun with(values: (String) -> String?): ImageReference {
 		val imagePath = this.imagePath.with(values)
-		val spritePath = this.spritePath?.with(values)
+		var spritePath = this.spritePath?.with(values)
+
+		if (spritePath == null) {
+			val frame = values.invoke("frame")
+
+			if (frame != null) {
+				spritePath = SBPattern.FRAME.with(values)
+			}
+		}
 
 		if (imagePath != this.imagePath || spritePath != this.spritePath) {
 			if (imagePath != this.imagePath) {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/SBPattern.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/SBPattern.kt
index 1bee7005..59ce1329 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/SBPattern.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/SBPattern.kt
@@ -52,8 +52,11 @@ class SBPattern private constructor(
 	}
 
 	fun resolve(values: (String) -> String?): String? {
-		if (names.isEmpty())
+		if (names.isEmpty()) {
 			return raw
+		} else if (pieces.size == 1) {
+			return pieces[0].resolve(values, params::get)
+		}
 
 		val buffer = ArrayList<String>(pieces.size)
 
@@ -116,6 +119,9 @@ class SBPattern private constructor(
 		@JvmField
 		val EMPTY = raw("")
 
+		@JvmField
+		val FRAME = of("<frame>")
+
 		override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
 			if (type.rawType == SBPattern::class.java) {
 				return object : TypeAdapter<SBPattern>() {
@@ -126,7 +132,7 @@ class SBPattern private constructor(
 					}
 
 					override fun read(`in`: JsonReader): SBPattern? {
-						return interner.intern(of(strings.read(`in`) ?: return null))
+						return of(strings.read(`in`) ?: return null)
 					}
 				} as TypeAdapter<T>
 			}
@@ -159,7 +165,7 @@ class SBPattern private constructor(
 						throw IllegalArgumentException("Malformed pattern string: $raw")
 					}
 
-					pieces.add(Piece(name = raw.substring(open + 1, closing - 1)))
+					pieces.add(Piece(name = raw.substring(open + 1, closing)))
 					i = closing + 1
 				}
 			}