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 | 4 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

Distance Transform

Distance function visualized through a gyroid volume. Solid red and blue are uncalculated inside/outside areas. Gradients are distance from the surface. Otherwise I thought it just made a cool animation.

This forms the core of applying surface effects at a specific distance. You could hollow a model > some distance from the surface or you could apply a pattern up to some distance on the surface to make it more interesting. Check out DistanceTransformExact.java for the code. We’re working on more optimized versions as well but this class forms the basis for unit testing other approximations. When we get the library level done then we’ll expose this at ShapeJS.

gyroid_distance

Posted in Uncategorized | Leave a comment