After last week’s post on importing Minecraft data – in this case from Tinkercad – into AutoCAD, in today’s post we’re going to focus on the ultimately more interesting use case of generating Minecraft data from AutoCAD. We’re going to see some code to dice up 3D AutoCAD geometry and generate blocks in a .schematics file using Substrate.
Our “dicing” process – a term I’ve just coined for iterating through a 3D space, chunk by chunk – is going to use a couple of different approaches for determining there’s any 3D geometry in each grid location. Firstly, though, we going generate a spatial index from the contents of the modelspace – a basic list of bounding boxes with the owning entity’s ObjectId (which could be optimised further by sorting based on location) – to decide whether we want to take a closer look at the geometry we find there.
If we get a “hit” from the spatial index, we can test the associated entity for whether the specific point we’re interested in actual does contain geometry. The specific test will vary based on the type of 3D object we find…
If it’s a Solid3d we can perform a simple test using the CheckInterference() method, passing in a cubic Solid3d occupying the location to test. This works fine, but will generate hits for internal cubes, too (i.e. we end up with a fully solid object, rather than just having blocks representing the shell). Ideally we would union the two solids and check the resultant volume to see if it’s an internal cube or not (if the volume doesn’t change then its internal), but that’s likely to be expensive. The ObjectARX equivalent, AcDb3dSolid::checkInterference() does allow this, but it’s more complicated from .NET. Right now we simply create blocks for internal locations, as well, which may also be what the user wants in many cases.
For Surface objects there’s a bit more to do: here we use ProjectOnToSurface(), passing in a DBPoint, to see whether there’s a point in the block that’s close to the surface. We do this for each of the location cube’s vertices, which may be overkill but seems to give the best results for complex surfaces. Needless to say, we stop checking for “clashes” the first time we get a hit in a particular location – there’s no need to keep looking (although we might want to if we wanted to get the best possible material for a block… for now we’re not worrying about materials at all).
To put the code through its paces, I went ahead and rebuilt a space shuttle model using the code in this previous post (although I performed the loft operations by hand – for some reason these don’t work for me anymore).
I then went and used the EMC command – changing the default block size to 0.1, to make the model more detailed – and then used IMC to reimport it into AutoCAD:
You should bear in mind that AutoCAD’s representation of an imported model is a lot heavier that it would be in Minecraft – each block is a block reference or a 3D solid, depending – so complex models that have trouble being loaded back into AutoCAD will often import just fine into Minecraft.
Here’s the space shuttle schematics file imported into a new world in MCEdit. I chose “diamond” as the material (just to add a bit of a bling factor for my kids), but you could easily hardcode another choice of material or select it based on the geometry’s layer (etc.).
The surface analysis algorithm could use some tweaking – you can see some holes on the sides where the surface is close to vertical, and the base is very thick – but it’s good enough for my purposes.
Here’s the C# code defining the EMC and IMC commands:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using AcDb = Autodesk.AutoCAD.DatabaseServices;
using Substrate;
using System;
using System.Collections.Generic;
namespace Minecraft
{
public class GeometryIndex
{
// We need a list of extents vs. ObjectIds
// (some kind of spatial sorting might help with
// performance, but for now it's just a flat list)
List<Tuple<Extents3d, ObjectId>> _extList;
public GeometryIndex()
{
_extList = new List<Tuple<Extents3d, ObjectId>>();
}
public int Size
{
get { return _extList.Count; }
}
public void PopulateIndex(BlockTableRecord ms, Transaction tr)
{
foreach (var id in ms)
{
var ent =
tr.GetObject(id, OpenMode.ForRead) as
Autodesk.AutoCAD.DatabaseServices.Entity;
if (ent != null)
{
_extList.Add(
new Tuple<Extents3d, ObjectId>(
ent.GeometricExtents, id
)
);
}
}
}
internal ObjectIdCollection PotentialClashes(
Point3d pt, double step
)
{
var res = new ObjectIdCollection();
foreach (var item in _extList)
{
var ext = item.Item1;
if (
pt.X + step >= ext.MinPoint.X &&
pt.X <= ext.MaxPoint.X + step &&
pt.Y + step >= ext.MinPoint.Y &&
pt.Y <= ext.MaxPoint.Y + step &&
pt.Z + step >= ext.MinPoint.Z &&
pt.Z <= ext.MaxPoint.Z + step
)
{
res.Add(item.Item2);
}
}
return res;
}
}
public class Commands
{
// Members that will be set by the EMC command and
// picked up by the IMC command
private double _blockSize = 1.0;
private Point3d _origin = Point3d.Origin;
[CommandMethod("IMC")]
public void ImportMinecraft()
{
var doc = Application.DocumentManager.MdiActiveDocument;
if (doc == null)
return;
var ed = doc.Editor;
var db = doc.Database;
// Request the name of the file to import
var opts =
new PromptOpenFileOptions(
"Import from Minecraft"
);
opts.Filter =
"Minecraft schematic (*.schematic)|*.schematic|" +
"All files (*.*)|*.*";
var pr = ed.GetFileNameForOpen(opts);
if (pr.Status != PromptStatus.OK)
return;
// Read in the selected Schematic file
var schem =
Substrate.ImportExport.Schematic.Import(pr.StringResult);
if (schem == null)
{
ed.WriteMessage("\nCould not find Minecraft schematic.");
return;
}
// Let the user choose the location of the geometry
ed.WriteMessage(
"\nDefault insert is {0}", _origin
);
var ppo = new PromptPointOptions("\nInsertion point or ");
ppo.Keywords.Add("Default");
ppo.AllowNone = true;
var ppr = ed.GetPoint(ppo);
Vector3d offset;
if (ppr.Status == PromptStatus.Keyword)
{
offset = _origin.GetAsVector();
}
else if (ppr.Status == PromptStatus.OK)
{
offset = ppr.Value.GetAsVector();
}
else
{
return;
}
// Let the user choose the size of the block
var pdo = new PromptDoubleOptions("\nEnter block size");
pdo.AllowNegative = false;
pdo.AllowNone = true;
pdo.DefaultValue = _blockSize;
pdo.UseDefaultValue = true;
var pdr = ed.GetDouble(pdo);
if (pdr.Status != PromptStatus.OK)
return;
_blockSize = pdr.Value;
var step = _blockSize;
// We only really care about the blocks
var blks = schem.Blocks;
// We can either create Solid3d objects for each Minecraft
// block, or we can create a BlockTableRecord containing
// a single Solid3d that we reference for each block
// (if useBlock is set to true)
var blkId = ObjectId.Null;
var useBlock = true;
using (var tr = db.TransactionManager.StartTransaction())
{
var bt =
(BlockTable)tr.GetObject(
db.BlockTableId, OpenMode.ForRead
);
if (useBlock)
{
bt.UpgradeOpen();
// Create our block and add it to the db & transaction
var btr = new BlockTableRecord();
btr.Name = "Minecraft Block";
blkId = bt.Add(btr);
tr.AddNewlyCreatedDBObject(btr, true);
// Create our cube and add it to the block & transaction
var cube = new Solid3d();
cube.CreateBox(step, step, step);
btr.AppendEntity(cube);
tr.AddNewlyCreatedDBObject(cube, true);
bt.DowngradeOpen();
}
var ms =
tr.GetObject(
bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite
) as BlockTableRecord;
if (ms != null)
{
using (var pm = new ProgressMeter())
{
pm.Start("Importing Minecraft schematic");
pm.SetLimit(blks.XDim * blks.YDim * blks.ZDim);
// Create a cubic solid for each block
for (int x = 0; x < blks.XDim; ++x)
{
for (int y = 0; y < blks.YDim; ++y)
{
for (int z = 0; z < blks.ZDim; ++z)
{
var blk = blks.GetBlock(x, y, z);
if (blk != null && blk.Info.Name != "Air")
{
// Minecraft has a right-handed coordinate
// system with Z & Y swapped and Z negated
var disp =
new Point3d(x * step, -z * step, y * step) +
offset;
AcDb.Entity ent;
if (useBlock)
{
ent = new BlockReference(disp, blkId);
}
else
{
var sol = new Solid3d();
sol.CreateBox(step, step, step);
sol.TransformBy(
Matrix3d.Displacement(disp.GetAsVector())
);
ent = sol;
}
// Assign the layer based on the material
ent.LayerId =
LayerForMaterial(tr, db, blk.Info.Name);
ms.AppendEntity(ent);
tr.AddNewlyCreatedDBObject(ent, true);
}
pm.MeterProgress();
System.Windows.Forms.Application.DoEvents();
}
}
}
pm.Stop();
System.Windows.Forms.Application.DoEvents();
}
tr.Commit();
}
}
// Zoom to the model's extents
ed.Command("_.ZOOM", "_EXTENTS");
}
private ObjectId LayerForMaterial(
Transaction tr, Database db, string layname
)
{
// If a layer with the material's name exists, return its id
var lt =
(LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead);
if (lt.Has(layname))
{
return lt[layname];
}
// Otherwise create a new layer for this material
var ltr = new LayerTableRecord();
ltr.Name = layname;
lt.UpgradeOpen();
var ltrId = lt.Add(ltr);
lt.DowngradeOpen();
tr.AddNewlyCreatedDBObject(ltr, true);
return ltrId;
}
[CommandMethod("EMC")]
public void ExportMinecraft()
{
var doc = Application.DocumentManager.MdiActiveDocument;
if (doc == null)
return;
var ed = doc.Editor;
var db = doc.Database;
var msId = SymbolUtilityServices.GetBlockModelSpaceId(db);
// Request the name of the file to export to
var opts =
new PromptSaveFileOptions(
"Export to Minecraft"
);
opts.Filter =
"Minecraft schematic (*.schematic)|*.schematic|" +
"All files (*.*)|*.*";
var pr = ed.GetFileNameForSave(opts);
if (pr.Status != PromptStatus.OK)
return;
var idx = new GeometryIndex();
var emin = db.Extmin;
var emax = db.Extmax;
// Let the user choose the size of the block - offer a
// default of a 50th of the diagonal length of the 3D extents
var defSize =
emax.GetAsVector().Subtract(emin.GetAsVector()).Length / 50;
var pdo = new PromptDoubleOptions("\nEnter block size");
pdo.AllowNegative = false;
pdo.AllowNone = true;
pdo.DefaultValue = defSize;
pdo.UseDefaultValue = true;
var pdr = ed.GetDouble(pdo);
if (pdr.Status != PromptStatus.OK)
return;
_blockSize = pdr.Value;
var step = _blockSize;
_origin = new Point3d(emin.X, emax.Y, emin.Z);
ed.WriteMessage(
"\nExporting with block size of {0} at {1}.",
step, _origin
);
// Set up our empty schematic container
var schem =
new Substrate.ImportExport.Schematic(
(int)Math.Ceiling((emax.X - emin.X) / step),
(int)Math.Ceiling((emax.Z - emin.Z) / step),
(int)Math.Ceiling((emax.Y - emin.Y) / step)
);
using (var tr = db.TransactionManager.StartTransaction())
{
// Get our modelspace
var ms =
tr.GetObject(msId, OpenMode.ForRead) as BlockTableRecord;
if (ms != null)
{
// Start by populating the spatial index based on the
// contents of the modelspace
idx.PopulateIndex(ms, tr);
// We'll just use two materials - air and the model
// material (which for now is "diamond")
var air = new AlphaBlock(BlockType.AIR);
var diamond = new AlphaBlock(BlockType.DIAMOND_BLOCK);
using (var cube = new Solid3d())
{
// We'll use a single cube to test interference
cube.CreateBox(step, step, step);
var std2 = step / 2.0;
var vecs =
new Vector3d[]
{
new Vector3d(std2,std2,std2),
new Vector3d(std2,std2,-std2),
new Vector3d(std2,-std2,std2),
new Vector3d(std2,-std2,-std2),
new Vector3d(-std2,std2,std2),
new Vector3d(-std2,std2,-std2),
new Vector3d(-std2,-std2,std2),
new Vector3d(-std2,-std2,-std2)
};
var blks = schem.Blocks;
using (var pm = new ProgressMeter())
{
pm.Start("Exporting Minecraft schematic");
pm.SetLimit(blks.XDim * blks.YDim * blks.ZDim);
for (int x = 0; x < blks.XDim; ++x)
{
for (int y = 0; y < blks.YDim; ++y)
{
for (int z = 0; z < blks.ZDim; ++z)
{
// Get the WCS point to test modespace contents
var wcsX = emin.X + step * x;
var wcsY = emax.Y + step * -z;
var wcsZ = emin.Z + step * y;
var pt = new Point3d(wcsX, wcsY, wcsZ);
// Check our point against bounding boxes
// to detect potential clashes
var ents = idx.PotentialClashes(pt, step);
// If we have some, verify using a more precise,
// per-entity interference check
if (ents.Count > 0)
{
var disp = pt.GetAsVector();
// Displace our interference cube to the
// location we want to test
cube.TransformBy(Matrix3d.Displacement(disp));
bool found = false;
// Check each of the potentially clashing
// entities against our test cube
foreach (ObjectId id in ents)
{
// For Solid3ds we simply check interference
// with our cube
var obj = tr.GetObject(id, OpenMode.ForRead);
var sol = obj as Solid3d;
if (sol != null)
{
if (sol.CheckInterference(cube))
{
// When we've found one we don't need to
// test the others
found = true;
break;
}
}
else
{
// For Surfaces we don't use the cube:
// we create a point at the location
// and project it onto the surface. If
// the resulting point is less than a
// step away, we assume we create the
// block at this location
var sur = obj as AcDb.Surface;
if (sur != null)
{
foreach (var v in vecs)
{
found =
SurfaceClash(sur, pt + v, step);
if (found)
break;
}
}
}
}
// Whether we've found a clash will drive
// whether we set the block to be stone or air
blks.SetBlock(x, y, z, found ? diamond : air);
// Displace the cube back again, ready for the
// next test
cube.TransformBy(Matrix3d.Displacement(-disp));
}
pm.MeterProgress();
System.Windows.Forms.Application.DoEvents();
}
}
}
pm.Stop();
System.Windows.Forms.Application.DoEvents();
}
}
// Finally we write the block information to a schematics
// file
schem.Export(pr.StringResult);
}
tr.Commit();
}
}
private static bool SurfaceClash(
AcDb.Surface sur, Point3d pt, double step
)
{
try
{
var found = false;
using (var dbp = new DBPoint(pt))
{
var ps =
sur.ProjectOnToSurface(
dbp, Vector3d.ZAxis
);
if (ps.Length > 0)
{
foreach (var p in ps)
{
var dbp2 = p as DBPoint;
if (!found && dbp2 != null)
{
// If a discovered point is within 2 block's width
// we consider it a hit
var dist = dbp2.Position - dbp.Position;
found = (dist.Length < 2.0 * step);
}
p.Dispose();
}
}
if (found)
{
return true;
}
}
}
catch { }
return false;
}
}
}
That’s all I currently have planned in terms of Minecraft-related posts, but if anyone has additional suggestions on where to take this, please do share them.
Unrelatedly, Minecraft is in the news a lot at the moment with the prospective acquisition of Mojang by Microsoft. It seems a lot of fans are concerned by this prospect, but one way or another it’ll be interesting to see how it all plays out…