Jump to content
 

Pseudo-Random coal and other loads (OpenSCAD etc)


Recommended Posts

I'm starting a new thread for this topic, rather than further derail this thread: 

 

 

This first post is copied from that thread.

 

I will start with excuses: my workflow is a little convoluted. I tend to make components with OpenSCAD and assembly them with Blender. And I quite often write Python scripts to modify STL files or, in some cases, to create STL files for a particular exotic object.

 

This will only make sense if you install OpenSCAD and try the examples. The starting point for a piece of coal is a low-resolution sphere - the resolution ($fn = 5) is so low that it is actually rendered as two truncated pyramidal cones.

 

coal_r = 1.5; # coal lump "radius" - a large piece of coal if units are mm and the scale is 1/76

sphere(coal_r, $fn = 5);

 

If you enter this into OpenSCAD and render it (F5 or F6) you will get this.

 

a_bit_like_coal.png.526f64709a4beda577bb7c86d34df050.png

 

This is a little bit like a piece of coal, but it needs a bit more randomness in its shape, and of course we need many different pieces. This is achieved by making two such spheres, each of which is stretched/squashed by a random amount in the X, Y and Z directions. Each sphere is then rotated randomly about all three directions. The final result is the intersection of these two spheres.

 

If you enter the following into OpenSCAD...

 

coal_r = 1.5;

module piece_of_coal(){
    scale_first = rands(0.6, 1.4, 3);  // produces an array of 3 random numbers in the range 0.6 to 1.4, for stretching
    scale_second = rands(0.6, 1.4, 3);
    rotate_first = rands(-90,90,3);  // produces an array of 3 random numbers in the range -90 to 90, for rotating
    rotate_second = rands(-90,90,3);

    intersection(){
    rotate(rotate_first)scale(scale_first)sphere(coal_r, $fn = 5);
    rotate(rotate_second)scale(scale_second)sphere(coal_r, $fn = 5);
    }
}
piece_of_coal();

 

... then every time you render it you will get a different piece of coal.

 

  coal_c.png.eb7eb38f124e095509670181b516fb9e.png coal_b.png.a5da4fe5a82ddb0e1231c3e489a617ed.png coal_a.png.29bf5e01d9b077673810fc08e4e20207.png

 

The coal load is made of a collection of these. These have to be pseudo-randomly placed, but it's necessary to exercise some control over where they go. Obviously they need to be inside the bunker or tender, but one might also wish to impose some sort of contour over the top surface.

  • Like 2
  • Interesting/Thought-provoking 1
Link to post
Share on other sites

So now I want multiple pieces of coal to fill a bunker, which will be 18mm (it's not explicit in OpenSCAD, but the units are mm because that's my choice) wide and 10 mm "long". I don't want to fill the bunker - I only want the top layer. For a quick sanity check, I will make a regularly-spaced grid non-random coal:

 

coal_r = 1.5;
separation = 1.5;

bunker_l = 10;
bunker_w= 18;

half_x_count = bunker_l/(2 * separation);
half_y_count = bunker_w/(2 * separation);

module piece_of_coal(){
  sphere(coal_r, $fn = 5);
}

for (x_count = [-half_x_count:half_x_count]) {
  for (y_count = [-half_y_count:half_y_count]) {
    translate([x_count * separation,
      y_count * separation, 0])piece_of_coal();  
  }
}

 

Which produces this awful result:worse_than_no_coal_at_all.png.cd832c4c906c122a3dc50af6f95b845c.png

 

 

  • Like 1
Link to post
Share on other sites

And now I reinstate the random coal.

 

coal_r = 1.5;
separation = 1.5;

bunker_l = 10;
bunker_w= 18;

half_x_count = bunker_l/(2 * separation);
half_y_count = bunker_w/(2 * separation);

module piece_of_coal(){
    scale_first = rands(0.6, 1.4, 3);  // produces an array of 3 random numbers in the range 0.6 to 1.4, for stretching
    scale_second = rands(0.6, 1.4, 3);
    rotate_first = rands(-90,90,3);  // produces an array of 3 random numbers in the range -90 to 90, for rotating
    rotate_second = rands(-90,90,3);

    intersection(){
    rotate(rotate_first)scale(scale_first)sphere(coal_r,
        $fn = 5);
    rotate(rotate_second)scale(scale_second)sphere(coal_r,
        $fn = 5);
    }
}

for (x_count = [-half_x_count:half_x_count]) {
  for (y_count = [-half_y_count:half_y_count]) {
    translate([x_count * separation,
      y_count * separation, 0])piece_of_coal();  
  }
}

 

This is better, but the regular placement is still evident.

 

random_lumps_on_a_regular_grid.png.e2fbeac7a99297a93390fdcdd86715fd.png

 

  • Like 2
Link to post
Share on other sites

To address the regular placement, I'm going to randomly displace each piece from its grid location. This creates some large gaps. These gaps could be addressed in lots of ways - I'm going to do it by reducing the separation.

 

coal_r = 1.5;
separation = 1.2;

bunker_l = 10;
bunker_w = 18;

half_x_count = bunker_l/(2 * separation);
half_y_count = bunker_w/(2 * separation);

module piece_of_coal(){
    scale_first = rands(0.6, 1.4, 3);  // produces an array of 3 random numbers in the range 0.6 to 1.4, for stretching
    scale_second = rands(0.6, 1.4, 3);
    rotate_first = rands(-90,90,3);  // produces an array of 3 random numbers in the range -90 to 90, for rotating
    rotate_second = rands(-90,90,3);

    intersection(){
    rotate(rotate_first)scale(scale_first)sphere(coal_r,
        $fn = 5);
    rotate(rotate_second)scale(scale_second)sphere(coal_r,
        $fn = 5);
    }
}

for (x_count = [-half_x_count:half_x_count]) {
  for (y_count = [-half_y_count:half_y_count]) {
    offsets = rands(-separation/2, separation/2, 3);
    translate([(x_count * separation) + offsets[0],
               (y_count * separation) + offsets[1],
               offsets[2]])piece_of_coal();  
  }
}

 

random_lumps_not_quite_on_the_grid.png.436d599e1da7b20ab9340ab4852f18c4.png

 

It's getting better, but it's still too flat.

 

random_lumps_not_quite_on_the_grid_but_still_flat.png.420e8a77f4914fb6e6d6a0ebb36c259e.png

 

 

 

Edited by TangoOscarMike
  • Like 1
Link to post
Share on other sites

Next I would like a contoured surface - higher in the middle. I'm going to do this with a height function that is zero at the edges, and some predefined height in the middle.

 

After a bit of tinkering with segments of circles, I decided that a parabola was easiest to handle. (1 - x*x) is 1 when x is zero, and zero when x is plus or minus one.

 

And (1 - x * x) * (1 - y * y) is 1 when x and y are both zero, and zero if either of x or y is 1. So (with x and y as my two horizontal dimensions) I can scale this function by bunker_l/2 and bunker_w/2 to fit the horizontal space, and multiply it by my chosen height.

 

coal_r = 1.5;
separation = 1;

bunker_l = 10;
bunker_w= 18;

half_x_count = bunker_l/(2 * separation);
half_y_count = bunker_w/(2 * separation);

coal_h = 2.5; 

function height(X, Y) =
   coal_h * ((1 - ((2 * X / bunker_l)^2)) * (1 - ((2 * Y / bunker_w)^2)));


module piece_of_coal(){
    scale_first = rands(0.6, 1.4, 3);  // produces an array of 3 random numbers in the range 0.6 to 1.4, for stretching
    scale_second = rands(0.6, 1.4, 3);
    rotate_first = rands(-90,90,3);  // produces an array of 3 random numbers in the range -90 to 90, for rotating
    rotate_second = rands(-90,90,3);

    intersection(){
    rotate(rotate_first)scale(scale_first)sphere(coal_r,
        $fn = 5);
    rotate(rotate_second)scale(scale_second)sphere(coal_r,
        $fn = 5);
    }
}

for (x_count = [-half_x_count:half_x_count]) {
  for (y_count = [-half_y_count:half_y_count]) {
      offsets = rands(-separation/2, separation/2, 3);
      x = x_count * separation;
      y = y_count * separation;
      
      translate([x + offsets[0],
                 y + offsets[1],
                 height(x, y) + offsets[2]]) piece_of_coal();  
  }
}

 

heap_from_side_on.png.225afae69e5fd4537a14ceaebf022080.png

 

heap_from_end_on.png.027ae73f95144294ec1598ac3bac34b9.png

 

This created more gaps, which I addressed as before by reducing the separation.

  • Like 1
Link to post
Share on other sites

Now i just want to do a little tidying up. And I think my pieces of coal are a little large, so I'm going to reduce their size (which again, requires a reduction in the separation).

 

I want to trim the sides, and this is done by creating an intersection of the coal (thus far) with a very tall block:

 

coal_r = 1;
separation = 0.6;

bunker_l = 10;
bunker_w = 18;

half_x_count = bunker_l/(2 * separation);
half_y_count = bunker_w/(2 * separation);

coal_h = 2.5; 

function height(X, Y) =
   coal_h * ((1 - ((2 * X / bunker_l)^2)) * (1 - ((2 * Y / bunker_w)^2)));


module piece_of_coal(){
    scale_first = rands(0.6, 1.4, 3);  // produces an array of 3 random numbers in the range 0.6 to 1.4, for stretching
    scale_second = rands(0.6, 1.4, 3);
    rotate_first = rands(-90,90,3);  // produces an array of 3 random numbers in the range -90 to 90, for rotating
    rotate_second = rands(-90,90,3);

    intersection(){
    rotate(rotate_first)scale(scale_first)sphere(coal_r,
        $fn = 5);
    rotate(rotate_second)scale(scale_second)sphere(coal_r,
        $fn = 5);
    }
}

intersection() {
for (x_count = [-half_x_count:half_x_count]) {
  for (y_count = [-half_y_count:half_y_count]) {
      offsets = rands(-separation/2, separation/2, 3);
      x = x_count * separation;
      y = y_count * separation;
      
      translate([x + offsets[0],
                 y + offsets[1],
                 height(x, y) + offsets[2]]) piece_of_coal();  
  }
}
cube([bunker_l, bunker_w, 40], center = true);
}

 

trimmed_coal.png.6854d35c67d2da037c2879d2df484021.png

 

  • Like 3
Link to post
Share on other sites

Nearly there. I want to remove the possibility of holes (which might matter for some purposes) and give the thing a smooth lower surface (just for the sake of tidiness). At the moment the underneath looks like this, which displeases me:

 

trimmed_coal_underside.png.b3bebbf7fa3b7c2cc7c81be1886beeba.png

 

For this I need a block with the same top surface as my coal height contour. The easiest way to do this is also a little cumbersome, because it requires a separate file of data points. I'm creating this separate file with the following Python script.

 

#!/usr/bin/python3

bunker_l = 10
bunker_w = 18
coal_h   = 2.5

def height(X, Y):
    return 20 + coal_h * ((1 - ((2 * X / bunker_l)**2)) * (1 - ((2 * Y / bunker_w)**2)));

min_x = int(-bunker_l/2)
max_x = int(bunker_l/2 + 1)

min_y = int(-bunker_w/2)
max_y = int(bunker_w/2 + 1)

ofile = open('heightmap.txt', 'w')

for x in range(min_x, max_x):
    line_of_output = []
    for y in range(min_y, max_y):
        line_of_output.append(str(height(x,y)))
    ofile.write(' '.join(line_of_output) + '\n')

 

This uses the same height function - the addition of 20 is to ensure that shape appears at the top of a tall block, which will then be translated down to the correct level. The text file is imported to form a solid block like this:

 

translate([0, 0, -20]) surface(file="heightmap.txt", center = true);

 

just_the_block.png.505d2a430e8c6a2666a15aaf168c9590.png

 

This needs to be added to the coal as a union():

 

coal_r = 1;
separation = 0.6;

bunker_l = 10;
bunker_w = 18;

half_x_count = bunker_l/(2 * separation);
half_y_count = bunker_w/(2 * separation);

coal_h = 2.5; 

function height(X, Y) =
   coal_h * ((1 - ((2 * X / bunker_l)^2)) * (1 - ((2 * Y / bunker_w)^2)));


module piece_of_coal(){
    scale_first = rands(0.6, 1.4, 3);  // produces an array of 3 random numbers in the range 0.6 to 1.4, for stretching
    scale_second = rands(0.6, 1.4, 3);
    rotate_first = rands(-90,90,3);  // produces an array of 3 random numbers in the range -90 to 90, for rotating
    rotate_second = rands(-90,90,3);

    intersection(){
    rotate(rotate_first)scale(scale_first)sphere(coal_r,
        $fn = 5);
    rotate(rotate_second)scale(scale_second)sphere(coal_r,
        $fn = 5);
    }
}


union() {
  intersection() {
  for (x_count = [-half_x_count:half_x_count]) {
    for (y_count = [-half_y_count:half_y_count]) {
        offsets = rands(-separation/2, separation/2, 3);
        x = x_count * separation;
        y = y_count * separation;
      
        translate([x + offsets[0],
                   y + offsets[1],
                   height(x, y) + offsets[2]]) piece_of_coal();  
      }
    }
    cube([bunker_l, bunker_w, 40], center = true);
  }
  translate([0, 0, -20])
    
    scale ([1.0001, 1.0001, 1]) surface(file="heightmap.txt", center = true);
}

 

The tiny bit of scaling is to avoid having two surfaces that coincide.

 

block_with_coal.png.5b67ca98c2c422491782870a212f00f3.png

 

If there had been significant smooth surface visible among the coal, I would have lowered the block a bit more.

Edited by TangoOscarMike
  • Like 2
Link to post
Share on other sites

And finally, I subtract the same block from underneath, but lowered down 1mm. It is scaled to be slightly larger than before, in order to ensure a clean unambiguous removal.

 

coal_r = 1;
separation = 0.6;

bunker_l = 10;
bunker_w = 18;

half_x_count = bunker_l/(2 * separation);
half_y_count = bunker_w/(2 * separation);

coal_h = 2.5; 

function height(X, Y) =
   coal_h * ((1 - ((2 * X / bunker_l)^2)) * (1 - ((2 * Y / bunker_w)^2)));


module piece_of_coal(){
    scale_first = rands(0.6, 1.4, 3);  // produces an array of 3 random numbers in the range 0.6 to 1.4, for stretching
    scale_second = rands(0.6, 1.4, 3);
    rotate_first = rands(-90,90,3);  // produces an array of 3 random numbers in the range -90 to 90, for rotating
    rotate_second = rands(-90,90,3);

    intersection(){
    rotate(rotate_first)scale(scale_first)sphere(coal_r,
        $fn = 5);
    rotate(rotate_second)scale(scale_second)sphere(coal_r,
        $fn = 5);
    }
}

difference() {
  union() {
    intersection() {
    for (x_count = [-half_x_count:half_x_count]) {
      for (y_count = [-half_y_count:half_y_count]) {
          offsets = rands(-separation/2, separation/2, 3);
          x = x_count * separation;
          y = y_count * separation;
      
          translate([x + offsets[0],
                     y + offsets[1],
                     height(x, y) + offsets[2]]) piece_of_coal();  
        }
      }
      cube([bunker_l, bunker_w, 40], center = true);
    }
    translate([0, 0, -20])    
      scale ([1.0001, 1.0001, 1]) surface(file="heightmap.txt", center = true);
  }
  translate([0, 0, -21])
    scale ([1.0002, 1.0002, 1]) surface(file="heightmap.txt", center = true);
}

 

final_from_above.png.14fcbfc924aa1bc0508b9f23f7a2439a.png

 

final_from_below.png.52cb452640792822340866595838b214.png

 

There's plenty of scope for more refinement, and for coal in a tender I've used a more complex curve.

  • Like 1
Link to post
Share on other sites

  • RMweb Gold

@TangoOscarMike

I've had a play around with OpenSCAD and your scripts.

I don't know how to do the python bit, so I improvised and came up with an stl I could print.

 

This is the result, using Elegoo Standard Grey resin (the exposure parameters could do with a bit of a tweak, this is just a first attempt):

DSC05796.JPG.ed0a43af4f1e943b793b2739e38019dc.JPG

 

Then with some primer (half dark grey, half black):

DSC05797.JPG.d79c8f52a2ea8770da4587c7c5a87174.JPG

 

And in a (prototype) wagon body:

DSC05798.JPG.20cc93ab386a861ec759232fd991cf11.JPG

 

I'd say that was quite a good result, and better than the moulded coal loads provided by the RTR manufacturers (although to be fair it does still look like moulded coal, not a pile of discrete lumps).

 

I'll make up a "traditional" coal load for comparison and report back...

 

  • Like 4
Link to post
Share on other sites

I have to say I do like the old fashioned way... but I think the printed ones would come up lovely with a wash and a drybrush.

In either case I had not seen OpenSCAD before. That's really exciting and I can't wait to have a play with it at some point.

 

I generally do everything in Blender and would probably have tried this with a displacement map and a picture of some coal. 

Not random as such, but maybe you can rotate/scale/translate the image for each bed of coal.

 

A common use of displacement maps for modelling textures which you may well have seen is those cylindrical texture rollers. I saw a neat tutorial for making 'skull bases' for Warhammer miniatures this way. (Not that I am advocating you fill your trucks with skulls, but the principal is the same as a sort of hybrid digital/modelling approach.)

 

  • Like 1
Link to post
Share on other sites

  • RMweb Gold
10 hours ago, Blefuscu said:

 

A common use of displacement maps for modelling textures which you may well have seen is those cylindrical texture rollers. I saw a neat tutorial for making 'skull bases' for Warhammer miniatures this way. (Not that I am advocating you fill your trucks with skulls, but the principal is the same as a sort of hybrid digital/modelling approach.)

 

So have you tried printing a roller using flexible material?

Link to post
Share on other sites

1 hour ago, simonmcp said:

So have you tried printing a roller using flexible material?

No, I haven't. I think it would be better to use a rigid material though, as it functions like a rolling pin.

 

However, I have used one of the roller tutorials on youtube to make a flat displacement map... I just didn't do the part where you warp the textured surface into a cylinder. (i was creating model road plates/trench plates from an image file.) If you want to retain a lot of detail it can be quite memory intensive as your surface will need to start with a high density of vertices... maybe after applying the displacement you could decimate it if that's an issue.

  • Thanks 1
Link to post
Share on other sites

  • RMweb Gold
2 hours ago, Blefuscu said:

No, I haven't. I think it would be better to use a rigid material though, as it functions like a rolling pin.

 

However, I have used one of the roller tutorials on youtube to make a flat displacement map... I just didn't do the part where you warp the textured surface into a cylinder. (i was creating model road plates/trench plates from an image file.) If you want to retain a lot of detail it can be quite memory intensive as your surface will need to start with a high density of vertices... maybe after applying the displacement you could decimate it if that's an issue.

My computer started to struggle with a complex brick structure so I don't think it would cope with a displacement map warp.

Link to post
Share on other sites

8 hours ago, simonmcp said:

My computer started to struggle with a complex brick structure so I don't think it would cope with a displacement map warp.

You're probably right - the resulting structure would be similar to a brick wall.

Have you tried the decimate modifer?

On my current project my computer started choking at around 350K vertices... although only when that model was in edit mode. I had never had any luck with 'retopology', but the decimate modifier has saved me a few times. Particularly after sculpting. Visually, it seems pretty good at preserving detail, but it can make a dogs dinner of your geometry.
 

I had never had any luck with other 'retopology' methods I tried... although it's supposedly a neater way to reduce resolution.

 

  • Thanks 1
Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...