Exporting Minecraft data from AutoCAD

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:

Shuttle export and reimport

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.).

Diamond shuttle in MCEdit

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

 

   
60;                  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โ€ฆ

6 responses to “Exporting Minecraft data from AutoCAD”

  1. If we could take Civil 3D and Revit files and convert a land development project into a Minecraft world, my son might see my job a little bit differently ๐Ÿ˜‰

    1. I hear you. That was a big motivating factor for me!

      Kean

  2. A point cloud to Minecraft export should be pretty straightforward, too. Maybe I take on this task. Will there be an Apphack event at this year's AU again? ๐Ÿ˜‰

    1. That would be really cool! Good question on the AppHack... I certainly hope there will be. ๐Ÿ™‚

      Kean

  3. Kean Do you know of any way to export say and MCEdit File to Google Sketchup I need to do this but All I find are ways to do it the other way.
    Any Ideas? I know I'm Not the first person that needs this, there are some other posts that I have come across that need to export from MCEdit maybe you could make a File converter for cad programs?
    Any help would be great Thanks.

    1. Kieran,

      Sorry - I don't have any experience with Sketchup-MCEdit interop.

      Regards,

      Kean

Leave a Reply to Tilo Pfliegner Cancel reply

Your email address will not be published. Required fields are marked *