diff --git a/isosurface.scad b/isosurface.scad index 78192971..6322b4d2 100644 --- a/isosurface.scad +++ b/isosurface.scad @@ -1,16 +1,22 @@ ///////////////////////////////////////////////////////////////////// // LibFile: isosurface.scad // An isosurface is a three-dimensional surface representing points of a constant -// value (e.g. density pressure, temperature, electric field strength, density) in a -// 3D volume. It is essentially a 3D cross-section of a 4-dimensional function. +// value (e.g. pressure, temperature, electric potential, density) in a +// 3D volume. It's the 3D version of a 2D contour; in fact, any 2D cross-section of an +// isosurface *is* a 2D contour. +// . +// For computer-aided design, isosurfaces of abstract functions can generate complex +// curved surfaces and organic-looking shapes. // An isosurface may be represented generally by any function of three variables, -// that is, the function returns a single value based on [x,y,z] inputs. The -// isosurface is defined by all return values equal to a constant isovalue. +// that is, the isosurface of a function $f(x,y,z)$ is the set of points where +// $f(x,y,z)=c$ for some constant value $c$. The constant $c$ is referred to as the "isovalue". // . // A [gryoid](https://en.wikipedia.org/wiki/Gyroid) (often used as a volume infill pattern in [FDM 3D printing](https://en.wikipedia.org/wiki/Fused_filament_fabrication)) // is an exmaple of an isosurface that is unbounded and periodic in all three dimensions. // Other typical examples in 3D graphics are [metaballs](https://en.wikipedia.org/wiki/Metaballs) (also known as "blobby objects"), -// which are bounded and closed organic-looking surfaces that meld together when in close proximity. +// which are bounded and closed organic-looking surfaces that smoothly meld together when in close proximity. +// . +// Below are modules and functions to create 3D models of isosurfaces as well as metaballs of various shapes. // // Includes: // include @@ -23,13 +29,15 @@ /* Lookup Tables for Transvoxel's Modified Marching Cubes -From https://gist.github.com/dwilliamson/72c60fcd287a94867b4334b42a7888ad +Adapted for OpenSCAD from https://gist.github.com/dwilliamson/72c60fcd287a94867b4334b42a7888ad Unlike the original paper (Marching Cubes: A High Resolution 3D Surface Construction Algorithm), these tables guarantee a closed mesh in which connected components are continuous and free of holes. Rotations are prioritized over inversions so that 3 of the 6 cases containing ambiguous faces are never added. 3 extra cases are added as a post-process, overriding inversions through custom-built rotations to eliminate the remaining ambiguities. -The cube index determines the sequence of edges to split. The index ranges from 0 to 255, representing all possible combinations of the 8 corners of the cube being greater or less than the isosurface threshold. For example, 10000110 (8-bit binary for decimal index 134) has corners 2, 3, and 7 greater than the threshold. After determining the cube's index value, the triangulation order is looked up in a table. +The cube index determines the sequence of edges to split. The index ranges from 0 to 255, representing all possible combinations of the 8 corners of the cube being greater or less than the isosurface threshold. + +For example, 10000110 (8-bit binary for decimal index 134) has corners 2, 3, and 7 greater than the threshold. After determining the cube's index value, the triangulation order is looked up in a table. Axes are z @@ -50,7 +58,7 @@ Vertex and edge layout (heavier = and # indicate closer to viewer): #/ #/ #/ #/ 0 +==========+ 4 +=====8=====+ -z changes fastest, then y, then x +z changes fastest, then y, then x. ----------------------------------------------------------- Addition by Alex Matulich: @@ -111,7 +119,7 @@ _MCEdgeVertexIndices = [ [2, 6], ]; -/// For each of the 255 configurations of a marching cube, define a list of triangles, specified as triples of edge indices. +/// For each of the 256 configurations of a marching cube, define a list of triangles, specified as triples of edge indices. _MCTriangleTable = [ [], [3,8,0], @@ -638,9 +646,9 @@ _MCTriangleTable_reverse = [ // SynTags: Geom,VNF // Topics: Isosurfaces, VNF Generators // Usage: As a module -// isosurface(voxel_size, bounding_box, isovalue, field_function, [additional=], [reverse=], [close_clip=], [show_stats=]); +// isosurface(f, isovalue, bounding_box, voxel_size, [reverse=], [closed=], [show_stats=]); // Usage: As a function -// vnf = isosurface(voxel_size, bounding_box, isovalue, field_function, [additional=], [close_clip=], [show_stats=]); +// vnf = isosurface(f, isovalue, bounding_box, voxel_size, [closed=], [show_stats=]); // Description: // When called as a function, returns a [VNF structure](vnf.scad) (list of triangles and faces) representing a 3D isosurface within the specified bounding box at a single isovalue or range of isovalues. // When called as a module, displays the isosurface within the specified bounding box at a single isovalue or range of isovalues. This module just passes the parameters to the function, and then calls {{vnf_polyhedron()}} to display the isosurface. @@ -674,89 +682,78 @@ _MCTriangleTable_reverse = [ // structure to {{vnf_unify_faces()}}. These steps can be expensive for execution time // and are not normally necessary. // Arguments: -// voxel_size = The size (scalar) of the voxel cube that determines the resolution of the surface. -// bounding_box = A pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]`, specifying the minimum and maximum corner coordinates of the bounding box. You don't have ensure that the voxels fit perfectly inside the bounding box. While the voxel at the minimum bounding box corner is aligned on that corner, the last voxel at the maximum box corner may extend a bit beyond it. +// f = A [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) taking as input `x,y,z` coordinates and optional additional parameters, and returns a single value. Say you have you created your own function, `my_func(x,y,z,a,b,c)` (call it whatever you want), which depends on x, y, z, and additional parameters a, b, c, and returns a single value. In the parameter list to `isosurface()`, you would set the `f` parameter to `function (x,y,z) my_func(x,y,z,a,b,c)`. // isovalue = As a scalar, specifies the output value of `field_function` corresponding to the isosurface. As a vector `[min_isovalue, max_isovalue]`, specifies the range of isovalues around which to generate a surface. For closed surfaces, a single value results in a closed volume, and a range results in a shell (with an inside and outside surface) enclosing a volume. A range must be specified for infinite-extent surfaces (such as gyroids) to create a manifold shape within the bounding box. -// field_function = A [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) taking as input an `[x,y,z]` coordinate and optional additional parameters, and returns a single value. +// bounding_box = A pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]`, specifying the minimum and maximum corner coordinates of the bounding box. You don't have ensure that the voxels fit perfectly inside the bounding box. While the voxel at the minimum bounding box corner is aligned on that corner, the last voxel at the maximum box corner may extend a bit beyond it. +// voxel_size = The size (scalar) of the voxel cube that determines the resolution of the surface. // --- -// additional = A single value, or an array of optional additional parameters that may be required by the field function. It is your responsibility to create a function literal compatible with these inputs. If `additional` is not set, only the `[x,y,z]` parameter is passed to the function; no additional parameters are passed. Default: undef // reverse = When true, reverses the orientation of the facets in the mesh. Default: false -// close_clip = When true, maintains a manifold surface where the bounding box clips it (there is a negligible speed penalty in doing this). When false, the bounding box clips the surface, exposing the back sides of facets. Setting this to false can be useful with OpenSCAD's "View > Thrown Together" menu option to distinguish inside from outside. Default: true +// closed = When true, maintains a manifold surface where the bounding box clips it (there is a negligible speed penalty in doing this). When false, the bounding box clips the surface, exposing the back sides of facets. Setting this to false can be useful with OpenSCAD's "View > Thrown Together" menu option to distinguish inside from outside. Default: true // show_stats = If true, display statistics about the isosurface in the console window. Besides the number of voxels found to contain the surface, and the number of triangles making up the surface, this is useful for getting information about a smaller bounding box possible for the isosurface, to improve speed for subsequent renders. Enabling this parameter has a speed penalty. Default: false -// Example(3D,ThrownTogether,NoAxes): A gyroid is an isosurface defined by all the zero values of a 3D periodic function. To illustrate what the surface looks like, `close_clip=false` has been set to expose both sides of the surface. The surface is periodic and tileable along all three axis directions. This a non-manifold surface as displayed, not useful for 3D modeling. This example also demonstrates the use of the `additional` parameter, which in this case controls the wavelength of the gyroid. -// gyroid = function (xyz, wavelength) let( +// Example(3D,ThrownTogether,NoAxes): A gyroid is an isosurface defined by all the zero values of a 3D periodic function. To illustrate what the surface looks like, `closed=false` has been set to expose both sides of the surface. The surface is periodic and tileable along all three axis directions. This a non-manifold surface as displayed, not useful for 3D modeling. This example also demonstrates using an additional parameters in the field function beyond just x,y,z; in this case controls the wavelength of the gyroid. +// function gyroid(x,y,z, wavelength) = let( // p = 360/wavelength, -// px = p*xyz[0], -// py = p*xyz[1], -// pz = p*xyz[2] +// px = p*x, py = p*y, pz = p*z // ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); // // bbox = [[-100,-100,-100], [100,100,100]]; -// isosurface(voxel_size=5, bounding_box=bbox, isovalue=0, -// field_function=gyroid, additional=200, close_clip=false); -// Example(3D,NoAxes): If we remove the `close_clip` parameter or set it to true, the isosurface algorithm encloses the entire half-space bounded by the "inner" gyroid surface, leaving only the "outer" surface exposed. This is a manifold shape but not what we want if trying to model a gyroid. -// gyroid = function (xyz, wavelength) let( +// isosurface(function (x,y,z) gyroid(x,y,z, wavelength=200), +// isovalue=0, bounding_box=bbox, voxel_size=5, +// closed=false); +// Example(3D,NoAxes): If we remove the `closed` parameter or set it to true, the isosurface algorithm encloses the entire half-space bounded by the "inner" gyroid surface, leaving only the "outer" surface exposed. This is a manifold shape but not what we want if trying to model a gyroid. +// function gyroid(x,y,z, wavelength) = let( // p = 360/wavelength, -// px = p*xyz[0], -// py = p*xyz[1], -// pz = p*xyz[2] +// px = p*x, py = p*y, pz = p*z // ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); // // bbox = [[-100,-100,-100], [100,100,100]]; -// isosurface(voxel_size=5, bounding_box=bbox, isovalue=0, -// field_function=gyroid, additional=200); -// Example(3D,ThrownTogether,NoAxes): To make the gyroid a double-sided surface, we need to specify a small range around zero for `isovalue`. Now we have a double-sided surface although with `close_clip=false` the edges are not closed where the surface is clipped by the bounding box. -// gyroid = function (xyz, wavelength) let( +// isosurface(function (x,y,z) gyroid(x,y,z, wavelength=200), +// isovalue=0, bounding_box=bbox, voxel_size=5); +// Example(3D,ThrownTogether,NoAxes): To make the gyroid a double-sided surface, we need to specify a small range around zero for `isovalue`. Now we have a double-sided surface although with `closed=false` the edges are not closed where the surface is clipped by the bounding box. +// function gyroid(x,y,z, wavelength) = let( // p = 360/wavelength, -// px = p*xyz[0], -// py = p*xyz[1], -// pz = p*xyz[2] +// px = p*x, py = p*y, pz = p*z // ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); // // bbox = [[-100,-100,-100], [100,100,100]]; -// isosurface(voxel_size=5, bounding_box=bbox, isovalue=[-0.3, 0.3], -// field_function=gyroid, additional=200, close_clip=false); -// Example(3D,ThrownTogether,NoAxes): To make the gyroid a valid manifold 3D object, we remove the `close_clip` parameter (same as setting `close_clip=true`), which closes the edges where the surface is clipped by the bounding box. The resulting object can be tiled, the VNF returned by the functional version can be wrapped around an axis using {{vnf_bend()}}, and other operations. -// gyroid = function (xyz, wavelength) let( +// isosurface(function (x,y,z) gyroid(x,y,z, wavelength=200), +// isovalue=[-0.3, 0.3], bounding_box=bbox, voxel_size=5, +// closed = false); +// Example(3D,ThrownTogether,NoAxes): To make the gyroid a valid manifold 3D object, we remove the `closed` parameter (same as setting `closed=true`), which closes the edges where the surface is clipped by the bounding box. The resulting object can be tiled, the VNF returned by the functional version can be wrapped around an axis using {{vnf_bend()}}, and other operations. +// function gyroid(x,y,z, wavelength) = let( // p = 360/wavelength, -// px = p*xyz[0], -// py = p*xyz[1], -// pz = p*xyz[2] +// px = p*x, py = p*y, pz = p*z // ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); // // bbox = [[-100,-100,-100], [100,100,100]]; -// isosurface(voxel_size=5, bounding_box=bbox, isovalue=[-0.3, 0.3], -// field_function=gyroid, additional=200); +// isosurface(function (x,y,z) gyroid(x,y,z, wavelength=200), +// isovalue=[-0.3, 0.3], bounding_box=bbox, voxel_size=5); // Example(3D,NoAxes): An approximation of the triply-periodic minimal surface known as [Schwartz P](https://en.wikipedia.org/wiki/Schwarz_minimal_surface). -// schwartz_p = function (xyz, wavelength) let( +// function schwartz_p(x,y,z, wavelength) = let( // p = 360/wavelength, -// px = p*xyz[0], -// py = p*xyz[1], -// pz = p*xyz[2] +// px = p*x, py = p*y, pz = p*z // ) cos(px) + cos(py) + cos(pz); // // bbox = [[-100,-100,-100], [100,100,100]]; -// isosurface(voxel_size=4, bounding_box=bbox, isovalue=[-0.2,0.2], -// field_function=schwartz_p, additional=100); +// isosurface(function (x,y,z) schwartz_p(x,y,z, 100), +// isovalue=[-0.2,0.2], bounding_box=bbox, voxel_size=4); // Example(3D,NoAxes): Another approximation of the triply-periodic minimal surface known as [Neovius](https://en.wikipedia.org/wiki/Neovius_surface). -// neovius = function (xyz, wavelength) let( +// function neovius(x,y,z, wavelength) = let( // p = 360/wavelength, -// px = p*xyz[0], -// py = p*xyz[1], -// pz = p*xyz[2] +// px = p*x, py = p*y, pz = p*z // ) 3*(cos(px) + cos(py) + cos(pz)) + 4*cos(px)*cos(py)*cos(pz); // // bbox = [[-100,-100,-100], [100,100,100]]; -// isosurface(voxel_size=4, bounding_box=bbox, isovalue=[-0.3,0.3], -// field_function=neovius, additional=200); +// isosurface(function (x,y,z) neovius(x,y,z,200), +// isovalue=[-0.3,0.3], bounding_box=bbox, voxel_size=4); -module isosurface(voxel_size, bounding_box, isovalue, field_function, additional, reverse=false, close_clip=true, show_stats=false) { - vnf = isosurface(voxel_size, bounding_box, isovalue, field_function, additional, reverse, close_clip, show_stats); +module isosurface(f, isovalue, bounding_box, voxel_size, reverse=false, closed=true, show_stats=false) { + vnf = isosurface(f, isovalue, bounding_box, voxel_size, reverse, closed, show_stats); vnf_polyhedron(vnf); } -function isosurface(voxel_size, bounding_box, isovalue, field_function, additional, reverse=false, close_clip=true, show_stats=false) = - assert(all_defined([voxel_size, bounding_box, isovalue, field_function]), "The parameters voxel_size, bounding_box, isovalue, and field_function must all be defined.") +function isosurface(f, isovalue, bounding_box, voxel_size, reverse=false, closed=true, show_stats=false) = + assert(all_defined([voxel_size, bounding_box, isovalue, f]), "The parameters f, isovalue, bounding_box, and voxel_size must all be defined.") let( isovalmin = is_list(isovalue) ? isovalue[0] : isovalue, isovalmax = is_list(isovalue) ? isovalue[1] : INF, @@ -765,7 +762,7 @@ function isosurface(voxel_size, bounding_box, isovalue, field_function, addition bbn = (bounding_box[1]-bounding_box[0]+[hv,hv,hv]) / voxel_size, bbsize = [round(bbn[0]), round(bbn[1]), round(bbn[2])] * voxel_size ) [bounding_box[0], bounding_box[0]+bbsize], - cubes = _isosurface_cubes(voxel_size, bbox=newbbox, fieldfunc=field_function, additional=additional, isovalmin=isovalmin, isovalmax=isovalmax, close_clip=close_clip), + cubes = _isosurface_cubes(voxel_size, bbox=newbbox, fieldfunc=f, isovalmin=isovalmin, isovalmax=isovalmax, closed=closed), tritablemin = reverse ? _MCTriangleTable_reverse : _MCTriangleTable, tritablemax = reverse ? _MCTriangleTable : _MCTriangleTable_reverse, trianglepoints = _isosurface_triangles(cubes, voxel_size, isovalmin, isovalmax, tritablemin, tritablemax), @@ -779,9 +776,9 @@ function isosurface(voxel_size, bounding_box, isovalue, field_function, addition // SynTags: Geom,VNF // Topics: Isosurfaces, VNF Generators // Usage: As a module -// isosurface_array(voxel_size, isovalue, fields, [origin=], [reverse=], [close_clip=], [show_stats=]); +// isosurface_array(field, isovalue, voxel_size, [origin=], [reverse=], [closed=], [show_stats=]); // Usage: As a function -// vnf = isosurface_array(voxel_size, isovalue, fields, [origin=], [reverse=], [close_clip=], [show_stats=]); +// vnf = isosurface_array(field, isovalue, voxel_size, [origin=], [reverse=], [closed=], [show_stats=]); // Description: // When called as a function, returns a [VNF structure](vnf.scad) (list of triangles and // faces) representing a 3D isosurface within the passed array at a single isovalue or @@ -800,21 +797,21 @@ function isosurface(voxel_size, bounding_box, isovalue, field_function, addition // The point list in the VNF structure contains many duplicated points. This is not a // problem for rendering the shape, but if you want to eliminate these, you can pass // the structure to {{vnf_merge_points()}}. Additionally, flat surfaces at the outer limits -// of the `fields` array are triangulated at the voxel size +// of the `field` array are triangulated at the voxel size // resolution, and these can be unified into a single face by passing the vnf // structure to {{vnf_unify_faces()}}. These steps can be expensive for execution time // and are not normally necessary. // Arguments: // voxel_size = The size (scalar) of the voxel cube that determines the resolution of the surface. // isovalue = As a scalar, specifies the output value of `field_function` corresponding to the isosurface. As a vector `[min_isovalue, max_isovalue]`, specifies the range of isovalues around which to generate a surface. For closed surfaces, a single value results in a closed volume, and a range results in a shell (with an inside and outside surface) enclosing a volume. A range must be specified for surfaces (such as gyroids) that have both sides exposed within the bounding box. -// fields = 3D array of field intesities. This array should be organized so that the indices are in order of x, y, and z when the array is referenced; that is, `fields[x_index][y_index][z_index]` has `z_index` changing most rapidly as the array is traversed. If you organize the array differently, you may have to perform a `rotate()` or `mirror()` operation on the final result to orient it properly. +// field = 3D array of numbers. This array should be organized so that the indices are in order of x, y, and z when the array is referenced; that is, `field[x_index][y_index][z_index]` has `z_index` changing most rapidly as the array is traversed. If you organize the array differently, you may have to perform a `rotate()` or `mirror()` operation on the final result to orient it properly. // --- -// origin = Origin in 3D space corresponding to `fields[0][0][0]`. The bounding box of the isosurface extends from this origin by multiples of `voxel_size` according to the size of the `fields` array. Default: [0,0,0] +// origin = Origin in 3D space corresponding to `field[0][0][0]`. The bounding box of the isosurface extends from this origin by multiples of `voxel_size` according to the size of the `field` array. Default: [0,0,0] // reverse = When true, reverses the orientation of the facets in the mesh. Default: false -// close_clip = When true, maintains a manifold surface where the bounding box clips it (there is a negligible speed penalty in doing this). When false, the bounding box clips the surface, exposes the back sides of facets. Setting this to false can be useful with OpenSCAD's "View > Thrown together" menu option to distinguish inside from outside. Default: true +// closed = When true, maintains a manifold surface where the bounding box clips it (there is a negligible speed penalty in doing this). When false, the bounding box clips the surface, exposes the back sides of facets. Setting this to false can be useful with OpenSCAD's "View > Thrown together" menu option to distinguish inside from outside. Default: true // show_stats = If true, display statistics about the isosurface in the console window. Besides the number of voxels found to contain the surface, and the number of triangles making up the surface, this is useful for getting information about a smaller bounding box possible for the isosurface, to improve speed for subsequent renders. Enabling this parameter has a speed penalty. Default: false // Example(3D): -// fields = [ +// field = [ // repeat(0,[6,6]), // [ [0,1,2,2,1,0], // [1,2,3,3,2,1], @@ -840,24 +837,24 @@ function isosurface(voxel_size, bounding_box, isovalue, field_function, addition // repeat(0,[6,6]) // ]; // rotate([0,-90,180]) -// isosurface_array(voxel_size=10, -// isovalue=0.5, fields=fields); +// isosurface_array(field, isovalue=0.5, +// voxel_size=10); -module isosurface_array(voxel_size, isovalue, fields, origin=[0,0,0], reverse=false, close_clip=true, show_stats=false) { - vnf = isosurface_array(voxel_size, isovalue, fields, origin, reverse, close_clip, show_stats); +module isosurface_array(field, isovalue, voxel_size, origin=[0,0,0], reverse=false, closed=true, show_stats=false) { + vnf = isosurface_array(field, isovalue, voxel_size, origin, reverse, closed, show_stats); vnf_polyhedron(vnf); } -function isosurface_array(voxel_size, isovalue, fields, origin=[0,0,0], reverse=false, close_clip=true, show_stats=false) = - assert(all_defined([voxel_size, fields, isovalue]), "The parameters voxel_size, fields, and isovalue must all be defined.") +function isosurface_array(field, isovalue, voxel_size, origin=[0,0,0], reverse=false, closed=true, show_stats=false) = + assert(all_defined([field, isovalue, voxel_size]), "The parameters field, isovalue, and voxel_size must all be defined.") let( isovalmin = is_list(isovalue) ? isovalue[0] : isovalue, isovalmax = is_list(isovalue) ? isovalue[1] : INF, bbox = let( - nx = len(fields)-1, - ny = len(fields[0])-1, - nz = len(fields[0][0])-1 + nx = len(field)-1, + ny = len(field[0])-1, + nz = len(field[0][0])-1 ) [origin, origin+[nx*voxel_size, ny*voxel_size, nz*voxel_size]], - cubes = _isosurface_cubes(voxel_size, bbox, fieldarray=fields, isovalmin=isovalmin, isovalmax=isovalmax, close_clip=close_clip), + cubes = _isosurface_cubes(voxel_size, bbox, fieldarray=field, isovalmin=isovalmin, isovalmax=isovalmax, closed=closed), tritablemin = reverse ? _MCTriangleTable_reverse : _MCTriangleTable, tritablemax = reverse ? _MCTriangleTable : _MCTriangleTable_reverse, trianglepoints = _isosurface_triangles(cubes, voxel_size, isovalmin, isovalmax, tritablemin, tritablemax), @@ -878,23 +875,21 @@ function isosurface_array(voxel_size, isovalue, fields, origin=[0,0,0], reverse= /// bfaces is an array of faces corresponding to the sides of the bounding box - this is empty most of the time; it has data only where the isosurface is clipped by the bounding box. /// The bounding box 'bbox' is expected to be quantized for the voxel size already. -function _isosurface_cubes(voxsize, bbox, fieldarray, fieldfunc, additional, isovalmin, isovalmax, close_clip=true) = let( +function _isosurface_cubes(voxsize, bbox, fieldarray, fieldfunc, isovalmin, isovalmax, closed=true) = let( // get field intensities - fields = is_def(fieldarray) + field = is_def(fieldarray) ? fieldarray : let(v = bbox[0], hv = 0.5*voxsize, b1 = bbox[1]+[hv,hv,hv]) [ for(x=[v[0]:voxsize:b1[0]]) [ for(y=[v[1]:voxsize:b1[1]]) [ for(z=[v[2]:voxsize:b1[2]]) - additional==undef - ? fieldfunc([x,y,z]) - : fieldfunc([x,y,z], additional) + fieldfunc(x,y,z) ] ] ], - nx = len(fields)-2, - ny = len(fields[0])-2, - nz = len(fields[0][0])-2, + nx = len(field)-2, + ny = len(field[0])-2, + nz = len(field[0][0])-2, v0 = bbox[0] ) [ for(i=[0:nx]) let(x=v0[0]+voxsize*i) @@ -902,19 +897,19 @@ function _isosurface_cubes(voxsize, bbox, fieldarray, fieldfunc, additional, iso for(k=[0:nz]) let(z=v0[2]+voxsize*k) let(i1=i+1, j1=j+1, k1=k+1, cf = [ - fields[i][j][k], - fields[i][j][k1], - fields[i][j1][k], - fields[i][j1][k1], - fields[i1][j][k], - fields[i1][j][k1], - fields[i1][j1][k], - fields[i1][j1][k1] + field[i][j][k], + field[i][j][k1], + field[i][j1][k], + field[i][j1][k1], + field[i1][j][k], + field[i1][j][k1], + field[i1][j1][k], + field[i1][j1][k1] ], mincf = min(cf), maxcf = max(cf), cubecoord = [x,y,z], - bfaces = close_clip ? _bbox_faces(cubecoord, voxsize, bbox) : [], + bfaces = closed ? _bbox_faces(cubecoord, voxsize, bbox) : [], cubefound_isomin = (mincf<=isovalmin && isovalmin Thrown together" menu option to distinguish inside from outside. Default: true +// closed = When true, maintains a manifold surface where the bounding box clips it (there is a negligible speed penalty in doing this). When false, the bounding box clips the surface, exposing the back sides of facets. Setting this to false can be useful with OpenSCAD's "View > Thrown together" menu option to distinguish inside from outside. Default: true // show_stats = If true, display statistics about the metaball isosurface in the console window. Besides the number of voxels found to contain the surface, and the number of triangles making up the surface, this is useful for getting information about a smaller bounding box possible, to improve speed for subsequent renders. Enabling this parameter has a speed penalty. Default: false // Example(3D,NoAxes): A group of five spherical metaballs with different charges. The parameter `show_stats=true` (not shown here) was used to find a compact bounding box for this figure. -// centers = [[-20,-20,-20], [-0,-20,-20], -// [0,0,0], [0,0,20], [20,20,10] ]; -// charges = [5, 4, 3, 5, 7]; -// type = MB_SPHERE; +// funcs = [ // spheres of different sizes +// move([-20,-20,-20]), mb_sphere(5), +// move([0,-20,-20]), mb_sphere(4), +// move([0,0,0]), mb_sphere(3), +// move([0,0,20]), mb_sphere(5), +// move([20,20,10]), mb_sphere(7) +// ]; // isovalue = 1; // voxelsize = 1.5; // boundingbox = [[-30,-31,-31], [32,31,31]]; -// metaballs(voxelsize, boundingbox, isovalue=isovalue, -// ball_centers=centers, charge=charges, ball_type=type); +// metaballs(funcs, isovalue, boundingbox, voxelsize); // Example(3D,NoAxes): A metaball can have negative charge. In this case we have two metaballs in close proximity, with the small negative metaball creating a dent in the large positive one. The positive metaball is shown transparent, and small spheres show the center of each metaball. The negative metaball isn't visible because its field is negative; the isosurface encloses only field values greater than the isovalue of 1. // centers = [[-1,0,0], [1.25,0,0]]; -// charges = [8, -3]; -// type = MB_SPHERE; +// funcs = [ +// move(centers[0]), mb_sphere(8), +// move(centers[1]), mb_sphere(-3) +// ]; // voxelsize = 0.25; // isovalue = 1; // boundingbox = [[-7,-6,-6], [3,6,6]]; -// -// #metaballs(voxelsize, boundingbox, isovalue=isovalue, -// ball_centers=centers, charge=charges, ball_type=type); +// #metaballs(funcs, isovalue, boundingbox, voxelsize); // color("green") for(c=centers) translate(c) sphere(d=1, $fn=16); // Example(3D,NoAxes): A cube, a rounded cube, and an octahedron interacting. -// centers = [[-7,-3,27], [7,5,21], [10,0,10]]; -// charge = 5; -// type = [MB_CUBE, MB_ROUNDCUBE, MB_OCTAHEDRON]; +// funcs = [ +// move([-7,-3,27]), mb_cube(5), +// move([7,5,21]), mb_roundcube(5), +// move([10,0,10]), mb_octahedron(5) +// ]; // voxelsize = 0.4; // a bit slow at this resolution // isovalue = 1; // boundingbox = [[-13,-9,3], [16,11,33]]; -// -// metaballs(voxelsize, boundingbox, isovalue=isovalue, -// ball_centers=centers, charge=charge, ball_type=type); +// metaballs(funcs, isovalue, boundingbox, voxelsize); // Example(3D,NoAxes): Interaction between two torus-shaped fields in different orientations. -// centers = [[-10,0,17], [7,6,21]]; -// charges = [[6,2], [7,3]]; -// type = MB_TORUS; -// axis_orient = [[0,0,1], [0,1,0]]; +// funcs = [ +// move([-10,0,17]), mb_torus(rbig=6, rsmall=2, axis=[0,0,1]), +// move([7,6,21]), mb_torus(rbig=7, rsmall=3, axis=[0,1,0]) +// ]; // voxelsize = 0.5; // isovalue = 1; // boundingbox = [[-19,-9,9], [18,10,32]]; -// -// metaballs(voxelsize, boundingbox, isovalue=isovalue, -// ball_centers=centers, charge=charges, ball_type=type, -// additional=axis_orient); +// metaballs(funcs, isovalue, boundingbox, voxelsize); // Example(3D): Demonstration of a custom metaball function, in this case a sphere with some random noise added to its electric field. -// noisy_sphere = function (cdist, charge, additional, -// rotation_matrix_unused, rcutoff=INF) +// custom function, 'dv' is internal distance vector +// function noisy_sphere(dv, charge, noise_level, rcutoff) = // let( -// r = norm(cdist) + rands(0, 0.2, 1)[0], +// r = norm(dv) + rands(0, noise_level, 1)[0], // suppress = let(a=min(r,rcutoff)/rcutoff) 1-a*a // ) r==0 ? 1000*charge : suppress*charge / r; // -// centers = [[-9,0,0], [9,0,0]]; -// charge = 5; -// type = [MB_SPHERE, MB_CUSTOM]; -// fieldfuncs = [undef, noisy_sphere]; +// funcs = [ +// move([-9,0,0]), mb_sphere(5), +// move([9,0,0]), function (dv) noisy_sphere(dv, 5, 0.2, INF), +// ]; // voxelsize = 0.4; +// isovalue = 1; // boundingbox = [[-16,-8,-8], [16,8,8]]; -// -// metaballs(voxelsize, boundingbox, isovalue=1, -// ball_centers=centers, charge=charge, ball_type=type, -// field_function=fieldfuncs); +// metaballs(funcs, isovalue, boundingbox, voxelsize); // Example(3D,Med,NoAxes,VPR=[55,0,0],VPD=200,VPT=[7,2,2]): A complex example using ellipsoids, spheres, and a torus to make a tetrahedral object with rounded feet and a ring on top. The bottoms of the feet are flattened by limiting the minimum z value of the bounding box. The center of the object is thick due to the contributions of four ellipsoids converging. Designing an object like this using metaballs requires trial and error with low-resolution renders. // ztheta = 90-acos(-1/3); // cz = cos(ztheta); // sz = sin(ztheta); -// type = [ -// MB_ELLIPSOID, MB_ELLIPSOID, -// MB_ELLIPSOID, MB_ELLIPSOID, -// MB_TORUS, MB_SPHERE, MB_SPHERE, MB_SPHERE +// funcs = [ +// // ellipsoid arms +// move([0,0,20])*yrot(90), +// mb_ellipsoid([6,2,2], cutoff=40), +// move([20*cz,0,20*sz])*yrot(-ztheta), +// mb_ellipsoid([7,2,2], cutoff=40), +// move(zrot(120, p=[20*cz,0,20*sz]))*rot([0,-ztheta,120]), +// mb_ellipsoid([7,2,2], cutoff=40), +// move(zrot(-120, p=[20*cz,0,20*sz]))*rot([0,-ztheta,-120]), +// mb_ellipsoid([7,2,2], cutoff=40), +// // ring on top +// move([0,0,35]), mb_torus(rbig=8, rsmall=2, axis=[0,1,0], cutoff=40), +// // feet +// move([32*cz,0,32*sz]), mb_sphere(5, cutoff=40), +// move(zrot(120, p=[32*cz,0,32*sz])), mb_sphere(5, cutoff=40), +// move(zrot(-120, p=[32*cz,0,32*sz])), mb_sphere(5, cutoff=40) // ]; -// centers = [ -// [0,0,20], [20*cz,0,20*sz], -// zrot(120, p=[20*cz,0,20*sz]), -// zrot(-120, p=[20*cz,0,20*sz]), -// [0,0,35], [32*cz,0,32*sz], -// zrot(120, p=[32*cz,0,32*sz]), -// zrot(-120, p=[32*cz,0,32*sz])]; -// cutoff = 40; // extent of influence of each ball -// rotation = [ -// [0,90,0], [0,-ztheta,0], [0,-ztheta,120], [0,-ztheta,-120], -// [0,0,0], undef, undef, undef]; -// axis = [ -// undef, undef, undef, undef, -// [0,1,0], undef, undef, undef -// ]; -// charge = [ -// [6,2,2], [7,2,2], [7,2,2], [7,2,2], -// [8,2], 5, 5, 5 -// ]; -// // voxelsize = 1; // isovalue = 1; // boundingbox = [[-23,-36,-15], [39,36,46]]; -// // // useful to save as VNF for copies and manipulations -// vnf = metaballs(voxelsize, boundingbox, isovalue=isovalue, ball_centers=centers, -// charge=charge, ball_type=type, additional=axis, rotation=rotation, -// radial_cutoff=cutoff); +// vnf = metaballs(funcs, isovalue, boundingbox, voxelsize); // vnf_polyhedron(vnf); -module metaballs(voxel_size, bounding_box, isovalue, ball_centers, charge=10, ball_type=MB_SPHERE, rotation=undef, field_function=undef, additional=undef, radial_cutoff=INF, close_clip=true, show_stats=false) { - vnf = metaballs(voxel_size, bounding_box, isovalue, ball_centers, charge, ball_type, rotation, field_function, additional, radial_cutoff, close_clip, show_stats); +module metaballs(funcs, isovalue, bounding_box, voxel_size, closed=true, show_stats=false) { + vnf = metaballs(funcs, isovalue, bounding_box, voxel_size, closed, show_stats); vnf_polyhedron(vnf); } -function metaballs(voxel_size, bounding_box, isovalue, ball_centers, charge=10, ball_type=MB_SPHERE, rotation=undef, field_function=undef, additional=undef, radial_cutoff=INF, close_clip=true, show_stats=false) = let( - isoval = is_vector(isovalue) ? isovalue[0] : isovalue, - nballs = len(ball_centers), - chg = is_list(charge) ? charge : repeat(charge, nballs), - interact = is_list(ball_type) ? ball_type : repeat(ball_type, nballs), - rotations = is_list(rotation) ? rotation : repeat(rotation, nballs), - fieldfuncs = is_list(field_function) ? field_function : repeat(field_function, nballs), - addl0 = is_list(additional) ? additional : repeat(additional, nballs), - rlimit = is_list(radial_cutoff) ? radial_cutoff : repeat(radial_cutoff, nballs) -) - assert(all_defined([voxel_size, bounding_box, isovalue, ball_centers]), "\nThe parameters voxel_size, bounding_box, isovalue, and ball centers must all be defined.") - assert(is_list(ball_centers), "\nball_centers must be a list of [x,y,z] coordinates; for a single value use [[x,y,z]].") - assert(len(chg)==nballs, "\nThe list of charges must be equal in length to the list of ball_centers.") - assert(len(interact)==nballs, "\nThe list of ball_types must be equal in length to the list of ball centers.") - assert(len(rotations)==nballs, "\nThe list of rotation vectors must be equal in length to the list of ball centers.") - assert(len(fieldfuncs)==nballs, "\nThe list of field_functions must be equal in length to the list of ball centers.") - assert(len(addl0)==nballs, "\nThe list of additional field function parameters must be equal in length to the list of ball centers.") - assert(len(rlimit)==nballs, "\nThe radial_cutoff list must be equal in length to the list of ball_centers.") +function metaballs(funcs, isovalue, bounding_box, voxel_size, closed=true, show_stats=false) = +assert(all_defined([funcs, isovalue, bounding_box, voxel_size]), "\nThe parameters funcs, isovalue, bounding_box, and voxel_size must all be defined.") +assert(len(funcs)%2==0, "\nThe funcs parameter must be an even-length list of alternating transforms and functions") let( - dum_align = _metaball_errchecks(nballs, interact, chg, addl0, fieldfuncs), - - // change MB_ROUNDCUBE squareness to exponents - addl = [ - for(i=[0:nballs-1]) - if (interact[i]==MB_ROUNDCUBE) - _squircle_se_exponent(addl0[i]==undef ? 0.5 : addl0[i]) - else if (interact[i]==MB_TORUS) - addl0[i]==undef ? [0,0,1] : addl0[i] - else - addl0[i] - ], - - // set up rotation matrices in advance - rotmatrix = [ - for(i=[0:nballs-1]) - rotations[i]==undef ? rot([0,0,0]) : rot(rotations[i]) - ], - - //set up function call array - funcs = [ - _metaball_sphere, //MB_SPHERE - _metaball_ellipsoid, //MB_ELLIPSOID - _metaball_roundcube, //MB_ROUNDCUBE - _metaball_cube, //MB_CUBE - _metaball_octahedron, //MB_OCTAHEDRON - _metaball_torus, //MB_TORUS - fieldfuncs //MB_CUSTOM + isoval = is_vector(isovalue) ? isovalue[0] : isovalue, + f = is_list(funcs) && is_def(funcs[0][0]) ? funcs : [funcs], + nballs = round(len(f) / 2), + // set up transformation matrices in advance + transmatrix = [ + for(i=[0:nballs-1]) let(j=i+i) + assert(is_matrix(f[j],4,4), str("\nfuncs entry at position ", j, " must be a 4×4 matrix.")) + assert(is_function(f[j+1]), str("\nfuncs entry at position ", j+1, "must be a function literal.")) + transpose(submatrix(matrix_inverse(f[j]), [0:2], [0:3])) ], // set up field array @@ -1395,19 +1359,8 @@ let( for(x=[v0[0]:voxel_size:b1[0]+halfvox]) [ for(y=[v0[1]:voxel_size:b1[1]+halfvox]) [ for(z=[v0[2]:voxel_size:b1[2]+halfvox]) - _metaball_fieldfunc([x,y,z], nballs, ball_centers, chg, interact, rotmatrix, addl, rlimit, funcs) + _metaball_fieldfunc([x,y,z], nballs, transmatrix, funcs) ] ] ] -) isosurface_array(voxel_size, isovalue, fieldarray, origin=v0, close_clip=close_clip, show_stats=show_stats); - - -function _metaball_errchecks(nballs, interact, charge, addl0, fieldfuncs) = [ -for(i=[0:nballs-1]) let( - dumm0 = assert(interact[i] != MB_ELLIPSOID || (interact[i]==MB_ELLIPSOID && is_vector(charge[i]) && len(charge[i])==3), "\nThe MB_ELLIPSOID charge value must be a vector of three numbers.") 0, - dumm1 = assert(interact[i] != MB_ROUNDCUBE || (interact[i]==MB_ROUNDCUBE && (is_undef(addl0[i]) || (is_num(addl0[i]) && 0<=addl0[i] && addl0[i]<=1))), "\nFor MB_ROUNDCUBE, additional parameter must be undef or a single number between 0.0 and 1.0.") 0, - dumm2 = assert(interact[i] != MB_TORUS || (interact[i]==MB_TORUS && is_vector(charge[i]) && len(charge[i])==2), "\nThe MB_TORUS charge value must be a vector of two numbers representing major and minor charges.") 0, - dumm4 = assert(interact[i] != MB_TORUS || (interact[i]==MB_TORUS && (addl0[i]==undef || (norm(addl0[i])==1 && sum(addl0[i])==1))), str("\nMB_TORUS ", i, " additional parameters (", addl0[i], ") must be a unit vector in the x, y, or z direction only.")) 0, - dumm5 = assert(interact[i] != MB_CUSTOM || (interact[i]==MB_CUSTOM && is_def(fieldfuncs[i])), "\nThe MB_CUSTOM ball_type requires a field_function to be defined.") 0 - ) 0 -]; +) isosurface_array(fieldarray, isovalue, voxel_size, origin=v0, closed=closed, show_stats=show_stats);