Voxels versus Triangles

We’ve been studying the size of voxel versus triangle models recently. For really complex models we knew that the file size for voxels would be smaller. As you approach one triangle per voxel you end up using significantly more space to express the details. The basic information for a triangle is at least 3 floats to describe its vertices. Compared to using a bit or byte per voxel that quickly adds up. At Shapeways we are starting to see a bunch more data exhibits this type of density. Scanned data, digital fabrics and fractal art all push the limits of what triangle formats can comfortably express. We’re seeing files that compress down to 66% of the original all the way to 4%. This spreadsheet shows a small trial we ran to get an idea of the data.

File Size Results

Now you might be thinking that binary STL files are not the best way to pack 3D data. It’s true that you can find much better formats to transmit triangle data. But what you won’t find is a format that is more widely adopted. About 70% of all uploads are STL. Import and Exporter writers have voted and STL is it. I believe this is related to how simple and stable the format is. It’s a few pages of wiki definition and it hasn’t changed in years. Any solution that is going to replace STL will need to best it in key features and remain as simple to implement. It’s possible to write an STL reader or writer in a night and lots of people have done it.

So how does SVX compare when handling normal files? I’d like to define 3 classes of data. Simple data are models with 25K and make up the bulk of today’s 3D printed models. The last class are Complex data. These models approach the complexity available from a 3D printer. Shapeways sits on an interesting pile of user data with over 3 million user models in our database. For this analysis we took a small sample of user uploads, removed any duplicates and then analyzed models over 25K triangles. What we found was that encoding these models in SVX had an average of a 48% size from binary STL. While we don’t consider the main advantage of using voxels as a space savings operation it is heartening to know it won’t mushroom our bandwidth and disk bills. We’ll want to perform a deeper analysis of many more models to really characterize this change but we feel this gives us a good place to start.

As the industry moves to denser models and multiple materials we think a native voxel format for uploads is necessary. Our users are already pushing the envelope for triangle uploads and by switching to voxels we can enable much higher fidelity designs that will have complete control of each voxel the printer prints.

Posted in Uncategorized | Leave a comment

Image Lathe – Variant

It’s been brought to my attention that our first Image Lathe example exhibited a great flaw. It seems some people think a Vase should be able to hold water! Now around the 3D printing world we talk about water-tight meshes but not usually this definition.

vase_diffuse

Here are the pieces:
react-diffuse2
profile

This code is fairly close to the original Image Lathe example. Instead of intersecting the pattern image with the profile we instead union the pattern with a slightly smaller profile shape. The innerShell is the profile shape that’s rotated to a cylinder that’s patternHeight smaller radius. If you wanted the pattern on the inside of something then you could make the innerShell larger instead.

function getDihedralSymmetry( n){
	var a = PI/(n);
	var cosa = Math.cos(a);
	var sina = Math.sin(a);

	var symm = new ReflectionSymmetry();
	var splanes = new Array();
	var count = 0;
	splanes[count++] = new ReflectionSymmetry.getPlane(new Vector3d(cosa,0,-sina),0);	
	splanes[count++] = new ReflectionSymmetry.getPlane(new Vector3d(-cosa,0, -sina), 0);

    symm.setGroup(splanes);
	return symm;
}

function makeShell(profilePath, width, height, voxelSize){
	var radius = width/2;
	var boxDepth = 2*Math.PI*radius;
	var boxWidth = radius;
	var boxHeight = height;

	var image = new ImageBitmap(profilePath, boxWidth, boxHeight, boxDepth, voxelSize);
	image.setBaseThickness(0.0);
	image.setUseGrayscale(false);
	image.setBlurWidth(2*voxelSize);

	var ct = new CompositeTransform();
	ct.add(new Rotation(0,1,0, -Math.PI/2)); 	
	// align side of the image box with xy lane 
	ct.add(new Translation(0, 0, -radius/2)); 
	ct.add(new RingWrap(radius)); 
	image.setTransform(ct);
	return image;
}

function main(arg){
	var voxelSize = 0.25*MM;

	var vaseWidth = 80*MM;
	var vaseHeight = 100*MM;
	var patternHeight = 4*MM;
	var symmetryOrder = 12;

	var imgPath = arg[0];
	var profilePath = arg[1];

	var img = loadImage(imgPath);
	var imgBoxHeight = vaseHeight;
	var imgBoxWidth = img.getWidth() * imgBoxHeight /img.getHeight();
	var imgBoxThickness = vaseWidth/2; 

	var image = new ImageBitmap(img, imgBoxWidth, imgBoxHeight, imgBoxThickness, voxelSize);

	image.setBaseThickness(0.0);
	image.setUseGrayscale(false);
	image.setBlurWidth(voxelSize);
	image.setImagePlace(ImageBitmap.IMAGE_PLACE_TOP);

	var padding = 2*MM;

	var gWidth = vaseWidth + 2*padding;
	var gHeight = vaseHeight + 2*padding;

	var ct = new CompositeTransform();
	ct.add(new Translation(0,0,-imgBoxThickness/2));
	ct.add(new RingWrap(vaseWidth/2));
	ct.add(getDihedralSymmetry(symmetryOrder));	
	image.setTransform(ct);

	var shell = makeShell(profilePath, vaseWidth, vaseHeight, voxelSize);
	var innerShell = makeShell(profilePath, vaseWidth-2*patternHeight, vaseHeight, voxelSize);

	var intersection = new Intersection(shell, image);
	var union = new Union(innerShell, intersection);	
	dest = createGrid(-gWidth/2,gWidth/2,-gHeight/2,gHeight/2,-gWidth/2,gWidth/2,voxelSize);	

        var maker = new GridMaker();
	maker.setSource(union);

	maker.makeGrid(dest);

	meshSmoothingWidth = 2;
	meshErrorFactor = 0.05;

	return dest;

}
Posted in Uncategorized | Leave a comment

Image Lathe – Aspect Ratio

One important issue when using images for making objects is getting the aspect ratio correct. You can somewhat ignore this at the beginning but eventually you’ll start asking why straight lines are off and how to get the exact wall thickness you want. For the Image Lathe this comes down to using the right aspect ratio for your profile and pattern image. If you use the correct values then your conversion from 2D to 3D will be much more enjoyable.

Let’s start with a candle profile. I want to make the candle be 2.75″ wide and 5.25″ tall. Since we are rotating the profile image around to make the object we want to use 1/2 of the total width which is 1.375″. So we want an image 1.375″ / 5.25″ in aspect ratio(0.262). I used 198 pixels for the width so the height would be 756 pixels.

candle_profile

For the pattern image we need this relationship: Pwidth/Pheight = (vaseWidth/vaseHeight)*(PI/symmetryOrder).

The pattern image aspect ratio is related to the physical dimensions of the object and the symmetry order we are using. For this example I’m making the object be 2.75″ x 5.25″ and using a symmetry order of 12. If it set the width to 396 pixels then the height will need to be 2888. In reality this was way more pixels then I needed for this design so feel free to lower that.

candle_circles

Here is the final object created from this:

candle1

Posted in Uncategorized | Leave a comment

Image Lathe

Let’s start with the good stuff first. Here is a 3D print of a new ShapeJS creator called the Image Lathe. This was printed at Shapeways in Black Glossy Ceramics for $25.

IMG_0631

The basic premise of the creator is to give the user 2 images to control creation of the object. The first image is the profile image. This describes the basic shape of the object. If your familiar with using a pottery wheel or a lathe then you know how this works. You take an outline and sweep it through a 360 degree path. This is great for making a bunch of common shapes like vases, bowls, cups, chess pieces etc. After you have the basic shape then we’ll use another image to make the surface pattern. This image is reflected several times to make an interesting final pattern.

Profile
The profile is created from an image that is rotated in space. This ShapeJS script takes an image and makes a shell out of it. So the 2D images on the left is turned into the 3D vase on the right.
profile
vase_profile

function makeShell(profilePath, width, height, voxelSize){
   var radius = width/2;
   var boxDepth = 2*Math.PI*radius;
   var boxWidth = radius;
   var boxHeight = height;

   var image = new ImageBitmap(profilePath, boxWidth, boxHeight, boxDepth, voxelSize);
   image.setBaseThickness(0.0);
   image.setUseGrayscale(false);
   image.setBlurWidth(2*voxelSize);

   var ct = new CompositeTransform();
   ct.add(new Rotation(0,1,0, -Math.PI/2));
   // align side of the image box with xy lane
   ct.add(new Translation(0, 0, -radius/2));
   ct.add(new RingWrap(radius));
   image.setTransform(ct);

   return image;
}

function main(arg){

   var voxelSize = 0.4*MM;
   var vaseWidth = 80*MM;
   var vaseHeight = 100*MM;
   var profilePath = arg[0];
   var padding = 2*MM;

   var gWidth = vaseWidth + 2*padding;
   var gHeight = vaseHeight + 2*padding;

   var shell = makeShell(profilePath, vaseWidth, vaseHeight, voxelSize);
   dest = createGrid(-gWidth/2,gWidth/2,-gHeight/2,gHeight/2,-gWidth/2,gWidth/2,voxelSize);

   var maker = new GridMaker();
   maker.setSource(shell);
   maker.makeGrid(dest);

   return dest;
}

Image Symmetry
The next step is to take a second image and use that to modify the profile geometry. Here we are going to use the symmetry engine of ShapeJS. This picture shows the setup for a symmetryOrder of 12. Your image is placed in a hall of mirrors. Here 12 planes of reflection are used. The reflection causes the image to invert and then tile. So you get 6 copies of the original + inverted image across the object. The volumetric space looks like pie slices from your image projected from a center point.

image_lathe_symmetry

We take this image from the user:
side_image2

And it generates this geometry using the symmetry engine:

vase_image_symmetry

Final Object

To make the final object we intersect the profile form with the image geometry. Everyplace the image geometry intersects the profile we get material.

vase

Here is the code for the final creator:

function getDihedralSymmetry( n){
   var a = PI/(n);
   var cosa = Math.cos(a);
   var sina = Math.sin(a);

   var symm = new ReflectionSymmetry();
   var splanes = new Array();
   var count = 0;
   splanes[count++] = new ReflectionSymmetry.getPlane(new Vector3d(cosa,0,-sina),0);
   splanes[count++] = new ReflectionSymmetry.getPlane(new Vector3d(-cosa,0, -sina), 0);

   symm.setGroup(splanes);

   return symm;
}

function makeShell(profilePath, width, height, voxelSize){
   var radius = width/2;
   var boxDepth = 2*Math.PI*radius;
   var boxWidth = radius;
   var boxHeight = height;

   var image = new ImageBitmap(profilePath, boxWidth, boxHeight, boxDepth, voxelSize);
   image.setBaseThickness(0.0);
   image.setUseGrayscale(false);
   image.setBlurWidth(2*voxelSize);

   var ct = new CompositeTransform();
   ct.add(new Rotation(0,1,0, -Math.PI/2));
   // align side of the image box with xy lane
   ct.add(new Translation(0, 0, -radius/2));
   ct.add(new RingWrap(radius));
   image.setTransform(ct);
   return image;
}

function main(arg){
   var voxelSize = 0.4*MM;
   var vaseWidth = 80*MM;
   var vaseHeight = 100*MM;
   var symmetryOrder = 12;

   var imgPath = arg[0];
   var profilePath = arg[1];

   var img = loadImage(imgPath);

   var imgBoxHeight = vaseHeight;
   var imgBoxWidth = img.getWidth() * imgBoxHeight /img.getHeight();
   var imgBoxThickness = vaseWidth/2;

   var image = new ImageBitmap(img, imgBoxWidth, imgBoxHeight, imgBoxThickness, voxelSize);

   image.setBaseThickness(0.0);
   image.setUseGrayscale(false);
   image.setBlurWidth(voxelSize);
   image.setImagePlace(ImageBitmap.IMAGE_PLACE_TOP);

   var padding = 2*MM;

   var gWidth = vaseWidth + 2*padding;
   var gHeight = vaseHeight + 2*padding;

   var ct = new CompositeTransform();
   ct.add(new Translation(0,0,-imgBoxThickness/2));
   ct.add(new RingWrap(vaseWidth/2));
   ct.add(getDihedralSymmetry(symmetryOrder));
   image.setTransform(ct);

   var shell = makeShell(profilePath, vaseWidth, vaseHeight, voxelSize);

   var intersection = new Intersection(image, shell);

   dest = createGrid(-gWidth/2,gWidth/2,-gHeight/2,gHeight/2,-gWidth/2,gWidth/2,voxelSize);

   var maker = new GridMaker();
   maker.setSource(intersection);

   maker.makeGrid(dest);

   meshSmoothingWidth = 2;
   meshErrorFactor = 0.05;

   return dest;
}
Posted in Uncategorized | 1 Comment

Journey from Twist to Spring to a new Creator

This post is filled with new ShapeJS features, twist and turns in development and the birth of a new creator. It all started with some prototyping work for this years art project. We’ve been playing with some triple helix designs. Early in the process having some 3D printed prototypes in hand can really help communicate your designs. One way to make a triple helix is using a Twist transform.  A Twist rotates the object around the z-axis.  In this first example we take the union of three boxes and twist them around into a triple helix structure.

twisttwist_orig

function main(args) {

  var vs = 0.1*MM;
  var r = 1*MM;
  var R = 5*MM;
  var length = 30*MM;

  var w = R + r + vs;  
  var sizeZ = length + 4*vs;

  var grid = createGrid(-w,w,-w,w,-sizeZ/2,sizeZ/2,vs);
  var maker = new GridMaker();

  var union = new Union();

  var part1 = new Box(R, 0, 0, r, 4*r, length);
  union.add(part1);

  var part2 = new Box(R, 0, 0, r, 4*r, length);
  part2.setTransform(new Rotation(0,0,1,PI*2/3));
  union.add(part2);

  var part3 = new Box(R, 0, 0, r, 4*r, length);
  part3.setTransform(new Rotation(0,0,1,-PI*2/3));
  union.add(part3);

  union.setTransform(new Twist(period));

  maker.setSource(union);
  maker.makeGrid(grid);
  meshSmoothingWidth = 2;

  return grid;
}

This technique worked well for a few turns. But a twist operation starts to look weird as you crank it. If you want to make a true Spring or Helix for many turns then you need something else. This time we created a new datasource called Spring. It’s a parametric implementation of an infinite spring.

spring3

function main(args) {

  var vs = 0.1*MM;
  var r = 1.47*MM;
  var R = 14.7*MM;
  var springPeriod = 50*MM;
  var springLength = 120*MM;
  var w = R + r + vs;
  var sizeZ = springLength + 2*r + 4*vs;
  var baseHeight = 2*MM;

  var grid = createGrid(-w,w,-w,w,-sizeZ/2,sizeZ/2,vs);
  var maker = new GridMaker();

  var spring1 = new Spring(R,r,springPeriod, springLength);
  var spring2 = new Spring(R,r,springPeriod, springLength);
  var spring3 = new Spring(R,r,springPeriod, springLength);

  spring2.setTransform(new Rotation(0,0,1,2*Math.PI/3));
  spring3.setTransform(new Rotation(0,0,1,2*2*Math.PI/3));
  var union = new Union();
  union.add(spring1);
  union.add(spring2);
  union.add(spring3);

  union.add(new Cylinder(new Vector3d(0,0,-springLength / 2), new Vector3d(0,0,-springLength / 2 - baseHeight), R+r));

  maker.setSource(union);
  maker.makeGrid(grid);
  return grid;
}

This is where our story takes a twist. One of the original creators I made for Shapeways is called the Statement Vase. This creator took a sentence and wrapped it around a cylinder. As we played with these twists and springs I thought it would make a nice variant of that idea. This creator takes an image tile and then wraps it around a cylinder with a specified number of bands. Here is the item 3D printed:

image_twist1

var voxelSize = 0.15*MM;

function makeTile(path, width, height, thickness){
    var img = new ImageBitmap(path, width, height, thickness);
    img.setBaseThickness(0.0);
    img.setVoxelSize(voxelSize);
    img.setBlurWidth(2*voxelSize);
    img.setImagePlace(ImageBitmap.IMAGE_PLACE_BOTH);
    img.setUseGrayscale(false);
    return img;
}

// makes rectangular tiling of a plane 
function getSymmetry(width, height){

    var splanes = new Array();
    var count = 0;

    splanes[count++] = new ReflectionSymmetry.getPlane(new Vector3d(-1,0,0),width/2);
    splanes[count++] = new ReflectionSymmetry.getPlane(new Vector3d(0,-1,0),height/2);
    splanes[count++] = new ReflectionSymmetry.getPlane(new Vector3d(1,0,0),width/2);
    splanes[count++] = new ReflectionSymmetry.getPlane(new Vector3d(0,1,0),height/2);

    return new ReflectionSymmetry(splanes);
}

function main(args) {

    var image = args[0];
    var thickness = 2.2*MM + voxelSize / 2.0;
    var baseThickness = 2.3*MM + voxelSize;
    var tileWidth = 32 *MM;
    var tileHeight = 32 *MM;
    var height = 101.6*MM;

    var wraps = 4; // how many bands
    var tilt = 6;  // how much tilt the bands (should be integer)
    var gapWidth = 0.5; // relative width of gap between bands

    var aspect = 1+ gapWidth;
    var dx = wraps*aspect*tileWidth;
    var dy = tilt*tileHeight;
    var alpha = Math.atan(dy/dx);
    var R = Math.sqrt(dx*dx + dy*dy)/(2*PI); // radius of wrap

    var w = R + thickness + voxelSize;
    var grid = createGrid(-w,w, -height/2, height/2 + baseThickness, -w, w, voxelSize);
    var tile = makeTile(image, tileWidth, tileHeight, thickness);
    var imageTransform = new CompositeTransform();

    imageTransform.add(getSymmetry(aspect*tileWidth, tileHeight));
    imageTransform.add(new Rotation(new Vector3d(0,0,1), alpha));
    imageTransform.add(RingWrap(R));

    tile.setTransform(imageTransform);

    var union = new Union();

    union.add(tile);

    // add base
    union.add(new Cylinder(new Vector3d(0,-height/2,0), new Vector3d(0,-height/2 + baseThickness,0), R+thickness/2));

    // add top
    var rim = new Subtraction(new Cylinder(new Vector3d(0,height/2,0), new Vector3d(0,height/2 + baseThickness,0), R+thickness/2),
        new Cylinder(new Vector3d(0,height/2,0), new Vector3d(0,height/2 + baseThickness,0), R-thickness/2));
    union.add(rim);

    var maker = new GridMaker();
    maker.setSource(union);
    maker.makeGrid(grid);

    meshSmoothingWidth = 1;

    return grid;
}

Now if your watching closely you’ll see that I didn’t actually use Twist or Spring in this new creator. Turns out our RingWarp and Symmetry engine was a better fit for this creator. This allowed us to layout the tiles without any distortion. So there’s the twisted journey, I hope you enjoyed it.

Posted in Uncategorized | 8 Comments

Hollow Out Example

We have a new version of ShapeJS deployed that makes it easy to write a hollow out routine. This allows you to take a solid object and turn it into a shell of specified thickness. Pull up the attached script in ShapeJS, add two params. One text param for the desired thickness(0.001 is 1mm), and a file param for the STL file you want to hollow out.

This script uses two new classes. DistanceTransformMultiStep which calculates the distance from the surface to a voxel. The other class is DensityGridExtractor which makes a density grid from the distance transform, ie it turns those distances into geometry. In combination you can use these two operations to hollow out a model. We calculate the distance of each voxel, then keep all the voxels which are further inside then the thickness we want. We then subtract that geometry from the original model to hollow it out.

I’ve set the default to .2mm for the voxelSize. If your using small objects you may want to set this to .1mm or smaller.

Load the following script into shapejs.shapeways.com, add the params to the interface:

function main(args){
  var thickness = parseFloat(args[0]);
  var baseFile = args[1];
  var voxelSize = 0.2 * MM;
  var maxAttribute = 255;

  var grid = load(baseFile,voxelSize);

  var bounds = java.lang.reflect.Array.newInstance(java.lang.Double.TYPE, 6);
  grid.getGridBounds(bounds);
  var xDist = Math.abs(bounds[1] - bounds[0]);
  var yDist = Math.abs(bounds[3] - bounds[2]);
  var zDist = Math.abs(bounds[5] - bounds[4]);

  var maxDepth = Math.min(Math.min(xDist,yDist),zDist) / 2;
  var maxInDistance = maxDepth + voxelSize;
  var maxOutDistance = voxelSize;

  var distanceGrid = new DistanceTransformMultiStep(maxAttribute, maxInDistance, maxOutDistance);
  var dg = distanceGrid.execute(grid);

  var dge = new DensityGridExtractor(maxDepth, -thickness,dg,maxInDistance,maxOutDistance, maxAttribute);
  var subsurface = createGrid(grid);

  subsurface = dge.execute(subsurface);

  dest = createGrid(grid);

  var maker = new GridMaker();
  maker.setMaxAttributeValue(maxAttribute);
  var dsg1 = new DataSourceGrid(grid,maxAttribute);
  var dsg2 = new DataSourceGrid(subsurface,maxAttribute);

  var result = new Subtraction(dsg1,dsg2);

  maker.setSource(result);
  maker.makeGrid(dest);
  return dest;
}
Posted in Uncategorized | 1 Comment

DensityGridExtraction

We’ve been working with Distance Grids which calculate how far a point is away from the closest surface point. They return a gradient that can be used in different calculations. A common use case is turning that gradient into some geometry. Let’s say you have a 3D model and you want to add some surface detail to it. You could calculate all the voxels within say 2mm of the surface. If you turn this into geometry then you can Intersect that with some volumetric function to create an interesting surface. We’ve created a new class called DistanceGridExtractor for that purpose. It will create a smooth grid that for the inside and outside distances you specify.

Below is a surface created by taking a torus as the main object. I calculated all voxels outside the torus for 2mm. I then turned that into geometry and intersected it with a SchwarzPrimitive volume. It’s cut in half to show the inside.

DensityGridExtractor2DensityGridExtractor

    public void testTorusBumpy(){

        int max_attribute = 127;
        int nx = 400;
        double sphereRadius = 16.0 * MM;
        AttributeGrid grid = makeTorus(nx, sphereRadius, 2 * MM, 
           voxelSize, max_attribute, surfaceThickness);
        double[] bounds = new double[6];
        grid.getGridBounds(bounds);

        double maxInDistance = 0*MM;
        double maxOutDistance = 2.1*MM + voxelSize;

        DistanceTransformExact dt_exact = 
           new DistanceTransformExact(max_attribute, maxInDistance, 
               maxOutDistance);
        AttributeGrid dg_exact = dt_exact.execute(grid);

        DensityGridExtractor dge = new DensityGridExtractor(0, 
           maxOutDistance - voxelSize,
           dg_exact,maxInDistance,maxOutDistance, max_attribute);
        AttributeGrid supersurface = (AttributeGrid) 
           grid.createEmpty(grid.getWidth(), grid.getHeight(), 
              grid.getDepth(), grid.getSliceHeight(), 
              grid.getVoxelSize());
        supersurface.setGridBounds(bounds);
        supersurface = dge.execute(supersurface);

        AttributeGrid dest = (AttributeGrid) 
           grid.createEmpty(grid.getWidth(), grid.getHeight(), 
              grid.getDepth(), grid.getSliceHeight(), 
              grid.getVoxelSize());
        dest.setGridBounds(bounds);

        GridMaker gm = new GridMaker();
        gm.setMaxAttributeValue(max_attribute);

        DataSourceGrid dsg1 = new DataSourceGrid(grid,max_attribute);

        DataSourceGrid dsg2 = new DataSourceGrid(supersurface,
           max_attribute);
        DataSource schwarz = new 
           VolumePatterns.SchwarzPrimitive(2*MM,0.5*MM);
        Intersection pattern = new Intersection(dsg2, schwarz);

        Union result = new Union();
        result.add(pattern);
        Subtraction subtract = new Subtraction(result, 
           new Plane(new Vector3d(0,1,0), new Vector3d(0,0,0)));

        gm.setSource(subtract);
        gm.makeGrid(dest);

        try {
            writeGrid(dest, "bumpy_torus.stl", max_attribute);
        } catch(IOException ioe) {
            ioe.printStackTrace();
        }
    }

Aside | Posted on by | Leave a comment