From ec9c3a71f96138d17c9e3cbb03faf7dc29cf14f0 Mon Sep 17 00:00:00 2001
From: MaxED <j.maxed@gmail.com>
Date: Mon, 11 Jul 2016 22:13:43 +0000
Subject: [PATCH] Added: preview sprites are now generated from voxels. Those
 are used as previews and when model rendering is disabled. Fixed: voxel
 support logic was outdated, which in some cases resulted in voxels not being
 loaded and in some weird behaviour when trying to change pitch/roll of
 associated things. Updated sprites lookup logic. Now it correctly handles
 sprites named like NNNNA0B0. Fixed: in some cases DECORATE parser was unable
 to correctly detect sprite name & frame(s) block. Updated ZDoom_ACS.cfg.
 Updated ZDoom_DECORATE.cfg.

---
 Build/Configurations/Includes/ZDoom_misc.cfg  |   4 +-
 Build/Scripting/ZDoom_ACS.cfg                 |   2 +-
 Build/Scripting/ZDoom_DECORATE.cfg            |  16 +-
 Source/Core/Builder.csproj                    |   3 +-
 Source/Core/Config/ThingTypeInfo.cs           | 133 +++++--
 Source/Core/Data/DataManager.cs               | 209 ++++++-----
 Source/Core/Data/DataReader.cs                |   8 +-
 Source/Core/Data/DirectoryReader.cs           |   5 +-
 Source/Core/Data/PK3Reader.cs                 |   5 +-
 Source/Core/Data/PK3StructuredReader.cs       |  27 +-
 Source/Core/Data/SpriteImage.cs               |  10 +-
 Source/Core/Data/VoxelImage.cs                | 325 ++++++++++++++++++
 Source/Core/Data/WADReader.cs                 |  39 ++-
 Source/Core/GZBuilder/Data/ModelData.cs       |   3 +-
 Source/Core/GZBuilder/md3/ModelReader.cs      |   3 +-
 Source/Core/Map/Thing.cs                      |  18 +-
 .../{Config => Rendering}/RenderModeEnums.cs  |   3 +-
 Source/Core/Rendering/Renderer2D.cs           |   8 +-
 Source/Core/Rendering/Renderer3D.cs           |  14 +-
 Source/Core/VisualModes/VisualThing.cs        |   5 +-
 Source/Core/ZDoom/ActorStructure.cs           |  90 +----
 Source/Core/ZDoom/StateStructure.cs           |  53 ++-
 Source/Core/ZDoom/VoxeldefParser.cs           |   7 +-
 .../VisualModes/BaseVisualGeometrySidedef.cs  |   5 +-
 .../VisualModes/BaseVisualThing.cs            |   2 +-
 25 files changed, 720 insertions(+), 277 deletions(-)
 create mode 100644 Source/Core/Data/VoxelImage.cs
 rename Source/Core/{Config => Rendering}/RenderModeEnums.cs (75%)

diff --git a/Build/Configurations/Includes/ZDoom_misc.cfg b/Build/Configurations/Includes/ZDoom_misc.cfg
index e686f4424..676295848 100644
--- a/Build/Configurations/Includes/ZDoom_misc.cfg
+++ b/Build/Configurations/Includes/ZDoom_misc.cfg
@@ -1426,14 +1426,14 @@ enums_strife
 		1 = "Base Key (Front)";
 		2 = "Governor's Key";
 		3 = "Travel Passcard";
-		4 = "Blue ID Badge";
+		4 = "ID Badge";
 		5 = "Prison Key";
 		6 = "Severed Hand";
 		7 = "Power Key 1";
 		8 = "Power Key 2";
 		9 = "Power Key 3";
 		10 = "Gold Key";
-		11 = "Gold ID Badge";
+		11 = "ID Card";
 		12 = "Silver Key";
 		13 = "Oracle Key";
 		14 = "Military ID";
diff --git a/Build/Scripting/ZDoom_ACS.cfg b/Build/Scripting/ZDoom_ACS.cfg
index 5b008f76a..d808289b0 100644
--- a/Build/Scripting/ZDoom_ACS.cfg
+++ b/Build/Scripting/ZDoom_ACS.cfg
@@ -310,7 +310,7 @@ keywords
 	PolyWait = "void PolyWait(int polyid)";
 	Print = "void Print(type:expression)\nPrint will print something to the screen.\nPrint will only display for the activator of the script\nFor printing to all players, use PrintBold.";
 	PrintBold = "void PrintBold(type:expression)\nThis is exactly the same as Print, except all players will see the printed text\non the screen instead of just the activator of the script.";
-	QuakeEx = "bool QuakeEx(int tid, int intensityX, int intensityY, int intensityZ, int duration, int damrad, int tremrad, str sound[, int flags = 0[, float mulwavex = 1.0[, float mulwavey = 1.0[, float mulwavez = 1.0[, int falloff = 0[, int highpoint = 0]]]]]])";
+	QuakeEx = "bool QuakeEx(int tid, int intensityX, int intensityY, int intensityZ, int duration, int damrad, int tremrad, str sound[, int flags = 0[, float mulwavex = 1.0[, float mulwavey = 1.0[, float mulwavez = 1.0[, int falloff = 0[, int highpoint = 0[, float rollintensity = 0.0[, float rollwave = 0.0]]]]]]]])";
 	Radius_Quake = "Radius_Quake(intensity, duration, damrad, tremrad, tid)";
 	Radius_Quake2 = "void Radius_Quake2(int tid, int intensity, int duration, int damrad, int tremrad, str sound)";
 	Random = "int Random(int min, int max)";
diff --git a/Build/Scripting/ZDoom_DECORATE.cfg b/Build/Scripting/ZDoom_DECORATE.cfg
index 23efb9d1d..afc4c4428 100644
--- a/Build/Scripting/ZDoom_DECORATE.cfg
+++ b/Build/Scripting/ZDoom_DECORATE.cfg
@@ -128,6 +128,7 @@ keywords
 	A_BulletAttack = "A_BulletAttack";
 	A_MonsterRail = "A_MonsterRail";
 	A_Explode = "A_Explode[(int explosiondamage = 128[, int explosionradius = 128[, int flags = XF_HURTSOURCE[, bool alert = false[, int fulldamageradius = 0[, int nails = 0[, int naildamage = 10[, str pufftype = \"BulletPuff\"]]]]]]])]";
+	A_RadiusDamageSelf = "[(int damage = 128[, float distance = 128.0[, int flags = 0[, str flashtype = \"None\"]]])]\nflags: RDSF flags";
 	A_RadiusThrust = "A_RadiusThrust(int force, int distance[, int flags[, int fullthrustdistance]])";
 	A_Detonate = "A_Detonate";
 	A_ThrowGrenade = "bool A_ThrowGrenade(str spawntype[, float spawnheight[, float throwspeed_horz[, float throwspeed_vert[, bool useammo]]]])";
@@ -176,7 +177,7 @@ keywords
 	A_SpawnDebris = "A_SpawnDebris(str type[, bool translation = false[, float horizontal_vel = 1.0[, float vertical_vel = 1.0]]])";
 	A_SpawnItem = "bool A_SpawnItem(str type, int distance, float zpos, bool useammo, bool translation)";
 	A_SpawnItemEx = "bool A_SpawnItemEx(str type[, float xoffset = 0.0[, float yoffset = 0.0[, float zoffset = 0.0[, float xvelocity = 0.0[, float yvelocity = 0.0[, float zvelocity = 0.0[, float angle = 0.0[, int flags = 0[, int skipchance = 0[, int tid = 0]]]]]]]]]])";
-	A_SpawnParticle = "A_SpawnParticle(color color[, int flags = 0[, int lifetime = 35[, int size = 1[, float angle = 0.0[, float xoff = 0.0[, float yoff = 0.0[, float zoff = 0.0[, float velx = 0.0[, float vely = 0.0[, float velz = 0.0[, float accelx = 0.0[, float accely = 0.0[, float accelz = 0.0[, float startalpha = 1.0[, float fadestep = -1.0]]]]]]]]]]]]]]])";
+	A_SpawnParticle = "A_SpawnParticle(color color[, int flags = 0[, int lifetime = 35[, float size = 1.0[, float angle = 0.0[, float xoff = 0.0[, float yoff = 0.0[, float zoff = 0.0[, float velx = 0.0[, float vely = 0.0[, float velz = 0.0[, float accelx = 0.0[, float accely = 0.0[, float accelz = 0.0[, float startalpha = 1.0[, float fadestep = -1.0[, float sizestep = 0.0]]]]]]]]]]]]]]]])";
 //State jumps
 	A_CheckBlock = "state A_CheckBlock(str block[, int flags = 0[, int pointer = AAPTR_TARGET[, float xoff = 0.0[, float yoff = 0.0[, float zoff = 0.0[, float angle = 0.0]]]]]])";
 	A_CheckCeiling = "state A_CheckCeiling(str state)\nstate A_CheckCeiling(int offset)";
@@ -307,9 +308,9 @@ keywords
 	A_Light1 = "A_Light1";
 	A_Light2 = "A_Light2";
 	A_LightInverse = "A_LightInverse";
-	A_Overlay = "bool A_Overlay(int layer[, state start[, bool nooverride]])";
+	A_Overlay = "bool A_Overlay(int layer[, state start = \"\"[, bool nooverride = false]])";
 	A_OverlayFlags = "A_OverlayFlags(int layer, int flags, bool set)\nflags: PSPF flags.";
-	A_OverlayOffset = "A_OverlayOffset[(int layer = 0[, float x = 0.0f[, float y = 32.0f[, int flags = 0]]]])]\nflags: WOF flags.";
+	A_OverlayOffset = "A_OverlayOffset[(int layer = 0[, float x = 0.0[, float y = 32.0[, int flags = 0]]]])]\nflags: WOF flags.";
 	A_Recoil = "A_Recoil(float force)";
 	A_ZoomFactor = "A_ZoomFactor[(float zoom = 1.0[, int flags = 0])]\nflags: ZOOM flags.";
 	A_SetCrosshair = "A_SetCrosshair(int number)";
@@ -378,7 +379,7 @@ keywords
 	A_SkelWhoosh = "A_SkelWhoosh";
 	A_StartFire = "A_StartFire";
 	A_FireCrackle = "A_FireCrackle";
-	A_BFGSpray = "A_BFGSpray[(str flashtype = \"BFGExtra\"[, int numrays = 40[, int damagecnt = 15[, float angle = 90.0[, float distance = 1024.0[, float vrange = 32.0[, int explicit_damage = 0]]]]]])]";
+	A_BFGSpray = "A_BFGSpray[(str flashtype = \"BFGExtra\"[, int numrays = 40[, int damagecnt = 15[, float angle = 90.0[, float distance = 1024.0[, float vrange = 32.0[, int explicit_damage = 0[, int flags = 0]]]]]]])]\nflags: BFGR flags.";
 	A_BarrelDestroy = "A_BarrelDestroy";
 //Miscellaneous functions not listed in the "Action functions" wiki article
 	A_Bang4Cloud = "A_Bang4Cloud";
@@ -1368,5 +1369,10 @@ constants
 	PSPF_ADDWEAPON;
 	PSPF_ADDBOB;
 	PSPF_POWDOUBLE;
-	PSPF_CVARFAST;	
+	PSPF_CVARFAST;
+//A_BFGSpray flags
+	BFGF_MISSILEORIGIN;
+	BFGF_HURTSOURCE;
+//A_RadiusDamageSelf flags
+	RDSF_BFGDAMAGE;
 }
diff --git a/Source/Core/Builder.csproj b/Source/Core/Builder.csproj
index 23db2d52e..6012955cb 100644
--- a/Source/Core/Builder.csproj
+++ b/Source/Core/Builder.csproj
@@ -705,7 +705,7 @@
     <Compile Include="Actions\HintsManager.cs" />
     <Compile Include="Config\AllTexturesSet.cs" />
     <Compile Include="Config\FlagTranslation.cs" />
-    <Compile Include="Config\RenderModeEnums.cs" />
+    <Compile Include="Rendering\RenderModeEnums.cs" />
     <Compile Include="Config\PasteOptions.cs" />
     <Compile Include="Config\ScriptDocumentSettings.cs" />
     <Compile Include="Config\SectorEffectData.cs" />
@@ -825,6 +825,7 @@
     <Compile Include="Data\PK3FileImage.cs" />
     <Compile Include="Data\PK3StructuredReader.cs" />
     <Compile Include="Data\DynamicBitmapImage.cs" />
+    <Compile Include="Data\VoxelImage.cs" />
     <Compile Include="Editing\CustomThingsFilter.cs" />
     <Compile Include="General\CRC.cs" />
     <Compile Include="General\ErrorItem.cs" />
diff --git a/Source/Core/Config/ThingTypeInfo.cs b/Source/Core/Config/ThingTypeInfo.cs
index 1a313903d..6bba25ad2 100644
--- a/Source/Core/Config/ThingTypeInfo.cs
+++ b/Source/Core/Config/ThingTypeInfo.cs
@@ -24,6 +24,7 @@ using CodeImp.DoomBuilder.Data;
 using CodeImp.DoomBuilder.GZBuilder.Data;
 using CodeImp.DoomBuilder.IO;
 using CodeImp.DoomBuilder.Map;
+using CodeImp.DoomBuilder.Rendering;
 using CodeImp.DoomBuilder.ZDoom;
 
 #endregion
@@ -459,9 +460,8 @@ namespace CodeImp.DoomBuilder.Config
 			
 			// Set sprite
 			StateStructure.FrameInfo info = actor.FindSuitableSprite(); //mxd
-			string suitablesprite = (locksprite ? string.Empty : info.Sprite); //mxd
-			if(!string.IsNullOrEmpty(suitablesprite)) 
-				sprite = suitablesprite;
+			if(!locksprite && !string.IsNullOrEmpty(info.Sprite)) //mxd. Added locksprite property
+				sprite = info.Sprite;
 			else if(string.IsNullOrEmpty(sprite))//mxd
 				sprite = DataManager.INTERNAL_PREFIX + "unknownthing";
 
@@ -548,42 +548,121 @@ namespace CodeImp.DoomBuilder.Config
 			if(blocking > THING_BLOCKING_NONE) errorcheck = THING_ERROR_INSIDE_STUCK;
 		}
 
-		//mxd. This tries to find all possible sprite rotations
-		internal void SetupSpriteFrame()
+		//mxd. This tries to find all possible sprite rotations. Returns true when voxel substitute exists
+		internal bool SetupSpriteFrame(HashSet<string> allspritenames, HashSet<string> allvoxelnames)
 		{
-			// Empty or internal sprites don't have rotations
-			if(string.IsNullOrEmpty(sprite) || sprite.StartsWith(DataManager.INTERNAL_PREFIX)) return;
+			// Empty, invalid or internal sprites don't have rotations
+			// Info: we can have either partial 5-char sprite name from DECORATE parser,
+			// or fully defined 6/8-char sprite name defined in Game configuration or by $Sprite property 
+			if(string.IsNullOrEmpty(sprite) || sprite.StartsWith(DataManager.INTERNAL_PREFIX) 
+				|| (sprite.Length != 5 && sprite.Length != 6 && sprite.Length != 8)) return false;
 
-			// Skip sprites with strange names
-			if(sprite.Length != 6 && sprite.Length != 8) return;
+			string sourcename = sprite.Substring(0, 4);
+			char   sourceframe = sprite[4];
+
+			// First try voxels
+			if(allvoxelnames.Count > 0)
+			{
+				// Find a voxel, which matches sourcename
+				HashSet<string> voxelnames = new HashSet<string>();
+				foreach(string s in allvoxelnames)
+				{
+					if(s.StartsWith(sourcename)) voxelnames.Add(s);
+				}
+
+				// Find a voxel, which matches baseframe
+				// Valid voxel can be either 4-char (POSS), 5-char (POSSA) or 6-char (POSSA0)
+				string newsprite = string.Empty;
+
+				// Check 6-char voxels...
+				foreach(string v in voxelnames)
+				{
+					if(v.Length == 6 && v.StartsWith(sourcename + sourceframe) && WADReader.IsValidSpriteName(v))
+					{
+						newsprite = v;
+						break;
+					}
+				}
+
+				// Check 5-char voxels...
+				if(voxelnames.Contains(sourcename + sourceframe)) newsprite = sourcename + sourceframe;
+
+				// Check 4-char voxels...
+				if(voxelnames.Contains(sourcename)) newsprite = sourcename;
+
+				// Voxel found?
+				if(!string.IsNullOrEmpty(newsprite))
+				{
+					// Assign new sprite
+					sprite = newsprite;
+
+					// Recreate sprite frame
+					spriteframe = new[] { new SpriteFrameInfo { Sprite = sprite, SpriteLongName = Lump.MakeLongName(sprite, true) } };
+
+					// Substitute voxel found
+					return true;
+				}
+			}
+
+			// Then try sprites
+			// Find a sprite, which matches sourcename
+			string sourcesprite = string.Empty;
+			HashSet<string> spritenames = new HashSet<string>();
+			foreach(string s in allspritenames)
+			{
+				if(s.StartsWith(sourcename)) spritenames.Add(s);
+			}
+
+			// Find a sprite, which matches baseframe
+			foreach(string s in spritenames)
+			{
+				if(s[4] == sourceframe || (s.Length == 8 && s[6] == sourceframe))
+				{
+					sourcesprite = s;
+					break;
+				}
+			}
+
+			// Abort if no sprite was found
+			if(string.IsNullOrEmpty(sourcesprite)) return false;
 
 			// Get sprite angle
-			string anglestr = sprite.Substring(5, 1);
+			string anglestr = sourcesprite.Substring(5, 1);
 			int sourceangle;
 			if(!int.TryParse(anglestr, NumberStyles.Integer, CultureInfo.InvariantCulture, out sourceangle))
 			{
-				General.ErrorLogger.Add(ErrorType.Error, "Error in actor \"" + title + "\":" + index + ". Unable to get sprite angle from sprite \"" + sprite + "\"");
-				return;
+				General.ErrorLogger.Add(ErrorType.Error, "Error in actor \"" + title + "\":" + index + ". Unable to get sprite angle from sprite \"" + sourcesprite + "\"");
+				return false;
 			}
 
 			if(sourceangle < 0 || sourceangle > 8)
 			{
-				General.ErrorLogger.Add(ErrorType.Error, "Error in actor \"" + title + "\":" + index + ", sprite \"" + sprite + "\". Sprite angle must be in [0..8] range");
-				return;
+				General.ErrorLogger.Add(ErrorType.Error, "Error in actor \"" + title + "\":" + index + ", sprite \"" + sourcesprite + "\". Sprite angle must be in [0..8] range");
+				return false;
 			}
 
-			// No rotations?
-			if(sourceangle == 0) return;
+			// No rotations? Then spriteframe is already setup
+			if(sourceangle == 0)
+			{
+				// Sprite name still incomplete?
+				if(sprite.Length < 6)
+				{
+					sprite = sourcesprite;
+
+					// Recreate sprite frame. Mirror the sprite if sourceframe matches the second frame block
+					spriteframe = new[] { new SpriteFrameInfo { Sprite = sprite, SpriteLongName = Lump.MakeLongName(sprite, true), 
+																Mirror = (sprite.Length == 8 && sprite[6] == sourceframe) } };
+				}
+				
+				return false;
+			}
 			
 			// Gather rotations
 			string[] frames = new string[8];
 			bool[] mirror = new bool[8];
 			int processedcount = 0;
-			string sourcename = sprite.Substring(0, 4);
-			IEnumerable<string> spritenames = General.Map.Data.GetSpriteNames(sourcename);
 
 			// Process gathered sprites
-			char sourceframe = sprite[4];
 			foreach(string s in spritenames)
 			{
 				// Check first frame block
@@ -596,7 +675,7 @@ namespace CodeImp.DoomBuilder.Config
 					if(!int.TryParse(anglestr, NumberStyles.Integer, CultureInfo.InvariantCulture, out targetangle))
 					{
 						General.ErrorLogger.Add(ErrorType.Error, "Error in actor \"" + title + "\":" + index + ". Unable to get sprite angle from sprite \"" + s + "\"");
-						return;
+						return false;
 					}
 
 					// Sanity checks
@@ -610,7 +689,7 @@ namespace CodeImp.DoomBuilder.Config
 					if(targetangle < 1 || targetangle > 8)
 					{
 						General.ErrorLogger.Add(ErrorType.Error, "Error in actor \"" + title + "\":" + index + ", sprite \"" + s + "\". Expected sprite angle in [1..8] range");
-						return;
+						return false;
 					}
 
 					// Even more sanity checks
@@ -640,7 +719,7 @@ namespace CodeImp.DoomBuilder.Config
 					if(!int.TryParse(anglestr, NumberStyles.Integer, CultureInfo.InvariantCulture, out targetangle))
 					{
 						General.ErrorLogger.Add(ErrorType.Error, "Error in actor \"" + title + "\":" + index + ". Unable to get sprite angle from sprite \"" + s + "\"");
-						return;
+						return false;
 					}
 
 					// Sanity checks
@@ -654,7 +733,7 @@ namespace CodeImp.DoomBuilder.Config
 					if(targetangle < 1 || targetangle > 8)
 					{
 						General.ErrorLogger.Add(ErrorType.Error, "Error in actor \"" + title + "\":" + index + ", sprite \"" + s + "\". Expected sprite angle in [1..8] range");
-						return;
+						return false;
 					}
 
 					// Even more sanity checks
@@ -697,7 +776,7 @@ namespace CodeImp.DoomBuilder.Config
 				}
 
 				General.ErrorLogger.Add(ErrorType.Error, "Error in actor \"" + title + "\":" + index + ". Sprite rotations " + ma + " for sprite " + sourcename + ", frame " + sourceframe + " are missing");
-				return;
+				return false;
 			}
 
 			// Create collection
@@ -706,6 +785,12 @@ namespace CodeImp.DoomBuilder.Config
 			{
 				spriteframe[i] = new SpriteFrameInfo { Sprite = frames[i], SpriteLongName = Lump.MakeLongName(frames[i]), Mirror = mirror[i] };
 			}
+
+			// Update preview sprite
+			sprite = spriteframe[1].Sprite;
+
+			// Done
+			return false;
 		}
 
 		// This is used for sorting
diff --git a/Source/Core/Data/DataManager.cs b/Source/Core/Data/DataManager.cs
index c42c5d3d4..79b237117 100644
--- a/Source/Core/Data/DataManager.cs
+++ b/Source/Core/Data/DataManager.cs
@@ -1499,6 +1499,22 @@ namespace CodeImp.DoomBuilder.Data
 		// This loads the sprites that we really need for things
 		private int LoadThingSprites()
 		{
+			//mxd. Get all sprite names
+			HashSet<string> spritenames = new HashSet<string>(StringComparer.Ordinal);
+			foreach(DataReader dr in containers)
+			{
+				IEnumerable<string> result = dr.GetSpriteNames();
+				if(result != null) spritenames.UnionWith(result);
+			}
+			
+			//mxd. Get names of all voxel models, which can be used "as is" (these do not require corresponding sprite to work)
+			HashSet<string> voxelnames = new HashSet<string>(StringComparer.Ordinal);
+			foreach(DataReader dr in containers)
+			{
+				IEnumerable<string> result = dr.GetVoxelNames();
+				if(result != null) voxelnames.UnionWith(result);
+			}
+			
 			// Go for all things
 			foreach(ThingTypeInfo ti in General.Map.Data.ThingTypes)
 			{
@@ -1506,51 +1522,69 @@ namespace CodeImp.DoomBuilder.Data
 				if(ti.Sprite.Length == 0 || ti.Sprite.Length > CLASIC_IMAGE_NAME_LENGTH) continue; //mxd
 					
 				//mxd. Find all sprite angles
-				ti.SetupSpriteFrame();
+				bool isvoxel = ti.SetupSpriteFrame(spritenames, voxelnames);
 
-				//mxd. Load them all
-				foreach(SpriteFrameInfo info in ti.SpriteFrame)
+				//mxd. Create voxel sprite?
+				if(isvoxel)
 				{
-					ImageData image = null;
-					
-					// Sprite not in our collection yet?
-					if(!sprites.ContainsKey(info.SpriteLongName))
+					if(!sprites.ContainsKey(Lump.MakeLongName(ti.Sprite)))
+					{
+						// Make new voxel image
+						VoxelImage image = new VoxelImage(ti.Sprite, ti.Sprite);
+
+						// Add to collection
+						sprites.Add(image.LongName, image);
+
+						// Add to preview manager
+						previews.AddImage(image);
+					}
+				}
+				else
+				{
+					//mxd. Load all sprites
+					foreach(SpriteFrameInfo info in ti.SpriteFrame)
 					{
-						//mxd. Go for all opened containers
-						bool spritefound = false;
-						if(!string.IsNullOrEmpty(info.Sprite))
+						ImageData image = null;
+
+						// Sprite not in our collection yet?
+						if(!sprites.ContainsKey(info.SpriteLongName))
 						{
-							for(int i = containers.Count - 1; i >= 0; i--)
+							//mxd. Go for all opened containers
+							bool spritefound = false;
+							if(!string.IsNullOrEmpty(info.Sprite))
 							{
-								// This contain provides this sprite?
-								if(containers[i].GetSpriteExists(info.Sprite))
+								for(int i = containers.Count - 1; i >= 0; i--)
 								{
-									spritefound = true;
-									break;
+									// This container provides this sprite?
+									if(containers[i].GetSpriteExists(info.Sprite))
+									{
+										spritefound = true;
+										break;
+									}
 								}
 							}
-						}
 
-						if(spritefound)
-						{
-							// Make new sprite image
-							image = new SpriteImage(info.Sprite);
+							if(spritefound)
+							{
+								// Make new sprite image
+								image = new SpriteImage(info.Sprite);
 
-							// Add to collection
-							sprites.Add(info.SpriteLongName, image);
+								// Add to collection
+								sprites.Add(info.SpriteLongName, image);
+							}
+							else
+							{
+								General.ErrorLogger.Add(ErrorType.Error, "Unable to find sprite lump \"" + info.Sprite + "\" used by actor \"" + ti.Title + "\":" + ti.Index + ". Forgot to include required resources?");
+							}
 						}
 						else
 						{
-							General.ErrorLogger.Add(ErrorType.Error, "Unable to find sprite lump \"" + info.Sprite + "\" used by actor \"" + ti.Title + "\":" + ti.Index + ". Forgot to include required resources?");
+							image = sprites[info.SpriteLongName];
 						}
-					}
-					else
-					{
-						image = sprites[info.SpriteLongName];
-					}
 
-					// Add to preview manager
-					if(image != null) previews.AddImage(image);
+						// Add to preview manager
+						if(image != null) previews.AddImage(image);
+					}
 				}
 			}
 			
@@ -1566,7 +1600,7 @@ namespace CodeImp.DoomBuilder.Data
 				// Go for all opened containers
 				for(int i = containers.Count - 1; i >= 0; i--)
 				{
-					// This contain provides this sprite?
+					// This container provides this sprite?
 					Stream spritedata = containers[i].GetSpriteData(pname, ref spritelocation);
 					if(spritedata != null) return spritedata;
 				}
@@ -1719,18 +1753,40 @@ namespace CodeImp.DoomBuilder.Data
 		}
 
 		//mxd. Returns all sprite names, which start with given string
-		internal IEnumerable<string> GetSpriteNames(string startswith)
+		internal IEnumerable<string> GetSpriteNames()
 		{
 			HashSet<string> result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 
 			foreach(DataReader reader in containers)
-				result.UnionWith(reader.GetSpriteNames(startswith));
+				result.UnionWith(reader.GetSpriteNames());
 
 			return result;
 		}
 		
 		#endregion
 
+		#region ================== mxd. Voxels
+
+		// This returns a specific voxel stream
+		internal Stream GetVoxelData(string pname, ref string voxellocation)
+		{
+			if(!string.IsNullOrEmpty(pname))
+			{
+				// Go for all opened containers
+				for(int i = containers.Count - 1; i >= 0; i--)
+				{
+					// This container provides this sprite?
+					Stream spritedata = containers[i].GetVoxelData(pname, ref voxellocation);
+					if(spritedata != null) return spritedata;
+				}
+			}
+
+			// No such voxel found
+			return null;
+		}
+
+		#endregion
+
 		#region ================== Things
 		
 		// This loads the things from Decorate
@@ -2210,43 +2266,14 @@ namespace CodeImp.DoomBuilder.Data
 			// Bail out when not supported by current game configuration
 			if(string.IsNullOrEmpty(General.Map.Config.DecorateGames)) return;
 			
-			// Get names of all voxel models, which can be used "as is"
-			HashSet<string> voxelnames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
-			
-			foreach(DataReader dr in containers) 
-			{
-				currentreader = dr;
-
-				IEnumerable<string> result = dr.GetVoxelNames();
-				if(result == null) continue;
-
-				foreach(string s in result) 
-				{
-					if(!voxelnames.Contains(s)) voxelnames.Add(s);
-				}
-			}
-
-			Dictionary<string, List<int>> sprites = new Dictionary<string, List<int>>(StringComparer.Ordinal);
-
 			// Go for all things
+			Dictionary<string, List<int>> allsprites = new Dictionary<string, List<int>>(StringComparer.Ordinal);
 			foreach(ThingTypeInfo ti in thingtypes.Values) 
 			{
 				// Valid sprite name?
-				string sprite;
-
-				if(ti.Sprite.Length == 0 || ti.Sprite.Length > CLASIC_IMAGE_NAME_LENGTH) 
-				{
-					if(ti.Actor == null) continue;
-					sprite = ti.Actor.FindSuitableVoxel(voxelnames);
-				} 
-				else 
-				{
-					sprite = ti.Sprite;
-				}
-
-				if(string.IsNullOrEmpty(sprite)) continue;
-				if(!sprites.ContainsKey(sprite)) sprites.Add(sprite, new List<int>());
-				sprites[sprite].Add(ti.Index);
+				if(string.IsNullOrEmpty(ti.Sprite) || ti.Sprite.Length > CLASIC_IMAGE_NAME_LENGTH) continue;
+				if(!allsprites.ContainsKey(ti.Sprite)) allsprites.Add(ti.Sprite, new List<int>());
+				allsprites[ti.Sprite].Add(ti.Index);
 			}
 
 			VoxeldefParser parser = new VoxeldefParser();
@@ -2264,13 +2291,34 @@ namespace CodeImp.DoomBuilder.Data
 					{
 						foreach(KeyValuePair<string, ModelData> entry in parser.Entries)
 						{
-							foreach(KeyValuePair<string, List<int>> sc in sprites)
+							foreach(KeyValuePair<string, List<int>> sc in allsprites)
 							{
-								if(sc.Key.Contains(entry.Key))
+								if(sc.Key.StartsWith(entry.Key, StringComparison.OrdinalIgnoreCase))
 								{
-									foreach(int id in sc.Value)
-										modeldefentries[id] = entry.Value;
-									processed.Add(entry.Key);
+									foreach(int id in sc.Value) modeldefentries[id] = entry.Value;
+									processed.Add(sc.Key);
+
+									// Create preview image if it doesn't exist...
+									ImageData sprite = GetSpriteImage(sc.Key);
+									if(sprite == null)
+									{
+										// Make new voxel image
+										sprite = new VoxelImage(sc.Key, entry.Value.ModelNames[0]);
+
+										// Add to collection
+										sprites.Add(sprite.LongName, sprite);
+
+										// Add to preview manager
+										previews.AddImage(sprite);
+									}
+
+									// Apply VOXELDEF settings to the preview image...
+									VoxelImage vi = sprite as VoxelImage;
+									if(vi != null)
+									{
+										vi.AngleOffset = (int)Math.Round(entry.Value.AngleOffset);
+										vi.OverridePalette = entry.Value.OverridePalette;
+									}
 								}
 							}
 						}
@@ -2286,19 +2334,18 @@ namespace CodeImp.DoomBuilder.Data
 			currentreader = null;
 
 			// Get voxel models
-			foreach(string voxelname in voxelnames) 
+			foreach(KeyValuePair<string, List<int>> sc in allsprites)
 			{
-				if(processed.Contains(voxelname)) continue;
-				foreach(KeyValuePair<string, List<int>> sc in sprites) 
+				if(processed.Contains(sc.Key)) continue;
+				
+				VoxelImage vi = GetSpriteImage(sc.Key) as VoxelImage;
+				if(vi != null)
 				{
-					if(sc.Key.Contains(voxelname)) 
-					{
-						// It's a model without a definition, and it corresponds to a sprite we can display, so let's add it
-						ModelData data = new ModelData { IsVoxel = true };
-						data.ModelNames.Add(voxelname);
+					// It's a model without a definition, and it corresponds to a sprite we can display, so let's add it
+					ModelData data = new ModelData { IsVoxel = true };
+					data.ModelNames.Add(vi.VoxelName);
 
-						foreach(int id in sprites[sc.Key]) modeldefentries[id] = data;
-					}
+					foreach(int id in sc.Value) modeldefentries[id] = data;
 				}
 			}
 		}
diff --git a/Source/Core/Data/DataReader.cs b/Source/Core/Data/DataReader.cs
index 64b88021f..8c0094119 100644
--- a/Source/Core/Data/DataReader.cs
+++ b/Source/Core/Data/DataReader.cs
@@ -86,8 +86,6 @@ namespace CodeImp.DoomBuilder.Data
 	{
 		#region ================== Constants
 
-		protected const string SPRITE_NAME_PATTERN = "(?i)\\A[a-z0-9]{4}([a-z][0-9]{0,2})$"; //mxd
-
 		#endregion
 
 		#region ================== Variables
@@ -215,7 +213,7 @@ namespace CodeImp.DoomBuilder.Data
 		public abstract bool GetSpriteExists(string pname);
 
 		//mxd. When implemented, returns all sprites, which name starts with given string
-		public abstract HashSet<string> GetSpriteNames(string startswith);
+		public abstract HashSet<string> GetSpriteNames();
 		
 		#endregion
 
@@ -258,10 +256,10 @@ namespace CodeImp.DoomBuilder.Data
 		public abstract IEnumerable<TextResourceData> GetCvarInfoData();
 
 		//mxd. When implemented, this returns the list of voxel model names
-		public abstract IEnumerable<string> GetVoxelNames();
+		public abstract HashSet<string> GetVoxelNames();
 
 		//mxd. When implemented, this returns the voxel lump
-		public abstract Stream GetVoxelData(string name);
+		public abstract Stream GetVoxelData(string name, ref string voxellocation);
 
 		#endregion
 
diff --git a/Source/Core/Data/DirectoryReader.cs b/Source/Core/Data/DirectoryReader.cs
index d07a2dce7..0834643cc 100644
--- a/Source/Core/Data/DirectoryReader.cs
+++ b/Source/Core/Data/DirectoryReader.cs
@@ -343,7 +343,7 @@ namespace CodeImp.DoomBuilder.Data
 		#region ================== Voxels (mxd)
 
 		//mxd.  This finds and returns a voxel stream
-		public override Stream GetVoxelData(string name)
+		public override Stream GetVoxelData(string name, ref string voxellocation)
 		{
 			// Error when suspended
 			if(issuspended) throw new Exception("Data reader is suspended");
@@ -351,7 +351,7 @@ namespace CodeImp.DoomBuilder.Data
 			// Find in any of the wad files
 			for(int i = wads.Count - 1; i >= 0; i--)
 			{
-				Stream voxel = wads[i].GetVoxelData(name);
+				Stream voxel = wads[i].GetVoxelData(name, ref voxellocation);
 				if(voxel != null) return voxel;
 			}
 
@@ -362,6 +362,7 @@ namespace CodeImp.DoomBuilder.Data
 				string filename = FindFirstFile(path, Path.GetFileName(name), true);
 				if((filename != null) && FileExists(filename))
 				{
+					voxellocation = location.GetDisplayName();
 					return LoadFile(filename);
 				}
 			}
diff --git a/Source/Core/Data/PK3Reader.cs b/Source/Core/Data/PK3Reader.cs
index d0735d050..0483a52ef 100644
--- a/Source/Core/Data/PK3Reader.cs
+++ b/Source/Core/Data/PK3Reader.cs
@@ -359,7 +359,7 @@ namespace CodeImp.DoomBuilder.Data
 		#region ================== Voxels (mxd)
 
 		//mxd. This finds and returns a voxel stream or null if no voxel was found
-		public override Stream GetVoxelData(string name) 
+		public override Stream GetVoxelData(string name, ref string voxellocation) 
 		{
 			// Error when suspended
 			if(issuspended) throw new Exception("Data reader is suspended");
@@ -367,7 +367,7 @@ namespace CodeImp.DoomBuilder.Data
 			// Find in any of the wad files
 			for(int i = wads.Count - 1; i >= 0; i--) 
 			{
-				Stream voxel = wads[i].GetVoxelData(name);
+				Stream voxel = wads[i].GetVoxelData(name, ref voxellocation);
 				if(voxel != null) return voxel;
 			}
 
@@ -377,6 +377,7 @@ namespace CodeImp.DoomBuilder.Data
 			string filename = FindFirstFile(VOXELS_DIR, pfilename, true);
 			if((filename != null) && FileExists(filename)) 
 			{
+				voxellocation = location.GetDisplayName();
 				return LoadFile(filename);
 			}
 
diff --git a/Source/Core/Data/PK3StructuredReader.cs b/Source/Core/Data/PK3StructuredReader.cs
index b4d4efb99..53be08c96 100644
--- a/Source/Core/Data/PK3StructuredReader.cs
+++ b/Source/Core/Data/PK3StructuredReader.cs
@@ -422,8 +422,8 @@ namespace CodeImp.DoomBuilder.Data
 			return new List<ImageData>(images.Values);
 		}
 
-		//mxd. Returns all sprites, which name starts with given string
-		public override HashSet<string> GetSpriteNames(string startswith)
+		//mxd. This returns all sprite names
+		public override HashSet<string> GetSpriteNames()
 		{
 			// Error when suspended
 			if(issuspended) throw new Exception("Data reader is suspended");
@@ -434,11 +434,11 @@ namespace CodeImp.DoomBuilder.Data
 			// Note the backward order, because the last wad's images have priority
 			for(int i = wads.Count - 1; i >= 0; i--)
 			{
-				result.UnionWith(wads[i].GetSpriteNames(startswith));
+				result.UnionWith(wads[i].GetSpriteNames());
 			}
 
 			// Load from out own files
-			string[] files = GetAllFilesWhichTitleStartsWith(SPRITES_DIR, startswith, true);
+			string[] files = GetAllFiles(SPRITES_DIR, true);
 			foreach(string file in files)
 			{
 				// Some users tend to place all manner of graphics into the "Sprites" folder...
@@ -553,22 +553,29 @@ namespace CodeImp.DoomBuilder.Data
 		#region ================== VOXELDEF (mxd)
 
 		//mxd. This returns the list of voxels, which can be used without VOXELDEF definition
-		public override IEnumerable<string> GetVoxelNames() 
+		public override HashSet<string> GetVoxelNames() 
 		{
 			// Error when suspended
 			if(issuspended) throw new Exception("Data reader is suspended");
 
-			string[] files = GetAllFiles("voxels", false);
-			List<string> voxels = new List<string>();
-			Regex spritename = new Regex(SPRITE_NAME_PATTERN);
+			HashSet<string> result = new HashSet<string>();
 
+			// Load from wad files
+			// Note the backward order, because the last wad's images have priority
+			for(int i = wads.Count - 1; i >= 0; i--)
+			{
+				result.UnionWith(wads[i].GetVoxelNames());
+			}
+
+			// Load from out own files
+			string[] files = GetAllFiles("voxels", false);
 			foreach(string t in files)
 			{
 				string s = Path.GetFileNameWithoutExtension(t).ToUpperInvariant();
-				if(spritename.IsMatch(s)) voxels.Add(s);
+				if(WADReader.IsValidVoxelName(s)) result.Add(s);
 			}
 
-			return voxels.ToArray();
+			return result;
 		}
 
 		//mxd
diff --git a/Source/Core/Data/SpriteImage.cs b/Source/Core/Data/SpriteImage.cs
index 9d8fb4be9..97e7be274 100644
--- a/Source/Core/Data/SpriteImage.cs
+++ b/Source/Core/Data/SpriteImage.cs
@@ -17,16 +17,22 @@
 #region ================== Namespaces
 
 using System;
-using CodeImp.DoomBuilder.IO;
 using System.IO;
 using System.Runtime.InteropServices;
+using CodeImp.DoomBuilder.IO;
 using CodeImp.DoomBuilder.Windows;
 
 #endregion
 
 namespace CodeImp.DoomBuilder.Data
 {
-	public sealed class SpriteImage : ImageData
+	public interface ISpriteImage //mxd
+	{
+		int OffsetX { get; }
+		int OffsetY { get; }
+	}
+
+	public sealed class SpriteImage : ImageData, ISpriteImage
 	{
 		#region ================== Variables
 
diff --git a/Source/Core/Data/VoxelImage.cs b/Source/Core/Data/VoxelImage.cs
new file mode 100644
index 000000000..316e2e3c2
--- /dev/null
+++ b/Source/Core/Data/VoxelImage.cs
@@ -0,0 +1,325 @@
+#region ================== Namespaces
+
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+using CodeImp.DoomBuilder.Rendering;
+using CodeImp.DoomBuilder.Windows;
+
+#endregion
+
+namespace CodeImp.DoomBuilder.Data
+{
+	public sealed class VoxelImage : ImageData, ISpriteImage
+	{
+		#region ================== Variables
+
+		private int offsetx;
+		private int offsety;
+		private readonly string voxelname;
+		private bool overridepalette;
+		private int angleoffset;
+		
+		#endregion
+
+		#region ================== Properties
+
+		public int OffsetX { get { return offsetx; } }
+		public int OffsetY { get { return offsety; } }
+		public string VoxelName { get { return voxelname; } }
+		public bool OverridePalette { get { return overridepalette; } internal set { overridepalette = value; } }
+		public int AngleOffset { get { return angleoffset; } internal set { angleoffset = value; } }
+
+		#endregion
+		
+		#region ================== Constructor / Disposer
+
+		// Constructor
+		internal VoxelImage(string name, string voxelname)
+		{
+			// Initialize
+			SetName(name);
+			this.voxelname = voxelname;
+
+			// We have no destructor
+			GC.SuppressFinalize(this);
+		}
+
+		#endregion
+
+		#region ================== Methods
+
+		override public void LoadImage()
+		{
+			// Do the loading
+			LocalLoadImage();
+
+			// Notify the main thread about the change to redraw display
+			IntPtr strptr = Marshal.StringToCoTaskMemAuto(this.Name);
+			General.SendMessage(General.MainWindow.Handle, (int)MainForm.ThreadMessages.SpriteDataLoaded, strptr.ToInt32(), 0);
+		}
+
+		// This loads the image
+		protected unsafe override void LocalLoadImage()
+		{
+			// Leave when already loaded
+			if(this.IsImageLoaded) return;
+
+			lock(this)
+			{
+				// Get the lump data stream
+				string voxellocation = string.Empty; //mxd
+				Stream lumpdata = General.Map.Data.GetVoxelData(voxelname, ref voxellocation);
+				if(lumpdata != null)
+				{
+					// Copy lump data to memory
+					lumpdata.Seek(0, SeekOrigin.Begin);
+					byte[] membytes = new byte[(int)lumpdata.Length];
+					lumpdata.Read(membytes, 0, (int)lumpdata.Length);
+					
+					using(MemoryStream mem = new MemoryStream(membytes))
+					{
+						mem.Seek(0, SeekOrigin.Begin);
+						PixelColor[] palette = new PixelColor[256];
+
+						// Create front projection image from the KVX
+						using(BinaryReader reader = new BinaryReader(mem, Encoding.ASCII))
+						{
+							reader.ReadInt32(); //numbytes, we don't use that
+							int xsize = reader.ReadInt32();
+							int ysize = reader.ReadInt32();
+							int zsize = reader.ReadInt32();
+
+							// Sanity check
+							if(xsize == 0 || ysize == 0 || zsize == 0)
+							{
+								General.ErrorLogger.Add(ErrorType.Error, "Cannot create sprite image for voxel \"" + Path.Combine(voxellocation, voxelname) 
+									+ "\" for voxel drawing: voxel has invalid size (width: " + xsize + ", height: " + zsize + ", depth: " + ysize);
+								loadfailed = true;
+								return;
+							}
+
+							int pivotx = (int)Math.Round(reader.ReadInt32() / 256f);
+							int pivoty = (int)Math.Round(reader.ReadInt32() / 256f);
+							int pivotz = (int)Math.Round(reader.ReadInt32() / 256f);
+
+							// Read offsets
+							int[] xoffset = new int[xsize + 1]; // why is it xsize + 1, not xsize?..
+							short[,] xyoffset = new short[xsize, ysize + 1]; // why is it ysize + 1, not ysize?..
+
+							for(int i = 0; i < xoffset.Length; i++)
+							{
+								xoffset[i] = reader.ReadInt32();
+							}
+
+							for(int x = 0; x < xsize; x++)
+							{
+								for(int y = 0; y < ysize + 1; y++)
+								{
+									xyoffset[x, y] = reader.ReadInt16();
+								}
+							}
+
+							// Read slabs
+							List<int> offsets = new List<int>(xsize * ysize);
+							for(int x = 0; x < xsize; x++)
+							{
+								for(int y = 0; y < ysize; y++)
+								{
+									offsets.Add(xoffset[x] + xyoffset[x, y] + 28); // for some reason offsets are counted from start of xoffset[]...
+								}
+							}
+
+							int counter = 0;
+							int slabsend = (int)(reader.BaseStream.Length - 768);
+
+							// Read palette
+							if(!overridepalette)
+							{
+								reader.BaseStream.Position = slabsend;
+								for(int i = 0; i < 256; i++)
+								{
+									byte r = (byte)(reader.ReadByte() * 4);
+									byte g = (byte)(reader.ReadByte() * 4);
+									byte b = (byte)(reader.ReadByte() * 4);
+									palette[i] = new PixelColor(255, r, g, b);
+								}
+							}
+							else
+							{
+								for(int i = 0; i < 256; i++) palette[i] = General.Map.Data.Palette[i];
+							}
+
+							// Populate projection pixels array
+							int imgwidth, imgheight, imgoffsetx;
+							bool checkalpha = false;
+
+							// Convert angleoffsets to the nearest cardinal direction...
+							angleoffset = General.ClampAngle((angleoffset + 45) / 90 * 90);
+
+							switch(angleoffset)
+							{
+								case 0:
+									imgwidth = xsize;
+									imgheight = zsize;
+									imgoffsetx = pivotx;
+									break;
+
+								case 90:
+									imgwidth = ysize;
+									imgheight = zsize;
+									imgoffsetx = imgwidth - pivoty;
+									checkalpha = true;
+									break;
+
+								case 180:
+									imgwidth = xsize;
+									imgheight = zsize;
+									imgoffsetx = imgwidth - pivotx;
+									checkalpha = true;
+									break;
+
+								case 270:
+									imgwidth = ysize;
+									imgheight = zsize;
+									imgoffsetx = pivoty;
+									break;
+
+								default: throw new InvalidDataException("Invalid AngleOffset");
+							}
+
+							int numpixels = imgwidth * imgheight;
+							PixelColor[] pixelsarr = new PixelColor[numpixels];
+
+							// Read pixel colors
+							for(int x = 0; x < xsize; x++)
+							{
+								for(int y = 0; y < ysize; y++)
+								{
+									reader.BaseStream.Position = offsets[counter];
+									int next = (counter < offsets.Count - 1 ? offsets[counter + 1] : slabsend);
+
+									// Read first color from the slab
+									while(reader.BaseStream.Position < next)
+									{
+										int ztop = reader.ReadByte();
+										int zleng = reader.ReadByte();
+										if(ztop + zleng > zsize) break;
+										byte flags = reader.ReadByte();
+
+										if(zleng > 0)
+										{
+											// Skip slab if no flags are given (otherwise some garbage pixels may be drawn)
+											if(flags == 0)
+											{
+												reader.BaseStream.Position += zleng;
+												continue;
+											}
+											
+											List<int> colorindices = new List<int>(zleng);
+											for(int i = 0; i < zleng; i++)
+											{
+												colorindices.Add(reader.ReadByte());
+											}
+
+											int z = ztop;
+											int cstart = 0;
+											while(z < ztop + zleng)
+											{
+												// Get pixel position
+												int pixelpos;
+												switch(angleoffset)
+												{
+													case 0:   pixelpos = x + z * xsize; break;
+													case 90:  pixelpos = y + z * ysize; break;
+													case 180: pixelpos = xsize - x - 1 + z * xsize; break;
+													case 270: pixelpos = ysize - y - 1 + z * ysize; break;
+													default: throw new InvalidDataException("Invalid AngleOffset");
+												}
+
+												// Add to projection pixels array
+												if((checkalpha && pixelsarr[pixelpos].a == 0) || !checkalpha)
+													pixelsarr[pixelpos] = palette[colorindices[cstart]];
+
+												// Increment counters
+												cstart++;
+												z++;
+											}
+										}
+									}
+
+									counter++;
+								}
+							}
+
+							// Draw to bitmap
+							if(bitmap != null) bitmap.Dispose();
+							bitmap = new Bitmap(imgwidth, imgheight, PixelFormat.Format32bppArgb);
+							BitmapData bmpdata = null;
+
+							try
+							{
+								bmpdata = bitmap.LockBits(new Rectangle(0, 0, imgwidth, imgheight), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
+							}
+							catch(Exception e)
+							{
+								General.ErrorLogger.Add(ErrorType.Error, "Cannot lock image for drawing voxel \""
+									+ Path.Combine(voxellocation, voxelname) + "\". " + e.GetType().Name + ": " + e.Message);
+								bitmap = null;
+							}
+
+							if(bmpdata != null)
+							{
+								// Apply pixels to image
+								PixelColor* pixels = (PixelColor*)bmpdata.Scan0.ToPointer();
+								int i = 0;
+
+								for(PixelColor* cp = pixels; cp < pixels + numpixels; cp++, i++)
+								{
+									if(pixelsarr[i].a == 255)
+									{
+										cp->r = pixelsarr[i].r;
+										cp->g = pixelsarr[i].g;
+										cp->b = pixelsarr[i].b;
+										cp->a = 255;
+									}
+								}
+
+								bitmap.UnlockBits(bmpdata);
+							}
+
+							if(bitmap != null)
+							{
+								// Get width and height from image
+								width = bitmap.Size.Width;
+								height = bitmap.Size.Height;
+								scale.x = 1.0f;
+								scale.y = 1.0f;
+								offsetx = imgoffsetx;
+								offsety = pivotz;
+							}
+							else
+							{
+								loadfailed = true;
+							}
+						}
+					}
+				}
+				else
+				{
+					// Missing voxel lump!
+					General.ErrorLogger.Add(ErrorType.Error, "Missing voxel lump \"" + voxelname + "\". Forgot to include required resources?");
+				}
+
+				// Pass on to base
+				base.LocalLoadImage();
+			}
+		}
+
+		#endregion
+	}
+}
diff --git a/Source/Core/Data/WADReader.cs b/Source/Core/Data/WADReader.cs
index 7e1d85512..f44cb6aff 100644
--- a/Source/Core/Data/WADReader.cs
+++ b/Source/Core/Data/WADReader.cs
@@ -42,6 +42,9 @@ namespace CodeImp.DoomBuilder.Data
 		private static readonly Regex sprite6 = new Regex(@"(\S{4}[A-Za-z\[\]\\]{1}[0-8]{1})");
 		private static readonly Regex sprite8 = new Regex(@"(\S{4}[A-Za-z\[\]\\]{1}[0-8]{1}[A-Za-z\[\]\\]{1}[0-8]{1})");
 
+		//mxd. Voxel recognition.
+		private static readonly Regex voxel = new Regex(@"^\S{4}(([A-Za-z][0-9]){0,2}|[A-Za-z]{0,1})$");
+
 		#endregion
 
 		#region ================== Structures
@@ -919,23 +922,17 @@ namespace CodeImp.DoomBuilder.Data
 			return false;
 		}
 
-		//mxd. Returns all sprites, which name starts with given string
-		public override HashSet<string> GetSpriteNames(string startswith)
+		//mxd. This returns all sprite names in the WAD
+		public override HashSet<string> GetSpriteNames()
 		{
 			// Error when suspended
 			if(issuspended) throw new Exception("Data reader is suspended");
 
 			HashSet<string> result = new HashSet<string>();
-			if(startswith.Length > 8) return result;
-
-			startswith = startswith.ToUpperInvariant();
 			foreach(LumpRange range in spriteranges)
 			{
 				for(int i = range.start; i < range.end + 1; i++)
-				{
-					if(file.Lumps[i].Name.StartsWith(startswith) && IsValidSpriteName(file.Lumps[i].Name))
-						result.Add(file.Lumps[i].Name);
-				}
+					if(IsValidSpriteName(file.Lumps[i].Name)) result.Add(file.Lumps[i].Name);
 			}
 
 			return result;
@@ -952,25 +949,23 @@ namespace CodeImp.DoomBuilder.Data
 		#region ================== Voxels (mxd)
 
 		//mxd. This returns the list of voxels, which can be used without VOXELDEF definition
-		public override IEnumerable<string> GetVoxelNames() 
+		public override HashSet<string> GetVoxelNames() 
 		{
 			// Error when suspended
 			if(issuspended) throw new Exception("Data reader is suspended");
 
-			List<string> voxels = new List<string>();
-			Regex spriteName = new Regex(SPRITE_NAME_PATTERN);
-
+			HashSet<string> result = new HashSet<string>();
 			foreach(LumpRange range in voxelranges) 
 			{
 				if(range.start == range.end) continue;
 
 				for(int i = range.start + 1; i < range.end; i++) 
 				{
-					if(spriteName.IsMatch(file.Lumps[i].Name)) voxels.Add(file.Lumps[i].Name);
+					if(IsValidVoxelName(file.Lumps[i].Name)) result.Add(file.Lumps[i].Name);
 				}
 			}
 
-			return voxels.ToArray();
+			return result;
 		}
 
 		//mxd
@@ -981,7 +976,7 @@ namespace CodeImp.DoomBuilder.Data
 		}
 
 		//mxd. This finds and returns a voxel stream or null if no voxel was found
-		public override Stream GetVoxelData(string name) 
+		public override Stream GetVoxelData(string name, ref string voxellocation) 
 		{
 			// Error when suspended
 			if(issuspended) throw new Exception("Data reader is suspended");
@@ -990,12 +985,22 @@ namespace CodeImp.DoomBuilder.Data
 			{
 				if(range.start == range.end) continue;
 				Lump lump = file.FindLump(name, range.start, range.end);
-				if(lump != null) return lump.Stream;
+				if(lump != null)
+				{
+					voxellocation = location.GetDisplayName();
+					return lump.Stream;
+				}
 			}
 
 			return null;
 		}
 
+		//mxd
+		internal static bool IsValidVoxelName(string name)
+		{
+			return (name.Length > 3 && name.Length < 7) && voxel.IsMatch(name);
+		}
+
 		#endregion
 
 		#region ================== Decorate, Gldefs, Mapinfo, etc...
diff --git a/Source/Core/GZBuilder/Data/ModelData.cs b/Source/Core/GZBuilder/Data/ModelData.cs
index edc58be71..3e2207ae1 100644
--- a/Source/Core/GZBuilder/Data/ModelData.cs
+++ b/Source/Core/GZBuilder/Data/ModelData.cs
@@ -38,7 +38,8 @@ namespace CodeImp.DoomBuilder.GZBuilder.Data
 
 		internal Vector3 Scale { get { return scale; } }
 		internal Matrix Transform { get { return (General.Settings.GZStretchView ? transformstretched : transform); } }
-		internal bool OverridePalette; //used for voxel models only 
+		internal bool OverridePalette; // Used for voxel models only 
+		internal float AngleOffset; // Used for voxel models only
 		internal bool InheritActorPitch;
 		internal bool InheritActorRoll;
 
diff --git a/Source/Core/GZBuilder/md3/ModelReader.cs b/Source/Core/GZBuilder/md3/ModelReader.cs
index 2ecba6434..5d1799c86 100644
--- a/Source/Core/GZBuilder/md3/ModelReader.cs
+++ b/Source/Core/GZBuilder/md3/ModelReader.cs
@@ -58,12 +58,13 @@ namespace CodeImp.DoomBuilder.GZBuilder.MD3
 		private static void LoadKVX(ModelData mde, List<DataReader> containers, Device device) 
 		{
 			mde.Model = new GZModel();
+			string unused = string.Empty;
 			foreach(string name in mde.ModelNames)
 			{
 				//find the model
 				foreach(DataReader dr in containers) 
 				{
-					Stream ms = dr.GetVoxelData(name);
+					Stream ms = dr.GetVoxelData(name, ref unused);
 					if(ms == null) continue;
 
 					//load kvx
diff --git a/Source/Core/Map/Thing.cs b/Source/Core/Map/Thing.cs
index 375ee534c..aee4b4100 100644
--- a/Source/Core/Map/Thing.cs
+++ b/Source/Core/Map/Thing.cs
@@ -36,7 +36,7 @@ namespace CodeImp.DoomBuilder.Map
 		#region ================== Constants
 
 		public const int NUM_ARGS = 5;
-		public static HashSet<ThingRenderMode> AlignableRenderModes = new HashSet<ThingRenderMode>
+		public static readonly HashSet<ThingRenderMode> AlignableRenderModes = new HashSet<ThingRenderMode>
 		{
 			ThingRenderMode.FLATSPRITE, ThingRenderMode.WALLSPRITE, ThingRenderMode.MODEL
 		}; 
@@ -546,14 +546,9 @@ namespace CodeImp.DoomBuilder.Map
 			// Check if the thing has model override
 			if(General.Map.Data.ModeldefEntries.ContainsKey(type))
 			{
-				if(General.Map.Data.ModeldefEntries[type].LoadState == ModelLoadState.None)
-				{
-					if(General.Map.Data.ProcessModel(type)) rendermode = ThingRenderMode.MODEL;
-				}
-				else
-				{
-					rendermode = ThingRenderMode.MODEL;
-				}
+				ModelData md = General.Map.Data.ModeldefEntries[type];
+				if((md.LoadState == ModelLoadState.None && General.Map.Data.ProcessModel(type)) || md.LoadState != ModelLoadState.None)
+					rendermode = (General.Map.Data.ModeldefEntries[type].IsVoxel ? ThingRenderMode.VOXEL : ThingRenderMode.MODEL);
 			}
 
 			// Update radian versions of pitch and roll
@@ -575,6 +570,11 @@ namespace CodeImp.DoomBuilder.Map
 					pitchrad = 0;
 					break;
 
+				case ThingRenderMode.VOXEL:
+					rollrad = 0;
+					pitchrad = 0;
+					break;
+
 				default: throw new NotImplementedException("Unknown ThingRenderMode");
 			}
 		}
diff --git a/Source/Core/Config/RenderModeEnums.cs b/Source/Core/Rendering/RenderModeEnums.cs
similarity index 75%
rename from Source/Core/Config/RenderModeEnums.cs
rename to Source/Core/Rendering/RenderModeEnums.cs
index 3cbceb676..c47df554f 100644
--- a/Source/Core/Config/RenderModeEnums.cs
+++ b/Source/Core/Rendering/RenderModeEnums.cs
@@ -1,4 +1,4 @@
-namespace CodeImp.DoomBuilder.Config
+namespace CodeImp.DoomBuilder.Rendering
 {
 	public enum ModelRenderMode
 	{
@@ -19,6 +19,7 @@
 	{
 		NORMAL,
 		MODEL,
+		VOXEL,
 		WALLSPRITE,
 		FLATSPRITE,
 	}
diff --git a/Source/Core/Rendering/Renderer2D.cs b/Source/Core/Rendering/Renderer2D.cs
index 84d1cf031..51b924ee6 100644
--- a/Source/Core/Rendering/Renderer2D.cs
+++ b/Source/Core/Rendering/Renderer2D.cs
@@ -1175,7 +1175,7 @@ namespace CodeImp.DoomBuilder.Rendering
 					if(!fixedcolor && t.Highlighted) continue;
 					
 					// Collect models
-					if(t.RenderMode == ThingRenderMode.MODEL) 
+					if(t.RenderMode == ThingRenderMode.MODEL || t.RenderMode == ThingRenderMode.VOXEL) 
 					{
 						if(!modelsByType.ContainsKey(t.Type)) modelsByType.Add(t.Type, new List<Thing>());
 						modelsByType[t.Type].Add(t);
@@ -1233,7 +1233,8 @@ namespace CodeImp.DoomBuilder.Rendering
 				foreach(KeyValuePair<int, List<Thing>> group in thingsByType)
 				{
 					// Skip when all things of this type will be rendered as models
-					if(group.Value[0].RenderMode == ThingRenderMode.MODEL && (General.Settings.GZDrawModelsMode == ModelRenderMode.ALL)) continue;
+					if((group.Value[0].RenderMode == ThingRenderMode.MODEL || group.Value[0].RenderMode == ThingRenderMode.VOXEL)
+						&& (General.Settings.GZDrawModelsMode == ModelRenderMode.ALL)) continue;
 					
 					// Find thing information
 					ThingTypeInfo info = General.Map.Data.GetThingInfo(group.Key);
@@ -1285,7 +1286,8 @@ namespace CodeImp.DoomBuilder.Rendering
 
 						foreach(Thing t in framegroup.Value)
 						{
-							if(t.RenderMode == ThingRenderMode.MODEL && ((General.Settings.GZDrawModelsMode == ModelRenderMode.SELECTION && t.Selected) || (General.Settings.GZDrawModelsMode == ModelRenderMode.ACTIVE_THINGS_FILTER && alpha == 1.0f)))
+							if((t.RenderMode == ThingRenderMode.MODEL || t.RenderMode == ThingRenderMode.VOXEL)
+								&& ((General.Settings.GZDrawModelsMode == ModelRenderMode.SELECTION && t.Selected) || (General.Settings.GZDrawModelsMode == ModelRenderMode.ACTIVE_THINGS_FILTER && alpha == 1.0f)))
 								continue;
 
 							bool forcespriterendering;
diff --git a/Source/Core/Rendering/Renderer3D.cs b/Source/Core/Rendering/Renderer3D.cs
index 50fb83ae6..479d8cd0f 100644
--- a/Source/Core/Rendering/Renderer3D.cs
+++ b/Source/Core/Rendering/Renderer3D.cs
@@ -1238,8 +1238,17 @@ namespace CodeImp.DoomBuilder.Rendering
 		//mxd
 		private Matrix CreateThingPositionMatrix(VisualThing t)
 		{
+			// Use normal ThingRenderMode when model rendering is disabled for this thing
+			ThingRenderMode rendermode = t.Thing.RenderMode;
+			if((t.Thing.RenderMode == ThingRenderMode.MODEL || t.Thing.RenderMode == ThingRenderMode.VOXEL) &&
+			   (General.Settings.GZDrawModelsMode == ModelRenderMode.NONE ||
+			   (General.Settings.GZDrawModelsMode == ModelRenderMode.SELECTION && !t.Selected)))
+			{
+				rendermode = ThingRenderMode.NORMAL;
+			}
+			
 			// Create the matrix for positioning
-			switch(t.Thing.RenderMode)
+			switch(rendermode)
 			{
 				case ThingRenderMode.NORMAL:
 					if(t.Info.XYBillboard) // Apply billboarding?
@@ -1266,6 +1275,7 @@ namespace CodeImp.DoomBuilder.Rendering
 
 				case ThingRenderMode.WALLSPRITE:
 				case ThingRenderMode.MODEL:
+				case ThingRenderMode.VOXEL:
 					return Matrix.Scaling(t.Thing.ScaleX, t.Thing.ScaleX, t.Thing.ScaleY) * t.Position;
 
 				default: throw new NotImplementedException("Unknown ThingRenderMode");
@@ -1722,7 +1732,7 @@ namespace CodeImp.DoomBuilder.Rendering
 			}
 
 			//mxd. Gather models
-			if(t.Thing.RenderMode == ThingRenderMode.MODEL && 
+			if((t.Thing.RenderMode == ThingRenderMode.MODEL || t.Thing.RenderMode == ThingRenderMode.VOXEL) && 
 				(General.Settings.GZDrawModelsMode == ModelRenderMode.ALL ||
 				 General.Settings.GZDrawModelsMode == ModelRenderMode.ACTIVE_THINGS_FILTER ||
 				(General.Settings.GZDrawModelsMode == ModelRenderMode.SELECTION && t.Selected))) 
diff --git a/Source/Core/VisualModes/VisualThing.cs b/Source/Core/VisualModes/VisualThing.cs
index e04f81bf8..6c54b6f47 100644
--- a/Source/Core/VisualModes/VisualThing.cs
+++ b/Source/Core/VisualModes/VisualThing.cs
@@ -292,6 +292,7 @@ namespace CodeImp.DoomBuilder.VisualModes
 				{
 					// Don't do anything
 					case ThingRenderMode.MODEL: break;
+					case ThingRenderMode.VOXEL: break;
 					
 					// Actor becomes a flat sprite which can be tilted with the use of the Pitch actor property.
 					case ThingRenderMode.FLATSPRITE:
@@ -407,7 +408,7 @@ namespace CodeImp.DoomBuilder.VisualModes
 						}
 						break;*/
 
-					default:
+					case ThingRenderMode.NORMAL:
 						if(info.RollSprite)
 						{
 							transform = Matrix.Translation(0f, 0f, -localcenterz) * Matrix.RotationY(-thing.RollRad) * Matrix.Translation(0f, 0f, localcenterz);
@@ -420,6 +421,8 @@ namespace CodeImp.DoomBuilder.VisualModes
 							}
 						}
 						break;
+
+					default: throw new NotImplementedException("Unknown ThingRenderMode");
 				}
 			}
 		}
diff --git a/Source/Core/ZDoom/ActorStructure.cs b/Source/Core/ZDoom/ActorStructure.cs
index 5da6f1458..e53f19e0d 100644
--- a/Source/Core/ZDoom/ActorStructure.cs
+++ b/Source/Core/ZDoom/ActorStructure.cs
@@ -31,7 +31,7 @@ namespace CodeImp.DoomBuilder.ZDoom
 	{
 		#region ================== Constants
 		
-		private readonly string[] SPRITE_POSTFIXES = new[] {"2C8", "2D8", "2A8", "2B8", "1C1", "1D1", "1A1", "1B1", "A2", "A1", "A0", "2", "1", "0" };
+		//private readonly string[] SPRITE_POSTFIXES = new[] {"2C8", "2D8", "2A8", "2B8", "1C1", "1D1", "1A1", "1B1", "A2", "A1", "A0", "2", "1", "0" };
 		internal const string ACTOR_CLASS_SPECIAL_TOKENS = ":{}\n;,"; //mxd
 
 		#endregion
@@ -678,8 +678,8 @@ namespace CodeImp.DoomBuilder.ZDoom
 				//mxd. Valid when internal or exists
 				if(sprite.StartsWith(DataManager.INTERNAL_PREFIX, StringComparison.OrdinalIgnoreCase) || General.Map.Data.GetSpriteExists(sprite))
 				{
-					result.Sprite = sprite; //mxd
-					return result; 
+					result.Sprite = sprite;
+					return result;
 				}
 
 				//mxd. Bitch and moan
@@ -733,90 +733,10 @@ namespace CodeImp.DoomBuilder.ZDoom
 				}
 			}
 			
-			if(!string.IsNullOrEmpty(result.Sprite))
-			{
-				// The sprite name is not actually complete, we still have to append
-				// the direction characters to it. Find an existing sprite with direction.
-				foreach(string postfix in SPRITE_POSTFIXES)
-				{
-					if(General.Map.Data.GetSpriteExists(result.Sprite + postfix))
-					{
-						result.Sprite += postfix;
-						return result;
-					}
-				}
-			}
-			
-			// No sprite found
+			//mxd. We've found something. Or not...
+			//Info: actual sprites are resolved in ThingTypeInfo.SetupSpriteFrame()
 			return result;
 		}
-
-		//mxd. 
-		///TODO: rewrite this
-		public string FindSuitableVoxel(HashSet<string> voxels) 
-		{
-			string result = string.Empty;
-			
-			// Try the idle state
-			if(HasState("idle")) 
-			{
-				StateStructure s = GetState("idle");
-				StateStructure.FrameInfo info = s.GetSprite(0);
-				if(!string.IsNullOrEmpty(info.Sprite)) result = info.Sprite;
-			}
-
-			// Try the see state
-			if(string.IsNullOrEmpty(result) && HasState("see")) 
-			{
-				StateStructure s = GetState("see");
-				StateStructure.FrameInfo info = s.GetSprite(0);
-				if(!string.IsNullOrEmpty(info.Sprite)) result = info.Sprite;
-			}
-
-			// Try the inactive state
-			if(string.IsNullOrEmpty(result) && HasState("inactive")) 
-			{
-				StateStructure s = GetState("inactive");
-				StateStructure.FrameInfo info = s.GetSprite(0);
-				if(!string.IsNullOrEmpty(info.Sprite)) result = info.Sprite;
-			}
-
-			// Try the spawn state
-			if(string.IsNullOrEmpty(result) && HasState("spawn")) 
-			{
-				StateStructure s = GetState("spawn");
-				StateStructure.FrameInfo info = s.GetSprite(0);
-				if(!string.IsNullOrEmpty(info.Sprite)) result = info.Sprite;
-			}
-
-			// Still no sprite found? then just pick the first we can find
-			if(string.IsNullOrEmpty(result)) 
-			{
-				Dictionary<string, StateStructure> list = GetAllStates();
-				foreach(StateStructure s in list.Values) 
-				{
-					StateStructure.FrameInfo info = s.GetSprite(0);
-					if(!string.IsNullOrEmpty(info.Sprite)) 
-					{
-						result = info.Sprite;
-						break;
-					}
-				}
-			}
-
-			if(!string.IsNullOrEmpty(result)) 
-			{
-				if(voxels.Contains(result)) return result;
-
-				// The sprite name may be incomplete. Find an existing sprite with direction.
-				foreach(string postfix in SPRITE_POSTFIXES)
-					if(voxels.Contains(result + postfix)) return result + postfix;
-			}
-
-
-			// No voxel found
-			return "";
-		}
 		
 		#endregion
 	}
diff --git a/Source/Core/ZDoom/StateStructure.cs b/Source/Core/ZDoom/StateStructure.cs
index 83558ae2c..60a3d7938 100644
--- a/Source/Core/ZDoom/StateStructure.cs
+++ b/Source/Core/ZDoom/StateStructure.cs
@@ -66,8 +66,7 @@ namespace CodeImp.DoomBuilder.ZDoom
 			while(parser.SkipWhitespace(true))
 			{
 				// Read first token
-				string token = parser.ReadToken();
-				token = token.ToLowerInvariant();
+				string token = parser.ReadToken().ToLowerInvariant();
 				
 				// One of the flow control statements?
 				if((token == "loop") || (token == "stop") || (token == "wait") || (token == "fail"))
@@ -147,25 +146,31 @@ namespace CodeImp.DoomBuilder.ZDoom
 					FrameInfo info = new FrameInfo(); //mxd
 					if(spriteframes.Length > 0)
 					{
-						// Make the sprite name
-						string spritename = token + spriteframes[0];
-						spritename = spritename.ToUpperInvariant();
-						
-						// Ignore some odd ZDoom things
-						if(!spritename.StartsWith("TNT1") && !spritename.StartsWith("----") && !spritename.Contains("#"))
+						//mxd. I'm not even 50% sure the parser handles all bizzare cases without shifting sprite name / frame blocks,
+						// so let's log it as a warning, not an error...
+						if(token.Length != 4)
 						{
-							info.Sprite = spritename; //mxd
-							sprites.Add(info);
+							parser.LogWarning("Invalid sprite name \"" + token.ToUpperInvariant() + "\". Sprite names must be exactly 4 characters long");
+						}
+						else
+						{
+							// Make the sprite name
+							string spritename = (token + spriteframes[0]).ToUpperInvariant();
+
+							// Ignore some odd ZDoom things
+							if(!spritename.StartsWith("TNT1") && !spritename.StartsWith("----") && !spritename.Contains("#"))
+							{
+								info.Sprite = spritename; //mxd
+								sprites.Add(info);
+							}
 						}
 					}
 					
 					// Continue until the end of the line
+					parser.SkipWhitespace(false);
 					string t = parser.ReadToken();
 					while(!string.IsNullOrEmpty(t) && t != "\n")
 					{
-						parser.SkipWhitespace(false);
-						t = parser.ReadToken().ToLowerInvariant();
-
 						//mxd. Bright keyword support...
 						if(t == "bright")
 						{
@@ -206,9 +211,23 @@ namespace CodeImp.DoomBuilder.ZDoom
 							// Break out of this loop
 							break;
 						}
-						
+						//mxd. Function params start (those can span multiple lines)
+						else if(t == "(")
+						{
+							int bracelevel = 1;
+							while(!string.IsNullOrEmpty(token) && bracelevel > 0)
+							{
+								parser.SkipWhitespace(true);
+								token = parser.ReadToken();
+								switch(token)
+								{
+									case "(": bracelevel++; break;
+									case ")": bracelevel--; break;
+								}
+							}
+						}
 						//mxd. Because stuff like this is also valid: "Actor Oneliner { States { Spawn: WOOT A 1 A_FadeOut(0.1) Loop }}"
-						if(t == "}")
+						else if(t == "}")
 						{
 							// Rewind so that this scope end can be read again
 							parser.DataStream.Seek(-1, SeekOrigin.Current);
@@ -216,6 +235,10 @@ namespace CodeImp.DoomBuilder.ZDoom
 							// Done here
 							return;
 						}
+
+						// Read next token
+						parser.SkipWhitespace(false);
+						t = parser.ReadToken().ToLowerInvariant();
 					}
 				}
 				
diff --git a/Source/Core/ZDoom/VoxeldefParser.cs b/Source/Core/ZDoom/VoxeldefParser.cs
index 6e6e84bc5..e10183526 100644
--- a/Source/Core/ZDoom/VoxeldefParser.cs
+++ b/Source/Core/ZDoom/VoxeldefParser.cs
@@ -61,13 +61,12 @@ namespace CodeImp.DoomBuilder.ZDoom
 						return false;
 					}
 
-					modelName = StripQuotes(token).ToLowerInvariant();
+					modelName = StripQuotes(token).ToUpperInvariant();
 				} 
 				else if(token == "{") //read the settings
 				{
 					ModelData mde = new ModelData { IsVoxel = true };
 					float scale = 1.0f;
-					float angleoffset = 0;
 
 					while(SkipWhitespace(true)) 
 					{
@@ -79,7 +78,7 @@ namespace CodeImp.DoomBuilder.ZDoom
 							if(!string.IsNullOrEmpty(modelName) && spriteNames.Count > 0) 
 							{
 								mde.ModelNames.Add(modelName);
-								mde.SetTransform(Matrix.RotationZ(Angle2D.DegToRad(angleoffset)), Matrix.Identity, new Vector3(scale));
+								mde.SetTransform(Matrix.RotationZ(Angle2D.DegToRad(mde.AngleOffset)), Matrix.Identity, new Vector3(scale));
 
 								foreach(string s in spriteNames)
 								{
@@ -104,7 +103,7 @@ namespace CodeImp.DoomBuilder.ZDoom
 							if(!NextTokenIs("=")) return false;
 
 							token = ReadToken();
-							if(!ReadSignedFloat(token, ref angleoffset))
+							if(!ReadSignedFloat(token, ref mde.AngleOffset))
 							{
 								// Not numeric!
 								ReportError("Expected AngleOffset value, but got \"" + token + "\"");
diff --git a/Source/Plugins/BuilderModes/VisualModes/BaseVisualGeometrySidedef.cs b/Source/Plugins/BuilderModes/VisualModes/BaseVisualGeometrySidedef.cs
index 73fe4ea78..92e60c009 100644
--- a/Source/Plugins/BuilderModes/VisualModes/BaseVisualGeometrySidedef.cs
+++ b/Source/Plugins/BuilderModes/VisualModes/BaseVisualGeometrySidedef.cs
@@ -635,15 +635,16 @@ namespace CodeImp.DoomBuilder.BuilderModes
 			if(options.FitWidth) 
 			{
 				float scalex, offsetx;
+				float linelength = (float)Math.Round(Sidedef.Line.Length); // Let's use ZDoom-compatible line length here
 
 				if(options.FitAcrossSurfaces) 
 				{
-					scalex = Texture.ScaledWidth / (Sidedef.Line.Length * (options.GlobalBounds.Width / Sidedef.Line.Length)) * options.HorizontalRepeat;
+					scalex = Texture.ScaledWidth / (linelength * (options.GlobalBounds.Width / linelength)) * options.HorizontalRepeat;
 					offsetx = (float)Math.Round((options.Bounds.X * scalex - Sidedef.OffsetX - options.ControlSideOffsetX) % Texture.Width, General.Map.FormatInterface.VertexDecimals);
 				} 
 				else 
 				{
-					scalex = Texture.ScaledWidth / Sidedef.Line.Length * options.HorizontalRepeat;
+					scalex = Texture.ScaledWidth / linelength * options.HorizontalRepeat;
 					offsetx = -Sidedef.OffsetX - options.ControlSideOffsetX;
 				}
 
diff --git a/Source/Plugins/BuilderModes/VisualModes/BaseVisualThing.cs b/Source/Plugins/BuilderModes/VisualModes/BaseVisualThing.cs
index 65c13f67e..9217b07fe 100644
--- a/Source/Plugins/BuilderModes/VisualModes/BaseVisualThing.cs
+++ b/Source/Plugins/BuilderModes/VisualModes/BaseVisualThing.cs
@@ -293,7 +293,7 @@ namespace CodeImp.DoomBuilder.BuilderModes
 					// Determine sprite size and offset
 					float radius = sprite.ScaledWidth * 0.5f;
 					float height = sprite.ScaledHeight;
-					SpriteImage spriteimg = sprite as SpriteImage;
+					ISpriteImage spriteimg = sprite as ISpriteImage;
 					if(spriteimg != null)
 					{
 						offsetx = radius - spriteimg.OffsetX;
-- 
GitLab